docs(adr): settle ADR 007's eleven open questions and state the decisions (improvise-cq1q.1)

The decisions are written into the clauses as plain design, tagged
[Amendment]: a measure formula's id is the measure's id; formulas may refer
to any id-bearing thing; ids always win, with the display as a preview;
user interfaces accept display names and scripts use ids and display_for;
slugify replaces every non-alphanumeric character with _, collapses _,
trims _ at both ends and prefixes n before a digit; a hand-written id obeys
the generated syntax; a leading _ is reserved for virtual ids, which are
lower case and never written to .improv; records-mode positions are values;
groups have a display and ordinary ids.

All eleven questions are deleted. Five new ones are open: the -<n> suffix
in hand-written ids, ASCII "alphanumeric", how a file records the virtual
categories, the dimension formula line and [_measure] tag, and which
headless entry points are scripts. core.md section 20's note follows the
new slug rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (adr007-steward)
2026-10-09 09:01:10 -07:00
committed by Edward Langley
parent 1190c80efe
commit 6e46787887
2 changed files with 247 additions and 307 deletions
+239 -300
View File
@@ -18,15 +18,16 @@ amendment is approved:
| clause | accepted rule | the amendment proposes |
|---|---|---|
| 1 | groups have no id | a group's identifier is an id |
| 1 | `_index`, `_dim`, `_measure` are the only ids beginning with `_` | the virtual views also have reserved `_` ids: `_records`, `_drill` |
| 2 | `CellKey` pairs are `(CategoryId, ItemId)` | `(CategoryId, ItemHandle)` (pending open question 1) |
| 1 | groups have no id | a group has an id and a display, and its id behaves like every other id |
| 1 | `_index`, `_dim`, `_measure` are the only ids beginning with `_` | a leading `_` is reserved for virtual ids in general, including the virtual views `_records` and `_drill`; virtual ids are never written to `.improv` |
| 2 | `CellKey` pairs are `(CategoryId, ItemId)` | `(CategoryId, ItemHandle)` |
| 2, 5 | a collapsed entry and a group reference name a group by name | by its id |
| 4 | a run of U+0020 spaces becomes one `_`; only a leading `_` is removed | each whitespace character becomes `_`, runs of `_` collapse after the strip, and leading and trailing `_` and `-` are removed |
| 4 | casefold; a run of U+0020 spaces becomes one `_`; strip characters outside `a`–`z`, `0`–`9`, `-`, `_`; remove a leading `_` | casefold; every non-alphanumeric character becomes `_`; runs of `_` collapse; leading and trailing `_` are removed; `n` is prepended to a slug that starts with a digit |
| 5 | a hand-written id uses the alphabet `a`–`z`, `0`–`9`, `-`, `_` and may begin with `_` only if reserved | a hand-written id obeys every syntax rule of a generated id |
| 12 | a typed name new to a formula edit is resolved against the document at commit | the client resolves it, ids first, and the wire carries only id-form text |
| 12 | the regression test commits the **unedited** text after the concurrent renames | the test edits a non-name token and commits, because a client may pass unedited text as a no-op without resolving any name |
Eleven questions are open (last section).
Five questions are open (last section).
## Context
@@ -73,55 +74,79 @@ human name moves to a new `display` field.
| item | yes | `ItemId` |
| formula | yes | `FormulaId` |
| view | yes | `ViewId` |
| group | **[Amendment]** yes (open questions 9 and 10) | open question 10 |
| group | **[Amendment]** yes, with a display | `GroupId` |
| record | `RecordHash`, over ids (clause 8) | unchanged shape |
**An id always names the same thing, so ids are unique in the whole document**, across
every kind. An item id names one item without its category. A `CellKey` still pairs
each item with its category, and an operation that creates an item still names the
parent category. **[Amendment]** A formula whose generated id would collide takes a
suffix like any other generated id (clause 4): a formula `Tax` in `CustomCat` gets
item id `tax`, then formula id `tax-<n>`. **[Amendment]** Formulas may refer to
anything in a typed context. What exactly that allows is open question 2.
parent category.
**[Amendment] Groups.** A group has an id and a display, like a category or an item,
and its id behaves like every other id: it is unique in the document, generated by
slugify (`group` is an empty slug's `<kind>`), declared beside the display in the file,
referred to by id, created by display, and renamed by a display rename. In code,
PR #336 (improvise-cq1q.12, open) already adds a private display to `Group` beside its
key; under this amendment that key becomes the group's id.
**Reserved ids.** The virtual categories have ids `_index`, `_dim` and `_measure`,
displayed `_Index`, `_Dim` and `_Measure`. **No other id may begin with `_`.**
**[Amendment]** A leading `_` is reserved for virtual categories, measures and other
entities. The virtual views have ids `_records` and `_drill`, displayed
`_Records` and `_Drill`. They are never persisted, but they are addressed on the wire
(`SwitchView`), and a slugified `_Drill` would otherwise be `drill` and could collide
with a user's view. Which entities the reservation covers is open question 6. Whether
records mode's synthetic positions under `_Index` and `_Dim` are ids is open question 7.
Moving today's capitalised keys to these ids is open question 8.
**[Amendment]** A leading `_` is reserved for virtual ids in general. The virtual views
have ids `_records` and `_drill`, displayed `_Records` and `_Drill`; they are addressed
on the wire (`SwitchView`), and a slugified `_Drill` would otherwise be `drill` and
could collide with a user's view. Slugify never produces a leading `_` (clause 4), so
the reservation needs no check in the grammar.
- **[Amendment] Virtual ids are lower case and are never written to `.improv`.** The
code, the wire, `ViewState` and the browser client spell the virtual categories
`_Index`, `_Dim` and `_Measure` today, and move to the lower-case ids in the re-key
step (clause 11). How the file records what it records today under `_Measure`,
`_Index` and `_Dim` is open question 3.
- **[Amendment] Records mode's synthetic positions are values, not ids.** The row
coordinate under `_index` and the column coordinate under `_dim` (a column index,
gotcha 1) are values of those virtual categories. They are never persisted or typed,
take no part in id uniqueness, and cannot collide with a user's item.
**Formula targets.** **[Amendment]** A measure is the normal formula target
**Formulas and measures.** **[Amendment]** A measure is the normal formula target
(`profit = revenue - cost`). A dimension (a category other than `_Measure`) is a rare
one, for a derived dimension.
- **[Amendment] A target outside `_Measure` is a real item**, with its own `ItemId`,
and its formula has a separate `FormulaId`.
- **A `_Measure` target has no item of its own**, and its handle is its formula's
`FormulaId`. So the category panel's `_Measure` rows use
`ItemHandle::{Item(ItemId), FormulaTarget(FormulaId)}` (clause 6).
- **[Amendment]** A measure has an id and a display, as everything else does.
Whether a formula-target measure keeps the formula's id or becomes an item with
its own is open question 1. The `ItemHandle` text in clauses 2, 5, 6, 8 and 11
depends on its answer.
- **[Amendment] A formula is a kind of measure, so a `_Measure` formula's id is the
measure's id.** A measure has an id and a display, as everything else does.
`Profit = Revenue - Cost` is one thing, `profit`: another formula refers to it as
`profit`, each Profit cell's `CellKey` holds `profit` under `_measure`, and
`ReplaceFormula` and `RemoveFormula` address `profit`. A formula that derives a
measure already stored under that display takes the stored measure's id, so the
column has one id, not two. Removing the formula removes a measure that exists only
through it; a measure that also holds stored data stays, as a stored measure.
- **[Amendment] A target outside `_Measure` is a real item**, with its own `ItemId`,
and its formula has a separate `FormulaId`. A formula whose generated id would
collide takes a suffix like any other generated id (clause 4): a formula `Tax` in
`CustomCat` gets item id `tax`, then formula id `tax-<n>`.
- **[Amendment] Formulas may refer to anything by id.** A function in a formula may
accept any id-bearing thing (a category, an item, a measure, a formula, a group),
referred to by its id. Whether a given function takes the thing itself or its value
(an item's display, say) is part of that function's signature. Global uniqueness
makes every such reference unambiguous, and a function may read any part of a record,
so nothing further is needed here.
### 2. Storage keys by id; `display` is the only place a human name lives
- **Categories and views:** `Model::categories` and `Workbook::views` are keyed by id,
and each `Category`, `Item` and `View` gains a `display: String`.
and each `Category`, `Item` and `View` gains a `display: String`. **[Amendment]** So
does `Group`.
- **Cells:** a `CellKey` is a sorted list of `(CategoryId, ItemId)` pairs.
**[Amendment]** The pairs are `(CategoryId, ItemHandle)`, so that a `_Measure`
formula cell's coordinate is `FormulaTarget` without claiming an `ItemId` the
document does not hold (pending open question 1).
document does not hold.
- The key stays sorted, now **by category id**, so the canonical form (gotcha 5)
is kept with a different sort key.
- `DataStore` interns ids exactly as it interns names today.
- An id's text names one thing, because ids are unique across kinds.
- **View configuration:** the axis maps, page selections, and hidden and collapsed sets
are keyed by ids. **[Amendment]** A collapsed entry names its group by the group's
id. Whether the category id stays beside it is part of open question 10.
id.
- **Formulas:** a formula's references are ids (clause 5), and the formula panel
renders them as display names (clause 6).
@@ -158,68 +183,68 @@ copyable, compared as an integer.
**Every** id the app generates comes from the display name by **slugify**: on
import, on interactive creation, and when an id-less `.improv` file is loaded. So a
thing's id never depends on how it was made. An id is lower case, in the alphabet
`a`–`z`, `0`–`9`, `-` and `_`.
thing's id never depends on how it was made. An id is lower case.
The steps, in order, as the amendment states them **[Amendment]**:
**The accepted steps:**
1. casefold, with Unicode full case folding;
2. replace each run of spaces with one `_`;
3. strip every character that is not `a`–`z`, `0`–`9`, `-` or `_`;
4. remove any leading `_`;
5. if nothing is left, use `<kind>-<n>`.
**[Amendment] The steps become:**
1. **Casefold, with Unicode full case folding**, so `Straße` → `strasse`. Rust's
`str::to_lowercase` is not enough (`ß` stays `ß` and is then stripped). The folding
table or crate must pass the `wasm` gate, because `improvise-core` is in the browser
bundle.
2. **Replace each whitespace character with `_`.** "Whitespace" is the Unicode
`White_Space` property (`char::is_whitespace`): U+0009–U+000D, U+0020, U+0085,
U+00A0, U+1680, U+2000–U+200A, U+2028, U+2029, U+202F, U+205F and U+3000.
- A tab, a no-break space and an ideographic space each become `_`.
- A zero-width space (U+200B) and a byte-order mark (U+FEFF) are not whitespace,
and step 3 strips them.
- The realistic case is a header pasted from a web page, where a no-break space
stands for a space.
3. **Strip every character outside the id alphabet.** This strips non-ASCII letters,
so an id never needs quoting in an `.improv` reference.
4. **Collapse each run of `_` to one `_`**, whether the underscores came from
whitespace or were in the display. The step comes after the strip (open
question 5).
5. **Remove every leading and trailing `_` and `-`**, repeating until neither end
holds either (open question 11).
- A leading `_` is reserved (clause 1).
- A trailing `_` is dropped.
- An id never begins or ends with `-`.
`str::to_lowercase` is not enough (`ß` stays `ß`). The folding table or crate must
pass the `wasm` gate, because `improvise-core` is in the browser bundle.
2. **Replace every character that is not alphanumeric with `_`.** That includes
spaces, tabs and every other whitespace, punctuation and `-`. Whether
"alphanumeric" means ASCII only is open question 2.
3. **Collapse each run of `_` to one `_`**, whether the underscores came from step 2
or were in the display.
4. **Remove every leading and trailing `_`.** A leading `_` is reserved for virtual
ids (clause 1), and a trailing one is dropped.
5. **If the slug starts with a digit, prepend `n`.** An id is never purely numeric
and never starts with a digit, so `2024` becomes `n2024`.
6. **If nothing is left, use `<kind>-<n>`**, such as `category-1` or `item-4`. `n`
comes from the shared counter below. A display of only `-`, `_` or non-ASCII letters
ends here, because `-` alone is not an id.
comes from the shared counter below.
The accepted text has five steps instead: casefold; replace each **run of spaces**
with one `_`; strip; remove any **leading** `_`; `<kind>-<n>` if empty.
So a generated id is a lower-case letter followed by letters, digits and single
underscores, never ending in `_`, plus, only when it collided, a `-<n>` suffix; or
`<kind>-<n>`. Whether a hand-written id may carry that suffix is open question 1.
Examples under the amendment:
Examples under the amendment, with "alphanumeric" read as ASCII:
| display | id |
|---|---|
| `East` | `east` |
| `Time Period`, `Time Period`, `Time\tPeriod`, `Time\u{A0}Period`, `Time \u{200B} Period` | `time_period` |
| `Time Period`, `Time Period`, `Time\tPeriod`, `Time \u{200B} Period` | `time_period` |
| `Coastal_East` | `coastal_east` |
| `a_ b`, `a__b` | `a_b` |
| `Q1-2024` | `q1-2024` |
| `Q1 - 2024` | `q1_-_2024` (only `_` runs collapse) |
| `Q1-2024`, `Q1 - 2024` | `q1_2024` |
| `East `, `East_`, `-East-` | `east` |
| `_Draft` | `draft` |
| `Region(code)` | `regioncode` |
| `Region(code)` | `region_code` |
| `Café` | `caf` |
| `Straße` | `strasse` |
| `Ü`, `-`, `---` | `<kind>-<n>` |
| `2024`, `_2024` | `n2024` |
| `2024 Budget` | `n2024_budget` |
| `Ü`, `-`, `%` | `<kind>-<n>` |
Under the accepted steps, the ones that differ are `Q1-2024` → `q1-2024`,
`Q1 - 2024` → `q1_-_2024`, `East ` → `east_`, `a_ b` → `a__b`, `Region(code)` →
`regioncode`, `2024` → `2024` and `-` → `-`.
**Collisions.** `Time Period` and `Time_Period` slug alike, and whichever comes later
takes a suffix. **[Amendment]** So do `East`, `East ` and `East_`, which slug alike
only under the amendment's steps (under the accepted steps `East ` slugs to `east_`). `East` in `Region` and `East` in
`Direction` also slug alike, and ids are global, so the second is `east-<n>`.
takes a suffix. **[Amendment]** So do `East`, `East ` and `East_`, and `Q1-2024` and
`Q1 2024`, which slug alike only under the amendment's steps. `East` in `Region` and
`East` in `Direction` also slug alike, and ids are global, so the second is `east-<n>`.
- **Any colliding generated id gets a suffix**, whatever kind of thing holds the
slug: on import, when an id-less file loads, and when the host creates something in
a session. An id that does not collide stays bare.
- **The suffix is `-` plus the next value of one increment-only counter per
document**, as a plain number (`east-1`, not `east-001`). The counter works like
Common Lisp's `gensym`: it never goes down and is never reused, so no order holds
among suffixes (`east-7` may follow `east` directly). **[Amendment]** A suffix ends
in a digit, so a suffixed id still meets the amendment's step 5.
among suffixes (`east-7` may follow `east` directly).
- **Retirement.** Within a session a removed id is never handed out again. The
counter is past any suffix it issued, and a removed bare slug stays reserved in
memory.
@@ -238,6 +263,9 @@ every data key and reference in that file spells it. So:
- **two identical hand-written ids in one file are refused at load**, even when they
declare different kinds of thing. Renaming one would corrupt the data section, and a
file with a duplicate cannot be resaved safely. The error names both lines.
- **[Amendment] A hand-written id obeys every syntax rule of a generated id**: lower
case, never starting with `_` or a digit, never ending in `_`, no runs of `_`, and no
character slugify would replace. One that does not is refused at load (clause 5).
**Reuse across a save and reload is accepted.** Retired ids are not persisted. So
remove `East`, save, reload and add `East`, and the new item gets `east` again. An
@@ -252,19 +280,18 @@ promised (improvise-8tk5).
**Built code.** `crates/improvise-core/src/model/id.rs` (`slugify`, `IdAllocator`)
implements the accepted steps, and `context/code/core.md` §20 describes them. Once
the amendment is approved, a follow-up must:
- test whitespace, not only U+0020;
- collapse `_` after the strip;
- trim a trailing `_` and both ends' `-`;
- settle what `claim` refuses (open question 11);
- add the virtual views to `VIRTUAL_IDS`;
- add `IdKind::Group` if groups are generated (open question 10).
- replace every non-alphanumeric character, not only spaces, with `_`, and collapse
`_` runs;
- trim a trailing `_`, and prepend `n` before a leading digit;
- make `claim` refuse every hand-written id a generated one could not be;
- add the virtual views to `VIRTUAL_IDS`, and `IdKind::Group`.
Nothing calls `slugify` yet.
### 5. Persistence: ids on declarations *and* references
- **Declarations** (section headers, item lines, formula lines) carry the display and
the id.
- **Declarations** (section headers, item lines, formula lines, and **[Amendment]**
group definitions) carry the display and the id.
- **Every reference** is the id: axis lines, `hidden:` and `collapsed:`, page
selections, `## Aggregation`, `Initial View:`, `## Data` keys (ids are guaranteed
unique, names might not be), and names inside formula text.
@@ -278,14 +305,17 @@ grammar spells it **with ranges**, because the generator drops `ASCII_ALPHANUMER
(`Sales(Copy)`), an empty tag or a tag with a trailing space does not match
`id_tag`, so it has no id. The error names the line;
- a hand-written id beginning with `_`, other than a reserved one;
- **[Amendment]** a hand-written id that is `-` (open question 11 asks whether a
leading or trailing `-`, or a trailing `_`, is refused too).
- **[Amendment]** a hand-written id that breaks any syntax rule of a generated id
(clause 4). Virtual ids are never written to a file, so no hand-written id begins
with `_`. The grammar's `id_token` stays permissive, and the loader refuses, so the
error can name the line.
**Inside formula text an id may still need quoting.** The formula grammar's
`bare_ident` must begin with a letter or `_`, and keywords are guarded (gotcha 34). So
the writer spells an id in formula text through `quote::quote_name`. An id beginning
with a digit (`2024`) or equal to a keyword (`sum`, `if`, `where`, `is`) is written
`|2024|` or `|sum|`.
the writer spells an id in formula text through `quote::quote_name`. An id equal to a
keyword (`sum`, `if`, `where`, `is`) is written `|sum|`, and under the accepted rules
an id beginning with a digit (`2024`) is written `|2024|`. **[Amendment]** No id begins
with a digit, so only keyword ids need quoting.
```pest
id_token = @{ ('a'..'z' | '0'..'9' | "_" | "-")+ }
@@ -306,9 +336,10 @@ id_ref = { id_token }
`model_name` has no id and keeps `header_name`. `Initial View:` is a reference and
takes an `id_ref`.
**Group references.** A group reference (`[Coastal]`) is a name.
**[Amendment]** It is the group's id. So is the group in a `collapsed:` line. The
group definition (`> Coastal`) declares the id. The spellings are open question 10.
**Group references.** A group reference (`[Coastal]`) is a name. **[Amendment]** It is
the group's id, as is the group in a `collapsed:` line, and the group definition
(`> Coastal`) declares a display and an id like any other declaration
(`> Coastal(coastal)`).
**[Amendment] An axis line is tried before `format:`, and a format that reads as an
axis line is pipe-quoted** (improvise-le4p). `view_entry` is an ordered choice, and
@@ -330,20 +361,19 @@ lose its axis line to the view's number format, and the new-version arm puts
that view also holds a category `format` on an axis.
**[Amendment] A formula line** is
`- <target>(<formula id>) = <id-form expression> [<category id>]` (pending open
question 1):
`- <target>(<formula id>) = <id-form expression> [<category id>]`:
- **The parenthesised id is the formula's `FormulaId`**, between the target and the
`=`.
- **`<target>` is the target item's id when the target is an item** (outside
`_Measure`). For a `_Measure` target, which has no item, it is the target's
display, held on the `Formula`.
`=`. For a measure formula it is also the measure's id.
- **For a measure formula, `<target>` is the measure's display**, held on the
`Formula`: `- Profit(profit) = revenue - cost`. How a dimension formula's line spells
its target, and whether the `[_measure]` tag is written, is open question 4.
- **The expression is over ids**, spelled through `quote::quote_name`. A reference to
a formula target is the id text of its `ItemHandle`: the item id outside `_Measure`,
the formula id for a `_Measure` target.
the formula's (and measure's) id for a `_Measure` target.
- **A `WHERE`/`IS` value follows the same rule**. It is the id text of an
`ItemHandle` in the filtered category, and the formula panel shows it as a display.
- **A display in the target position that looks like `Name(x)` is pipe-quoted**, as in
`- |Margin(%)|(margin) = …`. An item id never contains a parenthesis.
`- |Margin(%)|(margin) = …`. An id never contains a parenthesis.
**`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 parses as an
@@ -352,7 +382,8 @@ property 1 must be stated over the parsed result, not over the generated text.
**The version line alone picks the regime** (clause 10).
- **In an old-version file** a header is the whole line, a reference resolves against
display names, and ids are assigned by slugify.
display names, and ids are assigned by slugify. This resolution migrates a file
whose names were its only identity; it is not a reference by a person.
- **In a new-version file** declarations carry ids and a reference is an id.
**An old file naming an item it never declares** declares it at load. This happens
@@ -384,7 +415,7 @@ out, because reassigning an id would silently change what its references point a
**Import** (CSV, JSON) never parses ids out of dimension names. A header `Region(code)`
is the display `Region(code)`, and its id is its slug. The writer pipe-quotes any
display that would otherwise read as display plus id:
`## Category: |Region(code)|(regioncode)`.
`## Category: |Region(code)|(regioncode)` (**[Amendment]** `(region_code)`).
### 6. The wire: address by id, create by display, learn ids from projections
@@ -393,13 +424,17 @@ display that would otherwise read as display plus id:
being created is a display, and the host slugifies it:
- `AddCategory(display)` and `CreateView(display)`;
- `AddItem { category: CategoryId, display }`;
- `AddItemInGroup { category: CategoryId, display, group }`. Whether `group` is an
existing group's id or a new group's display is open question 10 **[Amendment]**;
- `AddItemInGroup { category: CategoryId, display, group }`. **[Amendment]** `group`
is the group's id, and a new group is created by its display under the same
create-by-display rule;
- `AddFormula { target_category: CategoryId, raw }`.
- **[Amendment] Only a client accepts a display name as a reference**, in its UI
layer, and it converts the name to an id as early as it can. The wire never carries
a display as a reference. Whether the old-version loader and headless `improvise
cmd`/`script` are exceptions is open question 4.
- **[Amendment] Only a user interface accepts a display name as a reference**, and it
converts the name to an id as early as it can. The TUI and browser clients are user
interfaces, and so is the headless command line. The wire never carries a display as
a reference.
- **[Amendment] Scripts refer to things by id.** A script that names something by its
display fails. A script gets a display only through a dedicated function such as
`display_for(id)`. Which headless entry points count as scripts is open question 5.
- **A frame cannot address a thing it creates**, because it does not know the id the
host will assign. A create-then-act compound becomes **one command**:
`create-and-switch-view` sends `[CreateView, SwitchView]` today and becomes
@@ -408,6 +443,7 @@ display that would otherwise read as display plus id:
- **New operations carry ids:**
- `RenameCategory { category, to: display }`;
- `RenameItem { category, item, to: display }`;
- **[Amendment]** a group rename by group id and new display;
- `ReplaceFormula { id, was, raw }`;
- `RenameRecordCoordinate`;
- the removes (`RemoveCategory`, `RemoveItem`, `RemoveFormula`, `DeleteView`).
@@ -415,8 +451,7 @@ display that would otherwise read as display plus id:
including `SetCell { view, … }` and `ClearCell { view, … }`), one whole variant at a
time.
- **Projections carry id and display together:**
- `CategoryRow { id, display, items: Vec<(ItemHandle, String)>, axis }` (pending open
question 1);
- `CategoryRow { id, display, items: Vec<(ItemHandle, String)>, axis }`;
- `FormulaCache` rows `{ id, stored, display }`. `display` is what the panel shows:
the target's display with its id in parentheses (`Profit (profit) = Revenue -
Cost`), then the expression over display names, with an id wherever a display is
@@ -425,15 +460,17 @@ display that would otherwise read as display plus id:
would bind elsewhere. `stored` is the id-form text the file carries, and
`ReplaceFormula`'s `was` is compared against it. So a rename of `Revenue` elsewhere
changes what the panel shows, but does not refuse an edit of `Profit`;
- `ViewSummary { id, display, … }`.
- `ViewSummary { id, display, … }`;
- **[Amendment]** the grid frame's group entries (`FrameGroup`) carry the group's id
and display.
`AxisFrame` labels stay display strings. They gain ids when a consumer needs them.
- **`ViewState` keys by id**: the cursors, `expanded_cats` and the view history
(`view_back_stack` / `view_forward_stack`). **[Amendment]** So do `active_view`
itself and `Workbook::active_view` (improvise-p003). A rename then orphans nothing.
- **[Amendment] A `_Measure` formula-target row** (`ItemHandle::FormulaTarget`) is
removed by `RemoveFormula` and renamed by a display rename of the formula's target
(improvise-i0n7; pending open question 1).
removed by `RemoveFormula` and renamed by a display rename of the measure
(improvise-i0n7).
- **[Amendment] CheckedBatch's created-record exemption is removed** (improvise-p003).
It exempts a record that neither resolves nor is claimed, for frames like
`[AddItem(East), SetCell(East)]`. A frame cannot address what it creates, so nothing
@@ -460,7 +497,7 @@ Per-entity versions are not added.
text pairs: the same function over different strings. **[Amendment]** The pairs are
`(CategoryId, ItemHandle)` spelled as id text. Ids are unique across kinds, so the hash
stays canonical. A record is the key minus `_measure`, so a `FormulaTarget` coordinate
is never part of a record (pending open question 1).
is never part of a record.
ADR 002's decision is unchanged in substance. Its prose ("sorts coordinates by
category name", records spelled in names) is out of date once this lands, and the
@@ -486,7 +523,7 @@ whole-line reading.
| `## View: \|Default (default)\|(default)` | pipes literal | display `Default (default)`, id `default` |
| `## 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: Budget (2024)` | `Budget (2024)` | **display `Budget`, id `2024`** (**[Amendment]** refused: an id may not start with a digit) |
| `## Category: Time Period` | `Time Period` | unchanged; id `time_period` generated |
The app itself writes such headers: `improvise import` on a CSV with headers like
@@ -496,7 +533,8 @@ If old files got the rule, that category would load as display `Region`, id `cod
Every reference in the file spells `Region(code)`, which resolves to neither, so the
axis lines and data keys would not resolve, and nothing would be drawn. Because old
files keep the whole-line reading, none of this happens. On the first save the writer
pipe-quotes such a display: `## Category: |Region(code)|(regioncode)`.
pipe-quotes such a display: `## Category: |Region(code)|(regioncode)`
(**[Amendment]** `(region_code)`).
### 10. The file version alone picks the regime
@@ -517,10 +555,12 @@ pipe-quotes such a display: `## Category: |Region(code)|(regioncode)`.
1. **Remove legacy JSON** (improvise-tt22). Done, in PR #224.
2. **Types:** the id newtypes over `Symbol`, the `display` fields, `FormulaId` and
`ViewId`. **[Amendment]** Also `ItemHandle`, which the re-key needs for `CellKey`
(pending open question 1).
`ViewId`. **[Amendment]** Also `GroupId`, a display on `Group`, and `ItemHandle`,
which the re-key needs for `CellKey`.
3. **Re-key storage by id** (clause 2). This is the wide, mechanical step, with
display→id resolution moved to every input edge.
display→id resolution moved to every input edge. **[Amendment]** The virtual
categories' keys move from `_Index`, `_Dim` and `_Measure` to `_index`, `_dim` and
`_measure` here, in the model, on the wire, in `ViewState` and in the browser client.
4. **Persistence:** the new literal (clause 10), clause 5's grammar, the loader and its
refusals, slugify, the writer, and the round-trip properties with a formula-line
strategy.
@@ -556,22 +596,22 @@ answers for those.
- **A typed name that was not in the captured mapping** is resolved against the
document at commit, and refused, naming it, if it does not resolve.
**[Amendment] The client does the resolving:**
**[Amendment] The client does the resolving, and ids always win:**
- **The client holds the mapping**, as it holds `OpenedCell`. The TUI and the browser
share one resolver in `improvise-protocol`.
- **A name new to the edit** is resolved by the client against its id-bearing
projection (clause 6), **ids first, then displays**. Ids are unambiguous, so they are
preferred wherever there is ambiguity.
preferred wherever there is ambiguity, now and once an autocomplete exists.
- A typed name equal to an id names that id's thing, even where it is also another
thing's display.
- Otherwise a display that exactly one thing in scope has names that thing.
- A display matching several things is refused, and the refusal lists their ids.
- A name that resolves to nothing is refused, and the refusal names it.
- Ids are matched exactly, with no case folding.
- **The long-term design separates two layers.** A view-layer autocomplete turns
typed display text into ids before commit, and the model layer refers to ids only.
What becomes of the ids-first order once that exists is open question 3. This ADR
does not specify the autocomplete.
- **Typing prefers ids and previews displays.** A view-layer autocomplete turns typed
text into ids before commit, preferring a match on an id, and shows the matched
thing's display as a preview. The model layer refers to ids only. This ADR does not
specify the autocomplete further.
- **The wire carries only id-form text.** `ReplaceFormula.raw` and `AddFormula.raw` are
in the file's id form, and the host never resolves a display. The host parses ids,
and one that no longer exists is `Rejection::Gone`.
@@ -579,8 +619,8 @@ answers for those.
**[Amendment] What ids-first costs.** Suppose item A is displayed `East` with id `east`,
and item B is displayed `east`, so slugify gave it `east-1`. Typing `east` names **A**.
B is reached by typing `east-1`. A user who typed `east` meaning B is bound to A
without being told. That is why the panel shows B as `east-1` and never as a bare
`east`.
without being told, unless the preview shows it. That is why the panel shows B as
`east-1` and never as a bare `east`.
**Required regression test.**
1. Open a formula editor on `Profit = Revenue - Cost`.
@@ -659,219 +699,118 @@ three things:
- The formula panel is a second rendering of formula text, so display names typed
there must be resolved back to ids (by the client, under the amendment).
- ADR 002's prose needs a wording update.
- **[Amendment]** Scripts that name things by display break and must be rewritten
over ids.
**Not addressed.** Per-entity versions, ids in `AxisFrame` and `CellId::Derived`, and
(under the accepted text) group ids.
**Not addressed.** Per-entity versions, ids in `AxisFrame` labels and
`CellId::Derived`, and (under the accepted text) group ids.
## Open questions
These are open, and nothing above depends on an answer except where it says so.
### 1. Is a formula and the measure it derives one thing with one id, or two things with two ids?
### 1. May a hand-written id carry the collision suffix?
**The question.** A formula is a derived measure. Take `Revenue` and `Cost`, stored
measures with ids `revenue` and `cost`, and add `Profit = Revenue - Cost`. Profit holds
no data: the formula computes its column. Two kinds of thing must name Profit by an
id:
- things that refer to the **measure**: another formula (`Margin = Profit / Revenue`),
each Profit cell's `CellKey`, a `hidden:` line, and ADR 008's cursor anchor;
- things that refer to the **formula**: `ReplaceFormula` and `RemoveFormula`.
**The question.** A hand-written id obeys every syntax rule of a generated id
(clause 4), and slugify turns `-` into `_`, so a slug never contains `-`. But a
generated id that collided carries a `-<n>` suffix (`east-1`), and an empty slug
becomes `<kind>-<n>` (`item-4`). Every save writes those ids into the file, so the
next load reads them as hand-written.
Do they use the same id?
**Why it matters.** It decides the `CellKey` type (clause 2), the formula line's
shape (clause 5), `RecordHash` (clause 8), the `_Measure` panel rows (clause 6) and
what `.improv` must write.
**Why it matters.** If a hand-written id may never contain `-`, the app refuses to load
a file it saved itself.
**Options.**
- **(a) One id.** The formula *is* the measure. Profit is `profit` everywhere, with
its display on the formula, and the line is `- Profit(profit) = revenue - cost`.
- A Profit cell's coordinate is `ItemHandle::FormulaTarget(profit)`.
- The cost: a stored `Profit` column and a formula `Profit` (merged into one measure
today by `measure_item_names`) would have two ids, and one must be chosen to key
cells.
- `RemoveFormula` removes the measure.
- The rare dimension target (an item `tax` plus formula `tax-1`) works differently.
- ADR 008 needs `ItemHandle`.
- This is the smallest change from the text above.
- **(b)** is (a) with the dimension line spelled `- Tax(tax-1)(tax) = … [customcat]`
(display, formula id, item id), so that the target position always holds a display.
- **(c) Two ids.** Profit is a `_Measure` item `profit`, and its formula is `profit-1`.
The line is `- profit(profit-1) = revenue - cost`.
- `CellKey` returns to `(CategoryId, ItemId)` and `ItemHandle` goes.
- A same-named stored measure is simply the item the formula attaches to.
- Measure and dimension targets work alike.
- The cost: the file must newly write the item and its display (today a formula
target is rebuilt from `## Formulas`).
- `RemoveFormula` leaves an item with no formula and maybe no data.
- ADR 009's `_Measure` fallback in `find_item_category` goes.
- **(a)** The syntax of an id is a slug, optionally followed by `-` and a number:
`[a-z][a-z0-9]*(_[a-z0-9]+)*` then optionally `-[0-9]+` (which also covers
`<kind>-<n>`). A `-` anywhere else is refused.
- **(b)** Change the suffix to avoid `-`, for example `east_1`. That collides with a
display such as `East 1`, which also slugs to `east_1`, so the counter must advance
past it.
Also to settle: whether the writer keeps omitting the `[_measure]` tag on a `_Measure`
formula line, as today's writer does.
**Author's recommendation:** (a). It keeps the counter's suffix visibly distinct from
anything a display can slug to.
**Recommendation:** none. Both (a) and (c) model a derived measure, and the choice is a
product judgement about whether a formula and its column are one thing.
### 2. Does "alphanumeric" mean ASCII?
### 2. What may a formula refer to?
**The question.** Formulas may refer to things in a typed context. An example is a
function counting how often a letter appears in a category value. Today a formula
reference (`Expr::Ref`) names an item or a formula target. Three things are open:
- (i) may a bare id in formula text also name a category, a view or a group, with
its kind given by where it appears?
- (ii) does a text function read an item's display (`Café`) or its id (`caf`)? They
differ, because slugify folds case and strips;
- (iii) must this ADR specify that, or only keep ids unique so it can be added later?
**Why it matters.** It decides whether the formula grammar and evaluator change with
this ADR.
**Author's recommendation:** (iii). The formula language has no string function
today, and global uniqueness already makes a reference to any kind unambiguous.
### 3. Does ids-first matching outlive the interim resolver?
**The question.** Clause 12's resolver matches typed text against ids before displays,
and ids are preferred wherever there is ambiguity. Once a view-layer autocomplete
turns display text into ids, does the preference carry into it?
**The question.** Slugify replaces every non-alphanumeric character with `_`. Unicode
counts `é`, `ü` and `日` as alphanumeric, but an id's alphabet is `a`–`z`, `0`–`9`, `_`
(and `-`).
**Options.**
- **(a) Yes.** Wherever typed text could name both an id and a display, the id wins,
including in the autocomplete's ranking.
- **(b) No.** Once the autocomplete exists, nothing is matched at commit, so the rule
ends with the interim resolver.
- **(a) ASCII.** A non-ASCII letter becomes `_`: `Café` → `caf`, `Ü` → `<kind>-<n>`,
`Crème Brûlée` → `cr_me_br_l_e`.
- **(b) Unicode, then drop what is outside the alphabet.** A non-ASCII letter
disappears without splitting the word: `Café` → `caf`, `Crème Brûlée` →
`crme_brle`.
- **(c) Unicode, then transliterate** (`é` → `e`). `Café` → `cafe`, `Crème Brûlée` →
`creme_brulee`. This needs a transliteration table that passes the `wasm` gate.
**Recommendation:** none. It matters only once an autocomplete is designed.
**Why it matters.** It decides every id generated from a non-English display.
### 4. May anything other than a client resolve a display name?
**Author's recommendation:** (a): it is the simplest reading of "non-alphanumeric
characters are replaced", and it needs no table.
**The question.** Only a client accepts a display as a reference (clause 6). Two
places would resolve displays outside one:
- the **old-version loader**, which resolves an old file's references by display
(clause 5), because in an old file the name is the only identity;
- **headless `improvise cmd` and `improvise script`**, which run command lines naming
things by display, with no client attached.
### 3. How does a file record the virtual categories, if virtual ids are never written?
**Why it matters.** If neither is allowed, old files cannot be migrated and the
headless CLI needs a client layer of its own.
**The question.** Virtual ids are never written to `.improv` (clause 1). But today's
files record three things under the virtual categories (`web/demo.improv` has all of
them):
- the virtual categories' axis placements, such as `_Measure: column` and
`_Index: none`;
- the stored measures, under `## Category: _Measure`;
- each data key's measure, as `_Measure=Revenue`.
**Author's recommendation:** allow both. The loader is migrating a file, not a person
referring to something. The CLI is the UI layer of its own invocation and converts at
its edge, as a client does.
### 5. Does the `_` collapse come after the strip?
**The question.** Clause 4 collapses runs of `_` (step 4) after stripping characters
outside the alphabet (step 3).
**Why it matters.** `Time \u{200B} Period` (a zero-width space between two spaces)
gives `time_period` only if the collapse comes after the strip. Before it, the
zero-width character still separates the two `_`, and the result is `time__period`.
**Options:** (a) after the strip, as written; (b) before it.
**Author's recommendation:** (a).
### 6. Which entities share the reserved leading `_`?
**The question.** A leading `_` is reserved for virtual categories, measures and other
entities.
- (i) Does "measures" mean virtual measures (none exists today), the `_measure`
category, or something else?
- (ii) Which "other entities" are covered? In particular, are the virtual views,
`_records` and `_drill` (clause 1), among them?
**Why it matters.** It fixes `VIRTUAL_IDS` and what `claim` refuses.
**Author's recommendation:** (ii) yes, as clause 1 is written. Otherwise `_Drill`
slugs to `drill` and can collide with a user's view.
### 7. Are records mode's positions under `_Index` and `_Dim` ids?
**The question.** Records mode gives each row a synthetic coordinate under `_Index`,
and each column one under `_Dim` (a column index, gotcha 1). They are never persisted
and never typed.
**Why it matters.** Every document with data has measures, so every file needs some
way to say which measure a cell holds and where `_Measure` sits.
**Options.**
- **(a)** Outside the id namespace: a position `0` under `_index` does not collide
with a user's item `0`.
- **(b)** Inside it, spelled so that it cannot collide (for example `_index-0`), under
the reservation of a leading `_`.
- **(c)** Something else.
- **(a) Reserved keywords.** The file keeps fixed spellings for the virtual categories
(`_Measure`, `_Index`, `_Dim`, or new lower-case keywords). They are part of the
format, not ids, so no id is written.
- **(b) Write the virtual ids** (`_measure: column`, `_measure=revenue`), an exception
to "never written".
- **(c) Leave the virtual categories implicit.** Their axis placements take defaults,
the measures section has no category header, and a data key's measure is written
without its category.
**Why it matters.** Under (b), uniqueness is checked for synthetic coordinates, and
`claim`'s exact match on reserved ids must change.
**Author's recommendation:** (a). The file needs these facts, and a keyword says so
without turning a virtual id into file content.
**Recommendation:** none.
### 4. How does a dimension formula's line spell its target, and is the `[_measure]` tag written?
### 8. How do the virtual categories move to their lower-case ids?
**The question.** A measure formula's line is `- Profit(profit) = revenue - cost`: the
measure's display, then its id (clause 5). A dimension formula (the rare case) has an
item id and a separate formula id. Its line may be:
- **(a)** `- tax(tax-1) = … [customcat]`: the item id in the target position. The
target position then holds a display for a measure and an id for a dimension.
- **(b)** `- Tax(tax-1)(tax) = … [customcat]`: display, formula id, item id. The target
position always holds a display.
**The question.** Today `_Index`, `_Dim` and `_Measure` are keys in the model, on the
wire, in `ViewState` and in `.improv` files. For example, `web/demo.improv` has
`_Measure: column`, `_Index: none`, `## Category: _Measure` and `_Measure=Revenue`.
Under clause 1 they become `_index`, `_dim` and `_measure`, with the displays
unchanged. The proposed migration:
- an old file's `_Measure` resolves to `_measure` through its display at load, like
any name, and the first save writes `_measure: column` and `_measure=revenue`;
- a new-version file must spell the id, and `_Measure` there is refused (ids are
lower case);
- the header becomes `## Category: _Measure(_measure)`;
- every string literal naming them in code, on the wire and in the browser client
moves in the re-key step.
Today's writer omits the category tag for a `_Measure` formula. Should the new writer
keep omitting `[_measure]`?
**Why it matters.** Every shipped document and much test code spells these names.
**Why it matters.** It fixes the formula-line grammar and its generator strategy.
**Author's recommendation:** the migration above.
**Author's recommendation:** (b), so one position never holds two kinds of thing;
keep omitting `[_measure]`, which also keeps the virtual id out of the file
(question 3).
### 9. Does a group also carry a display name?
### 5. Which headless entry points are scripts?
**The question.** A group's identifier is an id, not a friendly name (clause 1). Does
a group also have a separate display, as categories, items and views do?
**The question.** User interfaces accept display names, and scripts refer to things by
id only (clause 6). The headless front ends are `improvise cmd '<line>'`, which runs
one or more command lines given on the command line, and `improvise script <file>`,
which runs a file of them.
**Why it matters.**
- If it does not, the grid and panels show the id: `Coastal East` would appear as
`coastal_east`.
- If it does, groups need a display rename like everything else, and the file
declares both.
- The code is already moving: PR #336 (improvise-cq1q.12, open) adds a private
display to `Group`, beside its key.
**Why it matters.** It decides whether `improvise cmd 'add-item Region East'`-style
invocations that name existing things by display keep working.
**Options:** (a) yes, a display beside the id; (b) no, the id is all a group has.
**Author's recommendation:** (a), for consistency with every other id-bearing kind and
so that users see what they typed.
**Options.**
- **(a)** `improvise cmd` is a user interface and resolves displays; a script file is
a script.
- **(b)** Both are scripts: only the TUI and browser clients resolve displays.
### 10. How do groups carry ids?
**The question.** A group's identifier is an id. Not yet decided:
- (i) **Namespace.** Is a group id unique in the whole document, like every other id,
or only within its category?
- (ii) **Generation.** Is it the slug of a display (if question 9 gives it one), with
`group` as an empty slug's `<kind>` and the shared counter for collisions?
- (iii) **The file.** For example a definition `> Coastal(coastal)`, a membership
`- Coastal_East(coastal_east)[coastal]`, and a parent group (group hierarchies,
improvise-felo.6) named by id. Does a `collapsed:` line hold the group id with or
without its category id?
- (iv) **The wire.** Does `AddItemInGroup`'s `group` name an existing group by id,
create one by display, or either? Is there a `RenameGroup { group, to }`?
- (v) **The frame.** `AxisFrame`'s `FrameGroup` carries a group name today. What does
it carry?
**Why it matters.** Clauses 2, 5, 6 and 7 cannot be implemented for groups without
these answers.
**Author's recommendation:** (i) document-wide, as every other id is; (ii) slugify,
like every generated id. No recommendation on (iii)–(v).
### 11. What may a hand-written id look like at its ends, and do slugify's trims repeat?
**The question.** No id may be `-`, and no id begins or ends with `-`.
- (i) Is a hand-written id that begins or ends with `-` refused at load, as one
beginning with `_` is?
- (ii) Slugify drops a trailing `_`. Is a hand-written id ending in `_` refused too?
- (iii) Clause 4's step 5 repeats until neither end holds `_` or `-`, so `-_x` → `x`.
One pass of each would leave `_x`, which is reserved, or `x_`. Is repeating right?
**Why it matters.** It decides what `IdAllocator::claim` refuses. Today it accepts
`-`, `-x` and `x-`.
**Author's recommendation:** (i) yes; (ii) yes, so that a hand-written id obeys the
same shape as a generated one; (iii) repeat.
**Author's recommendation:** (a). A person types `improvise cmd` lines as they type
`:` lines in the TUI; a script file is a program that should not break when something
is renamed.
+8 -7
View File
@@ -1715,11 +1715,12 @@ to `east_`. Only *leading* underscores are removed (step 4).
**ADR 007's proposed amendment changes these rules, and this code predates it.**
The amendment (PR #326) is not yet approved. Its clause 4:
- each whitespace character becomes `_`, not only U+0020;
- runs of `_` collapse after the strip, so `a_ b` and `a__b` both slug to `a_b`
and `Time \u{200B} Period` to `time_period`;
- leading and trailing `_` and `-` are removed, so `East ` → `east`, `-East-` →
`east`, and `-` alone takes `<kind>-<n>`.
- every non-alphanumeric character becomes `_` (not only U+0020, and `-` too),
so `Q1-2024` slugs to `q1_2024` and `Region(code)` to `region_code`;
- runs of `_` collapse, so `a_ b` and `a__b` both slug to `a_b`;
- leading and trailing `_` are removed, so `East ` → `east` and `-East-` → `east`;
- `n` is prepended to a slug that starts with a digit, so `2024` → `n2024`;
- a hand-written id must obey every syntax rule of a generated one.
This section, `slugify`, and the tests
`slugify_collapses_a_run_of_spaces_to_one_underscore` (its `east_` case) and
@@ -1750,8 +1751,8 @@ the alphabet (non-empty, `a`–`z`, `0`–`9`, `-`, `_`), the reserved leading
`_` (only `_index`, `_dim` and `_measure` may carry one, per clause 1, and
the match is exact: `_indexer` and `_measure-1` are Reserved, since "no other
id may start with `_`"), and
duplicates. It accepts `-`, `-x` and `x-`. ADR 007's open question 11 asks
whether a hand-written id like that must be refused. It never renames.
duplicates. It accepts `-`, `-x`, `x-` and `2024`, all of which the amendment
refuses (a hand-written id obeys the syntax of a generated one). It never renames.
Refusing is the loader's job, and the loader
must name both lines of a duplicate; `ClaimError` carries only the id, so the
loader keeps the line numbers. The virtual views `_records` and `_drill` are