`007-stable-identities.md`). This file states only what the amendment changes or
adds, numbered by the accepted ADR's clauses; everything else in ADR 007 is still
true. Where the amendment replaces an ADR 007 rule or an existing behaviour:
| clause | accepted rule or existing behaviour | the amendment |
|---|---|---|
| 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` |
| 1 | the virtual categories are declared and named in the file (`## Category: _Measure`, `_Measure: column`) | they are defined by criteria with hardcoded ids and displays and referred to by id (`_measure`); the `## Category: _Measure` header lists the stored measures but declares nothing |
| 1 | `measure_item_names` merges a formula into a stored measure of the same name | a formula never merges: it gets its own id, `profit-<n>` beside a stored `profit` |
| 2 | a display already in use in a scope is refused | equal displays in a scope are shown with a gapless ` #2`, ` #3`, … suffix in id order |
| 4 | ids sort by plain string order (`CellKey::new` sorts with `String::cmp`, and the writer orders data lines by their text) | ids sort in natural order. `CellKey`'s canonical coordinate order changes with it, so `RecordHash` values change (they are on the wire, not in files), and a saved file's data lines and their coordinates are written in the new order; the loader canonicalises keys, so the data read back is the same |
| 4 | records mode string-sorts the store on entry (`DataStore::sort_by_key`, run by `SortData`) | records-mode rows come out in natural order, and so does the order `matching_positions` folds in (gotcha 18), so a `SUM`/`AVG` can change in the last bits |
| 4 | the writer string-sorts `hidden:`, `expanded(…)`, `collapsed:` and `## Aggregation` lines | those lines are written in natural order too |
| 4 | a GP fit string-sorts its records (`improvise-gp``dataset.rs`) | the feature matrix's row order, and so a seeded fit's folds, follow natural order: a seeded fit can produce a different tree |
| 4 | `app.rs` view-shape fingerprints and `Model`'s `Debug` string-sort ids | they sort naturally too; nothing visible changes |
| 2, 5 | a collapsed entry and a group reference name a group by name | by its id |
| 4 | casefold; a run of U+0020 spaces becomes one `_`; strip characters outside `a`–`z`, `0`–`9`, `-`, `_`; remove a leading `_` | casefold; every character outside `a`–`z`, `0`–`9` and `-` becomes `_`; runs of `_` collapse; leading and trailing `_` and `-` are removed; `n` is prepended before a leading digit |
| 5 | a hand-written id uses the alphabet `a`–`z`, `0`–`9`, `-`, `_` | a hand-written id obeys every syntax rule of a generated id |
| 6 | display names typed into minibuffers are turned into ids at the input edge | only CSV import and the `.improv` loaders read display names in files and command-language text, which names things by id; interactive input matches a display or an 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 | the test edits a non-name token and commits |
## Context
Implementing the accepted ADR (epic improvise-cq1q) and using it raised points the
accepted text does not settle: whether groups and measures carry ids, what a virtual
category is in a file, exactly what slugify produces, which inputs may read display
names, and which side resolves names in a formula edit. This amendment settles them.
## Decision
### 1. What carries an identity
**Groups have ids.** A group has an id and a display, like a category or an item, and
its id is an ordinary id: unique in the document, generated by slugify (`group` is an
empty slug's `<kind>`), declared beside its display, referred to by id, created by
display, and renamed by a display rename. Its wire address is `GroupId`. In code,
PR #336 (improvise-cq1q.12) already adds a display to `Group` beside its key; that key
becomes the group's id.
**Virtual ids.**
- **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 `_`, so the grammar does
not check the reservation.
- **Virtual ids are lower case.** The code, the wire, `ViewState` and the browser
client spell the virtual categories `_Index`, `_Dim` and `_Measure` today, and move
to `_index`, `_dim` and `_measure` in the re-key step.
- **Virtual categories and measures are defined by criteria, not declared.** Each
contains whatever in the document meets its criterion: `_measure` holds the measures,
and `_index` and `_dim` hold the record positions. Their ids and displays are
hardcoded in the application, so a file never declares one, and a line that refers to
one spells its id: `_measure: column`, `region=east, _measure=revenue = 1200`.
- **The stored measures are still listed in a `## Category: _Measure` section**, each
with its display and id (`- Revenue(revenue), Cost(cost)`). Its header names the
hardcoded virtual category and is not a declaration of it. The section is complete:
it lists every stored measure, so a file never relies on its data lines to discover
one. A formula's measure is not listed there; it is declared by its `## Formulas`
line, which carries the id it shares with its formula. The loader recognises
`_Measure`, `_Index` and `_Dim` by their hardcoded displays, and references are
written `_measure`.
- **Records mode's synthetic positions are values, not ids.** The row coordinate under
`_index`, and by the same reasoning the column coordinate under `_dim`, are values of
those virtual categories. They take no part in id uniqueness and cannot collide with
a user's item.
**Formulas and measures.** A measure is the normal formula target (`profit = revenue -
cost`); a dimension target is rare, for a derived dimension.
- **A formula is a kind of measure, so a `_Measure` formula's id is its measure's id.**
`Profit = Revenue - Cost` is one thing, `profit`: other formulas, each Profit cell's
`CellKey`, and `ReplaceFormula`/`RemoveFormula` all name `profit`.
- **Each measure's cells are keyed by one handle:** `ItemHandle::Item(id)` for a stored
measure, `ItemHandle::FormulaTarget(id)` for a formula's measure, never both.
- **A formula never merges into a stored measure.** With a stored `profit` (displayed
`Profit`), adding `Profit = Revenue - Cost` creates a second measure with a fresh id,
`profit-<n>`, by the ordinary collision rule. The stored measure keeps `profit` and
its data; removing the formula removes only `profit-<n>`.
- Both are named `Profit` in `_measure`'s scope, so they are shown as `Profit` and
`Profit #2` (clause 2's suffix rule).
- **A target outside `_Measure` is a real item** with its own `ItemId`, and its formula
has a separate `FormulaId`: a formula `Tax` in `CustomCat` gets item id `tax`, then
formula id `tax-<n>`.
- **Formulas may refer to anything by id.** A function in a formula may accept any
id-bearing thing (category, item, measure, formula, view, group) by its id. Whether a
function takes the thing or its value (an item's display, say) is part of that
function's signature.
### 2. Storage keys
- **`CellKey` pairs are `(CategoryId, ItemHandle)`**, so a `_Measure` formula cell's
coordinate is `FormulaTarget` without claiming an `ItemId` the document does not
hold.
- **A collapsed entry names its group by the group's id**, and `Group` gains a
`display`.
- **Displays stay unique within a scope by suffixing.** The accepted text refuses a
display already in use in the scope; a formula's measure beside a stored measure of
the same name (clause 1) cannot be refused. So when several things in one scope have
the same display, they are shown with a suffix: the first keeps the bare display, and
the rest get ` #2`, ` #3`, … (a space, then `#` and the number) as a gapless
sequence in the order their ids sort.
- The order is the ids' natural order (clause 4), so `profit`, `profit-1`,
`profit-2` and `profit-10` show as `Profit`, `Profit #2`, `Profit #3` and
`Profit #4`.
- Gapless: if the thing shown as `Profit #2` is removed, `Profit #3` becomes
`Profit #2`.
- The suffix is computed when a display is shown, from the things that share it, and
is never stored or written to a file.
- A suffixed display is a display like any other: interactive input that types
`Profit #2` picks that thing (clause 6), and the ids stay the unambiguous name.
- Old files cannot hold two things with one name in a scope, so loading one adds no
suffix.
- A suffix never repeats a display already in the scope. If something is literally
displayed `Profit #2`, the shared `Profit`s take the next free numbers instead:
`Profit`, `Profit #3`, `Profit #4`, still in id order. "Gapless" counts only the
suffixes actually assigned, so a number taken by a literal display is skipped,
like an id collision candidate that is already held.
### 4. How an id is made
**Slugify's steps:**
1.**Casefold**, with Unicode full case folding (`Straße` → `strasse`).
2.**Replace every character outside `a`–`z`, `0`–`9` and `-` with `_`.** Ids'
alphanumerics are ASCII and include `-`; whitespace, punctuation and every non-ASCII
letter become `_`.
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 `_` and `-`**, repeating until neither end holds
either.
5.**If the slug starts with a digit, prepend `n`**, so no id is purely numeric or
starts with a digit: `2024` → `n2024`.
6.**If nothing is left, use `<kind>-<n>`** from the shared counter. A display of only
`-`, only `_` or only non-ASCII letters ends here.
**The syntax of an id:** a lower-case ASCII letter, then `a`–`z`, `0`–`9`, `-` and
single underscores, never ending in `_` or `-`. Slugify's output has this shape, so
slugifying an id returns it unchanged, and a collision suffix `-<n>` keeps the shape.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.