ADR 007 revision 8: clause 12, capture-at-open for formula edits; fix stale facts (improvise-sboz, improvise-8vs2)
The adversarial review of4830fa30found clause 6 and the km71 section contradicted each other on a concurrent rename (B1, also Codex's P1 on29ad14f4). Clause 12, the ADR's own reading, fixes the display-name mapping when the formula editor opens, as OpenedCell does for cells, and requires a regression test. fdk1 (#221) and wpxr (#281) have landed, and CellId::Coordinate exists. Status stays proposed pending the user's confirmation of revision 8. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
# ADR 007 — Stable identities for categories, items, formulas and views
|
||||
|
||||
**Status:** proposed (2026-09-24). Revision 7 folds in the user's sixth round, one
|
||||
answer, and **no question is left open**. Revision 4 checked the ADR against work
|
||||
that landed since revision 1. This ADR is not accepted until the user says it is. ·
|
||||
**Status:** proposed (2026-09-24). The user approved revision 7. Revision 8 then
|
||||
followed the adversarial review of that head: it adds clause 12, the ADR's own reading
|
||||
of when a formula edit's names become ids, and corrects stale facts about main. It is
|
||||
not accepted until the user confirms revision 8. ·
|
||||
**Decided by:** the user, in part; see "What the user decided" ·
|
||||
**Ticket:** improvise-sboz · **Unblocks:** improvise-fz77, improvise-fdk1 ·
|
||||
**Revises:** improvise-km71's interim `FormulaId` (landed in PR #214) ·
|
||||
**Ticket:** improvise-sboz · **Unblocks:** improvise-fz77 ·
|
||||
**Revises:** improvise-km71's interim `FormulaId` (landed in PR #214) and
|
||||
improvise-fdk1's `RenameRecordCoordinate` (landed in PR #221) ·
|
||||
**Builds on:** ADR 001, ADR 002 · **Depended on:** improvise-tt22 (remove legacy JSON),
|
||||
which landed in PR #224
|
||||
|
||||
@@ -332,11 +334,14 @@ first message names what that costs:
|
||||
editing its own operation, with an interim id built from that pair (below).
|
||||
- **An edit whose target was concurrently replaced cannot be told apart from an edit of
|
||||
the replacement.**
|
||||
- **A records-mode coordinate rename has no wire form** (improvise-fdk1).
|
||||
- **A records-mode coordinate rename names its category and item by name.**
|
||||
improvise-fdk1 (PR #221) landed `RenameRecordCoordinate { record, category, item }`
|
||||
with `String` operands (below).
|
||||
|
||||
**What already exists.** `Category.id: CategoryId` and `Item.id: ItemId` are `usize`
|
||||
aliases, assigned from monotone counters. **No production code reads either value**;
|
||||
the only reader is one test. Neither has ever been written to `.improv`. `View` has no
|
||||
aliases, assigned from monotone counters. **No production caller uses either value.**
|
||||
`Model::add_category` and `Category::add_item` return them on the idempotent path, and
|
||||
nothing reads the returned value; the only reader is one test. Neither has ever been written to `.improv`. `View` has no
|
||||
id.
|
||||
|
||||
**`FormulaId` exists, as an interim.** km71 landed an opaque `FormulaId` whose private
|
||||
@@ -362,7 +367,7 @@ human name moves to a new `display` field.
|
||||
- **Display names are for people.** They appear on screen, are typed into minibuffers,
|
||||
and are resolved to ids at the input edge.
|
||||
|
||||
Eleven clauses.
|
||||
Twelve clauses.
|
||||
|
||||
### 1. What carries an identity
|
||||
|
||||
@@ -620,12 +625,12 @@ pipe-quotes any display name that would otherwise read as display plus id:
|
||||
- `RenameItem { category, item, to: display }`;
|
||||
- `ReplaceFormula { id, was, raw }`, which km71 already landed with an interim id
|
||||
(below);
|
||||
- fdk1's coordinate rename;
|
||||
- fdk1's `RenameRecordCoordinate`, which landed with name operands (below);
|
||||
- the removes (`RemoveCategory`, `RemoveItem`, `RemoveFormula`, `DeleteView`).
|
||||
- **Older variants** (`SetAxis`, `SetPageSelection`, `HideItem`, …) **must also move to
|
||||
ids**. Their operands are references, and a reference is now an id. They may move one
|
||||
whole variant at a time. If improvise-wpxr (PR #281, open) lands first, its
|
||||
`SetCell { view, … }` and `ClearCell { view, … }` join this list: they name a view by
|
||||
whole variant at a time. That includes `SetCell { view, … }` and
|
||||
`ClearCell { view, … }`, which improvise-wpxr (PR #281, merged) gave a view named by
|
||||
its name, like every other view-scoped variant (improvise-lltt).
|
||||
- **Projections carry id and display together:**
|
||||
- `CategoryRow { id, display, items: Vec<(ItemHandle, String)>, axis }`;
|
||||
@@ -635,7 +640,8 @@ pipe-quotes any display name that would otherwise read as display plus id:
|
||||
(ids are unique, answer 22). `stored` is the id-form text the file carries, and it is what
|
||||
`ReplaceFormula`'s `was` is compared against. So a rename of `Revenue` elsewhere
|
||||
changes what the panel shows but does not refuse an edit of `Profit`, which is
|
||||
the "avoid glitches" of answer 3;
|
||||
the "avoid glitches" of answer 3. What makes that hold for an edit already open
|
||||
is clause 12;
|
||||
- `ViewSummary { id, display, … }`.
|
||||
|
||||
`AxisFrame` labels stay display strings. They gain ids when a consumer needs them.
|
||||
@@ -744,6 +750,43 @@ Following answers 15 and 25:
|
||||
6. **Vocabulary:** the id-addressed variants, `Rejection::Gone`, `ids_named` and
|
||||
`broken_chain`'s id arm.
|
||||
|
||||
### 12. A formula edit's names are fixed when the editor opens
|
||||
|
||||
**This clause is the ADR's own reading, not a user answer.** It closes a contradiction
|
||||
the adversarial review of `4830fa30` found between clause 6 and the km71 section, which
|
||||
is also ChatGPT Codex's P1 on `29ad14f4`.
|
||||
|
||||
**The problem.** The formula panel shows display names (answer 19), and the user edits
|
||||
that text. If the host maps those names to ids at **commit**, a concurrent rename breaks
|
||||
the edit one of two ways:
|
||||
- the name the user typed no longer resolves, so the edit is refused, which contradicts
|
||||
clause 6's "does not refuse an edit of `Profit`";
|
||||
- or the name now belongs to something else (`Revenue` renamed away, another item
|
||||
renamed to `Revenue`), so the edit silently binds to a different thing. That is the
|
||||
glitch answer 3 exists to prevent.
|
||||
|
||||
**The rule.** The display-name-to-id mapping is **captured when the formula editor
|
||||
opens**, and the commit resolves the edited text through that captured mapping. This
|
||||
is the capture-at-open rule the cell editor already follows: `OpenedCell` in
|
||||
`improvise-protocol/src/client.rs` records the record, its version, the document
|
||||
generation and the view at open, and the commit answers for those rather than for
|
||||
whatever the document holds by then.
|
||||
- The mapping covers every display name the panel showed in that formula's text,
|
||||
including the target's.
|
||||
- A name the user types that was **not** in the captured mapping is new to the edit. It
|
||||
is resolved against the document at commit, as any typed name is. If it does not
|
||||
resolve, the edit is refused and says which name.
|
||||
- A captured id that no longer exists at commit is `Rejection::Gone` (clause 7).
|
||||
- `was` is still compared against the stored id-form text (clause 6), so a concurrent
|
||||
*replace* of the formula is still seen.
|
||||
|
||||
**Required regression test.** Open a formula editor on `Profit = Revenue - Cost`. From
|
||||
another client, rename the item `Revenue` to `Sales` and rename a different item to
|
||||
`Revenue`. Commit the unedited text. The formula must still reference the original
|
||||
item's id, and the commit must not be refused. The test must fail under
|
||||
commit-time resolution, which binds to the new `Revenue`, and it must fail under a
|
||||
refusal on rename. Both are the rivals this clause rules out.
|
||||
|
||||
## What this means for you
|
||||
|
||||
### improvise-km71 — editing a formula (landed as an interim)
|
||||
@@ -764,8 +807,9 @@ km71 landed in PR #214, before this ADR, and was built to be revised by it
|
||||
`_Measure` can be lifted, and so can the dangling-reference report core.md §18
|
||||
describes.
|
||||
- The panel shows `Profit (profit) = Revenue - Cost` (answer 19). The user edits that
|
||||
text. The host resolves the display names to ids, and `was` is compared against
|
||||
the stored id-form text (clause 6). The parenthesised id is not the user's to
|
||||
text. Its display names are resolved to ids through the mapping captured when the
|
||||
editor opened (clause 12), not through the names the document holds at commit, and
|
||||
`was` is compared against the stored id-form text (clause 6). The parenthesised id is not the user's to
|
||||
change, since the host assigns ids (answer 14). An edit that changes it is refused.
|
||||
This is the ADR's reading, not a user answer.
|
||||
- **Rejections.** Another client's deletion gives `Rejection::Gone(Formula(id))`. A
|
||||
@@ -789,16 +833,19 @@ km71 landed in PR #214, before this ADR, and was built to be revised by it
|
||||
|
||||
### improvise-fdk1 — renaming a records coordinate
|
||||
|
||||
- The operation is
|
||||
`RenameRecordCoordinate { record: RecordHash, category: CategoryId, to: ItemRef }`,
|
||||
where `ItemRef` is `Existing(ItemId)` or `New(display)`, the create-by-display half of
|
||||
clause 6.
|
||||
fdk1 landed in PR #221, before this ADR. What exists on main:
|
||||
- `ModelCommand::RenameRecordCoordinate { record: RecordHash, category: String, item:
|
||||
String }`, which moves a whole record to a new coordinate and creates the item if need
|
||||
be;
|
||||
- `CellId::Coordinate { record: RecordHash, category: String }`, the surrogate a
|
||||
records-mode coordinate cell carries.
|
||||
|
||||
**What this ADR changes**, as it does for km71's `FormulaId`:
|
||||
- `category` becomes a `CategoryId`, in the command and in `CellId::Coordinate`.
|
||||
- `item` becomes an `ItemRef`: `Existing(ItemId)` or `New(display)`. That is the
|
||||
create-by-display half of clause 6, since the host assigns the new item's id.
|
||||
- Unlike a display rename, this **changes a coordinate**, so the record's `CellKey`s and
|
||||
`RecordHash` change. It is a different record.
|
||||
- Map it onto `RecordRewrite`, which the drill and the plain records path already share
|
||||
(improvise-uo2p).
|
||||
- `CellId::Coordinate` grows `{ record, category: CategoryId }`.
|
||||
- The producer blocker (a browser cannot enter records editing) is untouched.
|
||||
`RecordHash` change. It is a different record, and that does not change.
|
||||
|
||||
## Questions as asked, and where each now stands
|
||||
|
||||
@@ -835,8 +882,12 @@ are answered.**
|
||||
|
||||
## Still open
|
||||
|
||||
Nothing. Every question Q1–Q22 is answered, and answer 29 settles the one case it
|
||||
raises: two identical hand-written ids are refused at load. This ADR still waits on the user's
|
||||
Nothing is open among Q1–Q22, and answer 29 settles the one case it raises: two
|
||||
identical hand-written ids are refused at load. **The user approved revision 7.
|
||||
Revision 8 adds clause 12, which is the ADR's own reading, and the user is asked to
|
||||
confirm or correct it before the ADR is marked Accepted.** Three follow-ups from the
|
||||
same review stay as tickets, not ADR text: improvise-le4p, improvise-m56i and
|
||||
improvise-p003. This ADR still waits on the user's
|
||||
explicit "Approved" before it is Accepted.
|
||||
|
||||
## Consequences
|
||||
|
||||
Reference in New Issue
Block a user