ADR 007: fold in the user's second-round answers; status stays proposed (improvise-sboz)
Thirteen inline answers on 5e46ce03 settle Q5, Q6 and Q8 to Q13. Each is
quoted verbatim with the line it sits on. Q14 is settled in part. The
affected clauses are rewritten:
- slugify: fold to lower case, then strip to [a-z0-9_-], applied to all
id generation;
- old files are read whole-line, selected by an exact version literal;
- the host assigns ids, and create-then-act becomes one command;
- a duplicate id refuses the load;
- ids may be reused across a reload.
Q15 to Q17 are left open: the order of slugify's steps, what the formula
panel shows, and whether hand-written ids may contain capitals.
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,7 +1,7 @@
|
||||
# ADR 007 — Stable identities for categories, items, formulas and views
|
||||
|
||||
**Status:** proposed (2026-09-23). Revision 2 folds in the user's answers; several
|
||||
questions are still open (listed under "Still open"). This ADR is not accepted until the
|
||||
**Status:** proposed (2026-09-23). Revision 3 folds in the user's second round of
|
||||
answers. What is left is listed under "Still open". This ADR is not accepted until the
|
||||
user says it is. ·
|
||||
**Decided by:** the user, in part; see "What the user decided" ·
|
||||
**Ticket:** improvise-sboz · **Unblocks:** improvise-km71, improvise-fz77,
|
||||
@@ -91,6 +91,76 @@ the name as an index. The user's answers make the **id the key** and the name a
|
||||
**display** string. So a rename becomes cheap: it changes one display field and moves no
|
||||
key, no surrogate and no basis tier. The first draft had called that impossible.
|
||||
|
||||
### Second round: answers on revision 2
|
||||
|
||||
The user answered revision 2 (`context/adr/007-stable-identities.md` at `5e46ce03`)
|
||||
with thirteen inline comments on PR #210, posted 2026-09-23 15:03–15:11Z. Each answer
|
||||
is mapped to the question **on the line it is attached to**, and quoted verbatim.
|
||||
|
||||
**9. Q5, group ids.** On line 468, inside Q5 ("Recommendation: not now … the one
|
||||
exception to 'every reference is an id'"):
|
||||
|
||||
> "This seems right"
|
||||
|
||||
Groups get no id in this ADR.
|
||||
|
||||
**10. Q6, per-entity versions.** On line 469, Q6 ("Recommendation: not in this ADR"):
|
||||
|
||||
> "Agreed"
|
||||
|
||||
**11. Q8, a duplicate or unknown id.** On line 473, the recommendation to refuse the
|
||||
load:
|
||||
|
||||
> "Yeah, don't load the file because you can't safely resave it"
|
||||
|
||||
**12. Q9, the reading of "opt-in".** On line 476, the recommendation that an id-less
|
||||
file is read forever and every save writes ids:
|
||||
|
||||
> "Yes"
|
||||
|
||||
**13. Q10, id reuse after a reload.** On line 485, inside option **(b)**, "Accept
|
||||
reuse across a reload (recommended)":
|
||||
|
||||
> "Yes, b"
|
||||
|
||||
This is option (b), and (b) was the recommendation. Within a session, retirement still
|
||||
prevents reuse. Across a save and reload, a removed name that comes back gets its old
|
||||
slug back.
|
||||
|
||||
**14. Q11, who assigns ids.** On line 492, inside option (a):
|
||||
|
||||
> "The host assigns"
|
||||
|
||||
**15. Q12, old-version files and the new header rule.** On line 502, inside option (a),
|
||||
"No (recommended)":
|
||||
|
||||
> "Yes, a as long as version is unambiguously marked"
|
||||
|
||||
Old files are read as they always were, on the condition that the version is marked
|
||||
unambiguously. Clause 9 says how the loader meets that condition.
|
||||
|
||||
**16. Q13, slugify.** Five answers, one per item:
|
||||
- **(a) where it applies.** On line 510:
|
||||
> "All id generation"
|
||||
- **(b) case.** On line 512, against the recommendation to keep case:
|
||||
> "lower case only"
|
||||
- **(c) `_` and `-`.** On line 515:
|
||||
> "Yeah, keep both"
|
||||
- **(d) non-ASCII letters.** On line 518:
|
||||
> "Strip non lower alphanumeric or -_"
|
||||
- **(e) the empty slug.** On line 520, the recommendation `<kind>-<n>`:
|
||||
> "Yes"
|
||||
|
||||
So an id is lower case, and its alphabet is `a`–`z`, `0`–`9`, `-` and `_`. Clause 4
|
||||
states the rule. Q15 asks the user to confirm the order of its steps.
|
||||
|
||||
**17. Q14, formula text.** On line 529, the recommendation "ids in the file":
|
||||
|
||||
> "Display in the formula panel but not as column/row/page headers."
|
||||
|
||||
This does not contradict ids in the file, which answer 3 already requires. It answers
|
||||
**where formula text is displayed**, and it can be read two ways. That is Q16.
|
||||
|
||||
## Context
|
||||
|
||||
**Today the name is the identity.** `Model::categories` is an `IndexMap` keyed by name.
|
||||
@@ -134,7 +204,7 @@ Ten clauses.
|
||||
| formula | yes | the document | `FormulaId` |
|
||||
| view | yes (the user's example) | the document | `ViewId` |
|
||||
| record | `RecordHash`, now over ids (clause 8) | the document | unchanged shape |
|
||||
| group | **still open** (Q5) | | |
|
||||
| group | **no**, not in this ADR (answer 9) | | |
|
||||
|
||||
**Virtual categories keep their reserved names as their ids**: `_Index`, `_Dim` and
|
||||
`_Measure`. A `_Measure` formula target has no item of its own. Its handle is its
|
||||
@@ -152,7 +222,7 @@ Following answers 1 to 3:
|
||||
interns names today.
|
||||
- **View configuration:** the axis maps, page selections, and hidden and collapsed sets
|
||||
are keyed by ids.
|
||||
- **Formulas:** a formula's references are ids (clause 5, and Q14).
|
||||
- **Formulas:** a formula's references are ids (clause 5). What the formula panel shows is Q16.
|
||||
|
||||
**What a rename now costs:** one `display` write. It moves no `CellKey`, no `RecordHash`
|
||||
and no `layout_version`, so an edit a client composed before the rename still lands after
|
||||
@@ -188,21 +258,25 @@ existing `SymbolTable`: copyable, with integer comparison. Two consequences.
|
||||
|
||||
### 4. How an id is made: slugify, one rule everywhere
|
||||
|
||||
Following answer 8, an id is generated from the display name by **slugify**: strip
|
||||
non-alphanumeric characters, replace spaces with hyphens, and collapse runs. The user
|
||||
stated this for CSV and JSON import. **This ADR proposes the same rule for every other
|
||||
creation too**: interactive creation, and assigning ids to an id-less `.improv` file at
|
||||
load. That way a thing's id never depends on how it was made. Whether to extend it that
|
||||
far is Q13(a).
|
||||
Following answers 8 and 16, **every** id the app generates comes from the display
|
||||
name by **slugify**. That covers import, interactive creation, and assigning ids to an
|
||||
id-less `.improv` file at load (answer 16(a), "All id generation"). So a thing's id
|
||||
never depends on how it was made. The steps, in order:
|
||||
|
||||
The user's rule leaves details open. They are gathered in **Q13**, with a
|
||||
recommendation for each:
|
||||
- **case:** keep it or fold it;
|
||||
- **`_` and `-`:** whether they are "non-alphanumeric";
|
||||
- **non-ASCII letters;**
|
||||
- **the empty slug:** what id a name made only of stripped characters gets.
|
||||
1. **Fold to lower case** (answer 16(b), "lower case only").
|
||||
2. **Replace each run of spaces with one `-`** (answer 8).
|
||||
3. **Strip every character that is not `a`–`z`, `0`–`9`, `-` or `_`** (answers 16(c)
|
||||
and 16(d)). This strips non-ASCII letters, so an id never needs quoting.
|
||||
4. **If nothing is left, use `<kind>-<n>`** (answer 16(e)), such as `category-1` or
|
||||
`item-4`, with the lowest free `n`.
|
||||
|
||||
Whatever Q13 settles, three rules are needed and are decided here:
|
||||
Step 1 before step 3 is this ADR's reading of the two answers together. Applied the
|
||||
other way round, "strip non lower" would delete every capital letter, so `East` would
|
||||
become `ast`. **Q15** asks the user to confirm the order. Examples: `East` → `east`,
|
||||
`Time Period` → `time-period`, `Coastal_East` → `coastal_east`, `Q1-2024` → `q1-2024`,
|
||||
`Region(code)` → `regioncode`, `Café` → `caf`, `Ü` → `category-1`.
|
||||
|
||||
Three more rules are decided here:
|
||||
|
||||
- **A slug collision gets a suffix.** Within the id's scope (clause 1), the first thing in
|
||||
creation or file order keeps the slug, and later ones get `-2`, `-3` and so on, the
|
||||
@@ -211,8 +285,12 @@ Whatever Q13 settles, three rules are needed and are decided here:
|
||||
explicit id in the file is collected, then slugs are generated for the rest, suffixed
|
||||
past anything already taken.
|
||||
- **Within a session an id is never reused.** Removing `east` and later adding `East`
|
||||
gives `east-2` while `east` is retired in memory. Whether that retirement survives a
|
||||
save and reload is Q10.
|
||||
gives `east-2` while `east` is retired in memory.
|
||||
- **Across a save and reload, reuse is accepted** (answer 13, Q10(b)). Retired ids are
|
||||
not persisted. So remove `East`, save, reload and add `East`, and the new item gets
|
||||
`east` again. An in-flight command from before the reload is refused by the
|
||||
generation tier. The one remaining risk is an outside reference that outlives the
|
||||
file, and none exists today.
|
||||
|
||||
### 5. Persistence: ids on declarations *and* references
|
||||
|
||||
@@ -221,15 +299,16 @@ Following answers 3 and 7, the file now carries ids everywhere:
|
||||
and the id.
|
||||
- **Every reference** carries the id: axis lines, `hidden:` and `collapsed:`, page
|
||||
selections, `## Aggregation`, `Initial View:`, `## Data` keys, and names inside formula
|
||||
text (Q14).
|
||||
text (answer 3; what the formula panel shows is Q16).
|
||||
|
||||
**Id alphabet.** An id is spelled in slug characters only. Whatever Q13 settles, those
|
||||
are a subset of `('A'..'Z' | 'a'..'z' | '0'..'9' | "_" | "-")`, **written with ranges**,
|
||||
because the generator drops `ASCII_ALPHANUMERIC` (improvise-rxz9). So an id never needs
|
||||
pipe quoting, in a declaration or a reference.
|
||||
**Id alphabet.** A generated id is spelled only in slug characters: `a`–`z`, `0`–`9`,
|
||||
`_` and `-` (clause 4). The grammar spells them **with ranges**, because the generator
|
||||
drops `ASCII_ALPHANUMERIC` (improvise-rxz9). So an id never needs pipe quoting, in a
|
||||
declaration or a reference. Whether a **hand-written** id may also use capitals is
|
||||
**Q17**. The grammar below takes the recommended answer, lower case only.
|
||||
|
||||
```pest
|
||||
id_token = @{ ('A'..'Z' | 'a'..'z' | '0'..'9' | "_" | "-")+ }
|
||||
id_token = @{ ('a'..'z' | '0'..'9' | "_" | "-")+ }
|
||||
// The space is optional (answer 6).
|
||||
id_tag = ${ " "* ~ "(" ~ id_token ~ ")" }
|
||||
|
||||
@@ -245,24 +324,33 @@ id_ref = { id_token }
|
||||
|
||||
`model_name` has no id, so it keeps `header_name`. `Initial View:` becomes a reference
|
||||
and takes an `id_ref`. A group reference (`[Coastal]`) stays a name while groups have no
|
||||
id (Q5).
|
||||
id (answer 9).
|
||||
|
||||
**`header_display` is guarded only by a lookahead**, which the generator skips (gotcha
|
||||
34). So the generator may emit a display ending in something that later parses as an id.
|
||||
Round-trip property 3 still holds, because the first parse settles the split. But
|
||||
property 1 must be stated over the parsed result, not over the generated text.
|
||||
|
||||
**The file version changes.** A file written after this change starts with a new version
|
||||
line.
|
||||
**The file version changes, and it is marked unambiguously** (answer 15's condition).
|
||||
- **Old files carry no ids**, and in them every reference is a display name. So the
|
||||
loader must know which regime it is reading, and the version line is how.
|
||||
- **In an old file**, a reference resolves against display names, and ids are assigned
|
||||
by slugify.
|
||||
- **In a new file**, a reference is an id.
|
||||
- **In an old file**, the file is read exactly as today (clause 9): a header is the
|
||||
whole line, a reference resolves against display names, and ids are assigned by
|
||||
slugify.
|
||||
- **In a new file**, declarations carry ids and a reference is an id.
|
||||
|
||||
Under Q13's recommendation, slugify keeps case, `_` and `-`, so every **bare** name
|
||||
slugifies to itself. Most of an old document's `## Data` therefore reads identically in
|
||||
the new format. Pipe-quoted names are the ones that change.
|
||||
**How the version is unambiguous.** The version line is already mandatory and is
|
||||
matched as an **exact literal** (`version_line = { "v2025-04-09" ~ NEWLINE ~ … }`).
|
||||
The new format gets its own distinct literal. The grammar's `file` rule gets one arm
|
||||
per literal, and each arm uses its own header and reference rules. So the regime is
|
||||
chosen **only** by the first line, never inferred from what the body looks like. A
|
||||
file whose first line is neither literal is refused, as it is today. The new literal's
|
||||
exact spelling is left to the implementation.
|
||||
|
||||
**Slugify folds case (answer 16(b)), so old data keys change spelling on first save.**
|
||||
An old file's `Region=East` resolves at load through the display `East`. The first
|
||||
save writes the id: `Region=East` becomes `region=east`. The data is the same, but a
|
||||
`git diff` across that first save touches every data line.
|
||||
|
||||
**Round-trip properties:**
|
||||
1. an id-less (old-version) file loads, gets ids, and is written in the new version on
|
||||
@@ -274,27 +362,33 @@ A **formula-line generator strategy** is required. The grammar walker emits no `
|
||||
the walker drops such lines, so without its own strategy every formula property is
|
||||
vacuous (improvise-rxz9).
|
||||
|
||||
**A duplicate or unknown id in a hand-edited file can no longer be repaired silently.**
|
||||
The first draft's repair argument rested on "no id is ever referred to", and answer 3
|
||||
reversed that. Reassigning a duplicate would silently change what its references point
|
||||
at. So Q8 is restated below, and the recommendation changes to **refusing the load** with
|
||||
an error naming the id.
|
||||
**A duplicate or unknown id in a hand-edited file refuses the load** (answer 11:
|
||||
"don't load the file because you can't safely resave it"). The error names the id,
|
||||
and for a duplicate it names both declarations. Repairing silently is ruled out,
|
||||
because references carry ids (answer 3): reassigning a duplicate would silently change
|
||||
what its references point at.
|
||||
|
||||
**Legacy JSON is removed** (improvise-tt22), so it needs no migration here.
|
||||
|
||||
**Import** (CSV, JSON) never parses ids out of dimension names (answer 8). A header
|
||||
`Region(code)` is the display name `Region(code)`, and its id is its slug. The writer
|
||||
pipe-quotes any display name that would otherwise read as display plus id:
|
||||
`## Category: |Region(code)|(Regioncode)`.
|
||||
`## Category: |Region(code)|(regioncode)`.
|
||||
|
||||
### 6. The wire: address by id, create by display name, learn ids from projections
|
||||
|
||||
- **The host assigns every id** (answer 14). A client never proposes one.
|
||||
- **Every operand naming an existing thing is an id.** The one operand naming the thing
|
||||
being created is a display name, and the host slugifies it:
|
||||
- `AddCategory(display)` and `CreateView(display)`;
|
||||
- `AddItem { category: CategoryId, display }`,
|
||||
`AddItemInGroup { category: CategoryId, display, group }` and
|
||||
`AddFormula { target_category: CategoryId, raw }`.
|
||||
- **A frame cannot address a thing it creates**, because it does not know the id the
|
||||
host will assign. So a create-then-act compound becomes **one command** carrying
|
||||
both halves. `create-and-switch-view` sends `[CreateView, SwitchView]` today and
|
||||
becomes `CreateView { display, switch: true }`. Any other compound of this shape
|
||||
gets the same treatment.
|
||||
- **New operations are born with ids:**
|
||||
- `RenameCategory { category, to: display }`;
|
||||
- `RenameItem { category, item, to: display }`;
|
||||
@@ -306,8 +400,8 @@ pipe-quotes any display name that would otherwise read as display plus id:
|
||||
whole variant at a time.
|
||||
- **Projections carry id and display together:**
|
||||
- `CategoryRow { id, display, items: Vec<(ItemHandle, String)>, axis }`;
|
||||
- `FormulaCache` rows `{ id, text }`, where the text is rendered with display names
|
||||
for people;
|
||||
- `FormulaCache` rows `{ id, text }`. Whether `text` is rendered over ids or over
|
||||
display names is **Q16**;
|
||||
- `ViewSummary { id, display, … }`.
|
||||
|
||||
`AxisFrame` labels stay display strings. They gain ids when a consumer needs them.
|
||||
@@ -327,7 +421,7 @@ The check has two halves, like the record tier:
|
||||
- **Within the frame:** an **id arm in `broken_chain`** refuses
|
||||
`[RemoveFormula(f), EditFormula(f)]` (improvise-gkyo).
|
||||
|
||||
Per-entity versions are still not added (Q6).
|
||||
Per-entity versions are not added (answer 10).
|
||||
|
||||
### 8. ADR 002: `RecordHash` is now defined over ids
|
||||
|
||||
@@ -349,10 +443,12 @@ it should: it *is* a different record.
|
||||
### 9. Headers that change meaning: the edge case answer 6 asks to document
|
||||
|
||||
Under answer 6, `header_decl` reads a trailing `(token)`, with or without a space before
|
||||
it, as the id. In a **new-version** file the writer never produces an ambiguous header,
|
||||
because it pipe-quotes any display that would split. So the edge cases below arise only
|
||||
when a **new-version** file is hand-written, or when an **old-version** file is read
|
||||
with the new header rule. Whether old files get that rule is **Q12**.
|
||||
it, as the id. **It applies only to new-version files** (answer 15, Q12(a)). An
|
||||
old-version file is read as it always was, so its headers never split. In a new-version
|
||||
file the writer never produces an ambiguous header, because it pipe-quotes any display
|
||||
that would split. So the edge cases below arise **only when a new-version file is
|
||||
written by hand**. The table shows what the header rule would do to lines that exist
|
||||
in old files today. That is why old files do not get the rule.
|
||||
|
||||
| header line | whole-line reading (today) | under `header_decl` |
|
||||
|---|---|---|
|
||||
@@ -361,7 +457,7 @@ with the new header rule. Whether old files get that rule is **Q12**.
|
||||
| `## View: Sales (copy)` | `Sales (copy)` | **display `Sales`, id `copy`** |
|
||||
| `## Category: Region(code)` | `Region(code)` | **display `Region`, id `code`** |
|
||||
| `## Category: Budget (2024)` | `Budget (2024)` | **display `Budget`, id `2024`** |
|
||||
| `## Category: Time Period` | `Time Period` | unchanged; id `Time-Period` generated |
|
||||
| `## Category: Time Period` | `Time Period` | unchanged; id `time-period` generated |
|
||||
|
||||
**The documents that change meaning** are those with a view or category header ending in
|
||||
a parenthesised run of letters, digits, `_` or `-`. **The app writes such headers
|
||||
@@ -369,7 +465,7 @@ itself**: `improvise import` on a CSV with headers like `Region(code)` or `Size(
|
||||
saves `## Category: Region(code)` today (improvise-80rm). Any view someone named
|
||||
`Sales (copy)` does the same.
|
||||
|
||||
**What it costs, if old files get the new rule:**
|
||||
**What it would cost if old files got the new rule** (the case answer 15 rules out):
|
||||
- The category is renamed on load: display `Region`, id `code`.
|
||||
- Every reference in that old file spells `Region(code)`, which resolves to neither the
|
||||
display nor the id.
|
||||
@@ -377,11 +473,10 @@ saves `## Category: Region(code)` today (improvise-80rm). Any view someone named
|
||||
- Today's loader would silently drop the axis lines and orphan the data, so nothing is
|
||||
drawn.
|
||||
|
||||
**What a user would have to do with such a file** is pipe-quote the header before opening
|
||||
it (`## Category: |Region(code)|`). That fixes it, because a pipe-quoted header without
|
||||
an id reads as the whole name.
|
||||
|
||||
Silent data loss on load is a real hazard, so this ADR does not decide it. It is **Q12**.
|
||||
**Decided (answer 15):** old-version files keep the whole-line reading, so none of
|
||||
this happens to a file that exists today. On the first save the writer pipe-quotes
|
||||
such a display, for example `## Category: |Region(code)|(regioncode)`, so the new file
|
||||
cannot split it either.
|
||||
|
||||
### 10. Migration
|
||||
|
||||
@@ -400,9 +495,11 @@ Silent data loss on load is a real hazard, so this ADR does not decide it. It is
|
||||
|
||||
### improvise-km71 — editing a formula
|
||||
|
||||
- **The operation.** The formula panel's rows carry `FormulaId`, and the row text is
|
||||
rendered with display names. Commit sends `EditFormula { formula, raw }`.
|
||||
- The user types `raw` with display names, and the host resolves them to ids.
|
||||
- **The operation.** The formula panel's rows carry `FormulaId`. Commit sends
|
||||
`EditFormula { formula, raw }`.
|
||||
- **What the row text shows, and so what the user edits, is Q16.** If the panel
|
||||
shows ids, `raw` is typed over ids. If it shows display names, the host resolves
|
||||
the names to ids. Build against the answer to Q16, not before it.
|
||||
- The formula is replaced **in place**, keeping its position and id.
|
||||
- **A changed target is the same formula.** Identity is the id. If another formula
|
||||
already owns the new target, the edit is refused. It must not fall through to
|
||||
@@ -444,89 +541,59 @@ Silent data loss on load is a real hazard, so this ADR does not decide it. It is
|
||||
|
||||
## Questions as asked, and where each now stands
|
||||
|
||||
The first draft asked Q1–Q9, and revision 1 added Q10 and Q11. Each original question
|
||||
and recommendation is kept below, with its status.
|
||||
The first draft asked Q1–Q9, revision 1 added Q10 and Q11, and revision 2 added
|
||||
Q12–Q14. Q1–Q14 are all answered. Revision 3 adds Q15–Q17, which come from reading the
|
||||
second round's answers together.
|
||||
|
||||
| # | question | my recommendation | status |
|
||||
|---|---|---|---|
|
||||
| 1 | Integer or word ids? | first draft: words; revision 1: integers only | **answered**: interned symbols (answer 4), generated by slugify (answer 8). Words. |
|
||||
| 1 | Integer or word ids? | first draft: words; revision 1: integers only | **answered**: interned symbols (answer 4), generated by slugify (answers 8, 16). Words. |
|
||||
| 2 | Formula id placement | F1, `- Profit(3) = …` | **answered: F1** (answer 5) |
|
||||
| 3 | Headers in old files | first draft: accept the rare break; revision 1: gate on version | **answered in part**: the space is optional (answer 6). Whether old files get the rule is still open (Q12). |
|
||||
| 3 | Headers in old files | first draft: accept the rare break; revision 1: gate on version | **answered**: the space is optional (answer 6), and old files keep the whole-line reading (answer 15, Q12) |
|
||||
| 4 | Do `## Data` keys carry ids? | no | **answered: yes, ids** (answer 7), reversing the recommendation |
|
||||
| 5 | Group ids? | not now | open |
|
||||
| 6 | Per-entity versions? | not in this ADR | open |
|
||||
| 5 | Group ids? | not now | **answered: not now** (answer 9) |
|
||||
| 6 | Per-entity versions? | not in this ADR | **answered: not in this ADR** (answer 10) |
|
||||
| 7 | `RecordHash` over ids? | no, keep names | **answered: over ids** (answer 3), reversing the recommendation |
|
||||
| 8 | Duplicate id: repair or refuse? | repair | open, **restated below** |
|
||||
| 9 | Reading of "opt-in" | read optional, write always | open |
|
||||
| 10 | Id reuse after a reload | persist a high-water mark | open, **restated below for slug ids** |
|
||||
| 11 | Who assigns ids? | the host | open, **restated below** |
|
||||
| 8 | Duplicate id: repair or refuse? | revision 2: refuse | **answered: refuse the load** (answer 11) |
|
||||
| 9 | Reading of "opt-in" | read optional, write always | **answered: yes** (answer 12) |
|
||||
| 10 | Id reuse after a reload | revision 2: (b) accept reuse | **answered: (b)** (answer 13) |
|
||||
| 11 | Who assigns ids? | the host | **answered: the host** (answer 14) |
|
||||
| 12 | Old files and the new header rule | (a) no | **answered: (a), with the version marked unambiguously** (answer 15) |
|
||||
| 13 | Slugify details | all generation; keep case; keep `_` `-`; strip non-ASCII; `<kind>-<n>` | **answered** (answer 16): all generation, **lower case only** (reversing the recommendation), keep both, strip, `<kind>-<n>` |
|
||||
| 14 | Formula text: ids or display names? | ids in the file | **answered in part** (answer 17): see Q16 |
|
||||
|
||||
## Still open
|
||||
|
||||
5. **Do groups get ids?** **Recommendation: not now.** Nothing addresses a group on the
|
||||
wire. The cost of deferring is that group references (`[Coastal]`, `collapsed:`) stay
|
||||
display names, the one exception to "every reference is an id".
|
||||
6. **Per-entity versions?** **Recommendation: not in this ADR.**
|
||||
8. **A duplicate or unknown id in a hand-edited file: repair or refuse?** *(Restated.)*
|
||||
References now carry ids, so reassigning a duplicate silently re-points every
|
||||
reference to it. **Recommendation: refuse the load**, with an error naming the id and
|
||||
both declarations.
|
||||
9. **"Opt-in": confirm the reading.** **Recommendation:** reading an id-less file is
|
||||
supported forever, as an old-version file whose ids are assigned by slugify, and every
|
||||
save writes the new version with ids.
|
||||
10. **May an id be reused after a save and reload?** *(Restated for slug ids.)* Slug ids
|
||||
are only reused when a removed thing's name comes back: remove `East`, save, reload,
|
||||
add `East`, and it gets `East` again.
|
||||
- **(a)** Persist the retired ids in the file, so the new `East` becomes `East-2`.
|
||||
The list grows forever.
|
||||
- **(b) Accept reuse across a reload (recommended).** Within a session, retirement
|
||||
still prevents reuse. An in-flight command from before the reload is refused by the
|
||||
generation tier. The one remaining risk is an outside reference that outlives the
|
||||
file, and none exists today.
|
||||
- **(c)** No retirement even within a session. That brings back in-session glitches.
|
||||
11. **Who assigns ids?** *(Restated.)* Ids are now derived from names, so a client
|
||||
*could* compute one. `create-and-switch-view` sends `[CreateView, SwitchView]`, a
|
||||
frame that addresses what it creates.
|
||||
- **(a) The host assigns (recommended).** A create-then-act compound becomes one
|
||||
command, for example `CreateView { display, switch: true }`. The host alone
|
||||
resolves collisions and suffixes.
|
||||
- **(b)** The client proposes the slug and the host refuses one that is taken. The
|
||||
frame can then address what it creates, but a collision becomes a refusal the user
|
||||
sees.
|
||||
12. **Do old-version files get the new header rule?** *(New; the hazard answer 6 asked to
|
||||
document.)* See clause 9.
|
||||
- **(a) No (recommended).** An old-version file is read as it always was: a header is
|
||||
the whole line. That is safe because no file written before this change can contain
|
||||
an id, so any `(token)` in its headers is part of the name. The optional-space rule
|
||||
applies to new-version files, where the writer quotes every ambiguous display.
|
||||
Nothing that exists today changes meaning.
|
||||
- **(b) Yes.** Headers like `Region(code)` and `Sales (copy)` split on load, and the
|
||||
data under them silently disappears unless the user pipe-quotes the header first.
|
||||
If this is chosen, the loader should at least **refuse** such a file with an error
|
||||
naming the header, rather than orphaning data silently.
|
||||
13. **Slugify details.** *(New; the rule as stated leaves these open.)*
|
||||
- **(a) Where slugify applies.** The recommendation is *all* id generation:
|
||||
interactive creation and old-file load as well as import. That gives one rule, and
|
||||
bare names slugify to themselves.
|
||||
- **(b) Case.** Recommendation: **keep it**, so `East` stays `East` and old data keys
|
||||
read the same.
|
||||
- **(c) `_` and `-`.** Recommendation: **keep both.** They are already legal in bare
|
||||
names, so every existing bare name is its own slug. The literal "strip
|
||||
non-alphanumeric" would turn `Coastal_East` into `CoastalEast` and `Q1-2024` into
|
||||
`Q12024`.
|
||||
- **(d) Non-ASCII letters.** Recommendation: **strip them** (ASCII only), so an id
|
||||
never needs quoting. A name made only of such letters falls to (e).
|
||||
- **(e) The empty slug.** Recommendation: `<kind>-<n>` (for example `category-1` or
|
||||
`item-4`), with the lowest free `n`.
|
||||
14. **Formula text in the file: ids or display names?** *(New; a consequence of answer 3
|
||||
that is visible in the file.)* Answer 3 says every reference is an id, so a formula
|
||||
line would be written `- Profit(Profit) = Revenue - Cost` over ids. With Q13's
|
||||
recommendations the ids equal the bare display names, so most formulas read the same.
|
||||
A display rename changes the UI rendering and not the file. The cost is that
|
||||
`Formula::raw` is no longer stored verbatim: the file gets a normalized rendering over
|
||||
ids, and the UI a rendering over display names. **Recommendation: ids in the file**,
|
||||
per answer 3. Otherwise every rename must rewrite formula text, which is exactly the
|
||||
glitch answer 3 is avoiding.
|
||||
15. **The order of slugify's steps: confirm "fold, then strip".** Answer 16(b) says
|
||||
"lower case only" and answer 16(d) says "Strip non lower alphanumeric or -_". Clause
|
||||
4 folds to lower case first and strips second, so `East` → `east`. Stripping first
|
||||
would delete the capitals, so `East` → `ast`, which is surely not meant.
|
||||
**Recommendation: fold, then strip.**
|
||||
16. **What the formula panel shows.** Answer 17, on Q14, is "Display in the formula
|
||||
panel but not as column/row/page headers." Both readings below keep ids in the
|
||||
file, and both keep display names on grid headers. They differ on what the formula
|
||||
panel shows, and so on what km71 edits:
|
||||
- **(a) The panel shows formula text over ids**, the file's own form:
|
||||
`profit = revenue - cost`. The grid headers show display names (`Profit`). One
|
||||
rendering, `raw` is edited as stored, and nothing resolves display names in
|
||||
formula text. The cost is that the user edits formulas in slug spelling.
|
||||
- **(b) The panel shows formula text over display names**:
|
||||
`Profit = Revenue - Cost`. This is a second rendering, so the host must resolve
|
||||
display names typed into the panel back to ids. The phrase "but not as
|
||||
column/row/page headers" then needs another meaning, and I cannot supply one with
|
||||
confidence.
|
||||
|
||||
**No recommendation. The answer's wording reads more naturally as (a)**, with ids
|
||||
as the unstated subject of "Display", but the user should say.
|
||||
17. **May a hand-written id contain capitals?** Generated ids are lower case (answer
|
||||
16(b)). A hand-edited new-version file could still write `Sales(Copy)`.
|
||||
- **(a) No (recommended).** `id_token` is lower case only. `Sales(Copy)` then fails
|
||||
to match `id_tag` and falls through to the whole-line display `Sales(Copy)`,
|
||||
with a generated id. The loader should warn when a header ends in a
|
||||
parenthesised run that contains capitals, as it already warns on an empty or
|
||||
trailing-space tag.
|
||||
- **(b) Yes.** Hand-written ids keep their case, and ids are case-sensitive, so
|
||||
`Copy` and `copy` are two ids.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -542,7 +609,8 @@ and recommendation is kept below, with its status.
|
||||
resolution moves to the input edges.
|
||||
- A new file version.
|
||||
- `Rejection` loses `Copy`.
|
||||
- `Formula::raw` is no longer verbatim (if Q14 holds).
|
||||
- `Formula::raw` is no longer verbatim: the file carries formula text over ids.
|
||||
- Ids are lower case, so an old file's data keys change spelling on its first save.
|
||||
- ADR 002's prose needs a wording update when this lands.
|
||||
|
||||
**Not addressed.** Per-entity versions (Q6), group ids (Q5), and ids in `AxisFrame` and
|
||||
|
||||
Reference in New Issue
Block a user