ADR 007 revision 8: clause 12, capture-at-open for formula edits; fix stale facts (improvise-sboz, improvise-8vs2)

The adversarial review of 4830fa30 found clause 6 and the km71 section
contradicted each other on a concurrent rename (B1, also Codex's P1 on
29ad14f4). 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:
Edward Langley
2026-09-24 15:51:52 -07:00
parent 4830fa3069
commit fcb8f5397a
+77 -26
View File
@@ -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