Repository navigation
Commit f623e2f
fix(spec): the protocol 17 → 18 upgrade rationale states each cited decision in words instead of a tracker number (stage 9) (#21698)
Part of #20749
Clause-②: no
Stage 9 of the `domain:spec` lane's share of the runtime-string
burn-down (ruling `5902360492`, form D): the author-visible tracker ids
in **step 18's** rationale in
`packages/spec/src/migrations/registry.ts`, per the pointer `5968465093`
and stage 8's landing `5976757373`. Every cited decision is now stated
in words, or the citation is dropped where its sentence already said
what was decided. Text only. The test strings (class (e)) are a later
stage, so this PR closes no card.
## What changed
- **`STEP18_RATIONALE`: 49 of its 79 fragments carried 88 tracker ids.**
72 are objectstack issue numbers, 12 point into objectui (nine issues
spelled `objectui#N`, one issue spelled `ui#N` twice, one PR), and 4 are
director decision-batch numbers that the gate's id pattern also counts.
Exactly those 49 fragments changed, and each now reads id-free. Fragment
ids, orders and the other 30 fragments are byte-identical.
- **Two two-digit decision-batch numbers** (`decision batch #91`, `#77`)
sat inside two of the same parentheticals. The gate's pattern (three to
five digits) does not count them. They went with the sentence they
belonged to, and the ruling they named is stated instead.
- **No reader pinned the rewritten words.** Every string and regex
literal in the repository's code was matched against each fragment's old
and new text. No assertion that matched the old text fails on the new
one. See "Readers" below.
- **Nothing to regenerate.** No generated artefact prints step 18 yet:
`docs/protocol-upgrade-guide.md` prints majors up to `PROTOCOL_MAJOR`
(17), and `spec-changes.json` carries no step rationale.
`check:generated` reads all 15 artefacts up to date at this head. `os
migrate meta` prints the text at runtime today, because its chain runs
to the highest registered major.
- One `@objectstack/spec` **patch** changeset, `Clause-②: no`. The
rationale ships in `dist/migrations`.
- No step-17 text, no code comment, no test title, and no schema, order,
id, conversion or behaviour change.
## Census at the base (A1)
Base `7d0781482d`, the worktree before any edit, which is the claim's
stamp. I reused stage 8's instruments byte-identically (`census.cjs`,
md5 `6e42a45a926d375013c32d62f16a296e`; `skeleton.cjs`, md5
`a43a72c63f55c883da7a54b6f600ca08`) and held the census to the same
reference: `check-doc-authoring.mjs --census`, run from a scratch copy
whose `PACKAGES_PROSE_ROOT` / `PACKAGES_PROSE_EXCLUDED` point at
`packages/spec/src` and at nothing. The copy is byte-identical to the
one stages 7 and 8 used (md5 `8edc9a4064f55d2766c1eb0d3e8d889f`; the
live gate differs from it only on those two lines). Result: **88 = 88
ids, 0 differences per (line, id)**.
| region of `migrations/registry.ts` | messages | ids | gate literals |
|:--|--:|--:|--:|
| step 17 (`const step17`) | 0 | 0 | 0 |
| step 18 (`STEP18_RATIONALE`, lines 5025–6408) | **49** | **88** | 81 |
| step 18 entries (`surface` / `replacement` / `reason` /
`acceptanceCriteria`, generated regions) | 0 | 0 | 0 |
| rest of the file | 0 | 0 | 0 |
- **Reconciled with stage 8's 46 / 85.** I re-ran the instrument on the
registry at stage 8's base `eea82af677`: 46 / 85, reproduced. Between
that base and this one, PR #21673 (stage 5 of #21464, landed as
`72f3c74d60`) added three step-18 fragments, orders 71–73
(`ui-object-metric-compare-to-typed`,
`ui-object-metric-drill-down-typed`, `ui-object-grid-columns-typed`),
each citing that card once. 46 + 3 = 49 and 85 + 3 = 88. No other
fragment differs between the two bases. Ancestry with a control leg on
this shallow checkout: `72f3c74d60` is an ancestor of HEAD (exit 0) and
not of `eea82af677` (exit 1), while the older stage-7 landing
`36e4647520` is an ancestor of `eea82af677` (exit 0).
- 1859 files scanned, 1355 parsed, 0 parse diagnostics. Test strings at
the base: 1803 messages / 1919 ids in 425 files, stage 8's after-count.
- **Lit controls:** at the base, `:5066` (`#8681`) and `:5126` (`#118`)
count. After the rewrite, an id planted into a scratch copy of the new
file (one literal of the `targetVariable` fragment) reads 1 message / 1
id, folded over the whole fragment (lines 5515–5522).
- **Dark controls:** in the `STEP18_RATIONALE` region, 96 raw `#NNN`
hits, 8 of them on comment lines (the step-18 docblock,
`:6421`–`:6440`), which read 0: 96 − 8 = 88. Past `:6444`, 957 raw hits
sit on comment lines in the generated entry regions, and all read 0. The
header docblock (`:19`, `:32`, `:40`, `:62`) and step 17's 18
comment-line hits read 0.
## After
At `9b8c7f38d7`: step 18 reads **0 / 0**, step 17 still reads 0 / 0, and
`packages/spec/src` outside test strings reads 0 messages / 0 ids. The
gate reference agrees (0 sites). Test strings are unchanged at 1803 /
1919.
## Text-only proof
Stage 8's `skeleton.cjs`, base copy against the head file: **SAME —
22335 skeleton tokens, 3508 string groups, 49 changed (each id-bearing
before, id-free after), diagnostics 0/0.** A `+` chain's string run is
one group, so a fragment's whole text is one group; the 49 changed
groups are the 49 fragments, and every other string in the file
(fragment ids, entry ids, surfaces, conversions) is byte-identical.
Controls, each mutation counted on disk on a scratch copy of the head
file (anchor hit 1, mutation landed):
| control | expected | got |
|:--|:--|:--|
| rename the `step18` binding | DIFF, exit 1 | exit 1 |
| a fragment's `order` 43 → 44 | DIFF, exit 1 | exit 1 |
| a fragment's `id` string renamed | VIOLATION, exit 3 | exit 3 |
| a step-18 entry's `surface` edited | VIOLATION, exit 3 | exit 3 |
| a literal re-split inside a `+` chain | SAME, exit 0 | exit 0, 49
changed |
| an id-free fragment's text edited | VIOLATION, exit 3 | exit 3 |
| one fragment reverted to its base text | SAME, 48 changed | exit 0, 48
changed |
| an id planted in a rewritten fragment | VIOLATION, exit 3 | exit 3 |
Lines grew where words replaced a number: the longest new line is 111
characters, inside the region's existing maximum of 117. There is no
`max-len` rule; ESLint reads 0 / 0 on the file.
## Per-record table
Every cited record was read through REST with all its comments (59
objectstack issues and PRs, 11 objectui issues and PRs). The four
three-digit decision-batch numbers are not cards of this repository:
objectstack issues 118, 121, 132 and 210 are unrelated broken-link
reports and a docs PR from 2026-01, which is the control that they are
batch numbers. Each batch's decision was read from where it is recorded
(a ruling comment, or the spec CHANGELOG). Numbers below are written
bare.
| fragment (order) | record | decision as read (where) | in the text now
|
|:--|:--|:--|:--|
| `metadata-plugin-additional-types-retired` (2) | 8586 | remove
`additionalTypes` (ruling 5293094846) | dropped |
| `field-scale-precision-integer-refused` (3) | 8321 | refuse malformed
`scale` / `precision` at authoring (body, ACCEPT 5296942541) | dropped |
| same | 7501 | enforce `scale` by rejecting, never by rounding (ruling
5250623270) | "the write-time `scale` check, which refuses an over-scale
value rather than rounding it" |
| `admin-export-wildcard-removed` (4) | 8681 | remove `allowExport` from
the `'*'` entry of both admin sets (ruling 5299825940) | dropped |
| same | 5491 | remove `member_default`'s `'*'` object grant on create /
read / edit (ruling 5219845380) | "the earlier removal of
`member_default`'s CRUD wildcard" |
| `record-chatter-position-vocabulary-converged` (5) | 8762 | the
renderer's vocabulary, a conversion, the defaults dropped (ruling
5299771841) | dropped; the ruling's date stays |
| `element-input-target-variable-retired` (6) | 9198 | retire the hint,
the retirement route (ACCEPT 5311252358) | dropped |
| `element-filter-retired` (7) | 9220 | retire `element:filter` at
element grain (verdict 5312176877) | dropped |
| same | 9198 | its sweep recorded the element-grain lead and left it |
"the wider finding that the `targetVariable` retirement recorded" |
| `element-form-retired` (8) | 9249 | retire `element:form` at element
grain (cloud reading 5350190376) | dropped |
| same | 9220 | the `element:filter` retirement's own verdict sweep
found it | "the `element:filter` shape … recorded by that retirement's
own verdict sweep" |
| same | 7751 | direction A: the `object-*` blocks' props enter
`ComponentPropsMap`, so the component-props gate judges them (ruling
5261743573) | "its props declared for the component-props gate" |
| `field-inline-and-related-list-columns-closed` (9) | 9227 | a strict
element schema for `inlineColumns`, `relatedListColumns` in the same
pass (ruling 5315735776) | dropped |
| same | objectui 3951 | the published spelling `name` wins, the reader
is fixed, no tolerant dual read (ruling 5236150020) | "objectui aligned
the widget to `name` and retired the `field` spelling with no tolerant
alias" |
| `cube-metric-filters-retired` (10) | 10414 | the remove leg (triage
5363699539) | dropped |
| same | 10298 | the dataset path answered unfiltered aggregates under
the measure's name (body) | "the same defect the dataset path had" |
| same | 10411 (PR) | the strategy compiles a measure's `field` and
`filter` on both doors (ACCEPT 5360094389 on 10298) | "repaired … when
the analytics strategy began compiling each dataset measure's `filter`"
|
| `record-highlights-field-icon-retired` (12) | 10054 | option A, retire
(ruling 5364978909) | dropped; the ruling's date stays |
| same | 8691 | the rail's row declares what the renderer reads; `icon`
is refused, since no render path reads it (ACCEPT 5296436860) | "the
shape that got the reference-rail `icon` refused" |
| same | 5176 | A: declare `readonly`, because the chip's gate reads it
(ruling 5196402876) | "declared because the chip's read-only gate reads
it" |
| `page-component-responsive-retired` (15) | 11027 | B: retire, and
repair the four redirect texts (ruling 5380752244) | dropped |
| same | 4876 | A: retire `widgets[].responsive` (ruling 5169512655) |
dropped; "tombstone" already says it |
| `object-grid-default-sort-retired` (17) | 11805 | retire, the
changeset not major (ruling 5404972152) | dropped |
| same | objectui 5861, objectui 4869 | 2026-08-22 「接受所有」: lower the
legacy pair through the shared sort sink now, and retire
`table.defaultSort` on its own card (ruling 5377367456 on 4869) | "the
producer half of objectui's `table.defaultSort` retirement, which the
maintainer's 2026-08-22 「接受所有」 ruling on objectui's sort sink ordered" |
| `permission-restore-purge-bits-retired` (18) | 12497 | the
implementing card of the ruling below (body) | dropped |
| same | 1883 | B: retire the two bits; the card stays open as the M2
anchor, and the keys return with the feature (ruling 5421209848); gating
operations that do not exist is false compliance (ruling 5163046733) |
"which chose retiring the two bits over gating operations that do not
exist"; "the M2 lifecycle initiative …, which stays open as their
anchor" |
| same | 8106 (PR) | pins the engine's seven-verb dispatch vocabulary
(body) | "which a test pins" |
| same | 3004 | `owner_id` is server-managed for non-privileged writers
(body); `allowTransfer` enforced through that write guard (1883 ruling
5163046733) | "the server guards who may rewrite a record's owner" |
| `field-reference-to-spelling-retired` (21) | 13700, objectui 6837 | C:
the server normalizes the protocol and the renderer only executes it;
half 1 on the server, half 2 deletes objectui's arms (ruling 5475017957;
PASS 5480047935) | "the server half of the maintainer's 2026-08-31
ruling …"; "which the ruling's objectui half deletes" |
| same | 4923 | equal values: the shadowed alias is deleted with a
notice; different values: both kept, the strict door names both (ruling
5173149122) | "the house precedence for a shadowed alias" |
| `compliance-deadline-keys-retired` (23) | 14477 | A: retire per
family; e-signature too unless roadmapped (ruling 5518646938) | dropped;
"the deadline-key ruling" |
| same | 15513 | A: the three families retire whole, not roadmapped
(ruling 5548577921) | dropped |
| `duration-keys-unit-in-key` (24) | 14478 | B: a gate plus a conversion
of every offender, no baseline (ruling 5518649320) | dropped; the
ruling's date stays |
| same | 14519 | folded into that ruling (5546967881) | dropped |
| `metadata-manager-config-inert-cache-keys-retired` (25) | 14478 | the
rename gave `ttl` its `ttlSeconds` spelling | "the duration rename's
respelling of `ttl`" |
| same | 15624 | the outer three keys are read by nothing (triage
5548953183) | dropped |
| `cron-positions-deleted` (26) | 16320, 15954 | retire the seven cron
positions family by family (option A), not mark them experimental
(ruling 5559778263) | "the 2026-09-06 ruling retired each family rather
than marking it experimental" |
| same | 3786 | derive a list from its single source, never a hand copy
(sweep 5131678201) | dropped; the sentence says what the lint does |
| `list-view-page-mount-retired` (27) | 17063 | 「撤」, 2026-09-09 (body) |
dropped |
| `object-kanban-quick-add-retired` (28) | 17260, objectui 8285, batch
91 | B: the board grows no inline record-creation path, and the key
retires (ruling 5583979207) | "the director-seat ruling of 2026-09-08
that the board grows no inline record-creation path and retires the key"
|
| `list-view-sort-string-clause-retired` (29) | 17053, objectui 8221,
batch 77 | B: the legacy string clause is retired, one spelling, the
array (ruling 5567944420) | "ruled 2026-09-07: the legacy string clause
is retired, one spelling, the array" |
| same | objectui 8758 (PR) | `convertSortToQueryParams` refuses a
runtime string; merged 2026-09-09, before the spec half | "whose
consumer half shipped in objectui first" |
| `chart-config-aria-retired` (31) | batch 118 item 2 | recommendation
C: remove rather than enforce, the protocol judged wrong for this one
key (spec CHANGELOG entry `2bf6ef1`) | "maintainer decision of
2026-09-12 — judge the protocol wrong for this one key" |
| `dashboard-widget-chart-config-structure-refused` (32) | batch 121
item 1 | C+D: the dataset owns structure, `chartConfig` appearance, and
the structural keys are refused by name (ruling 5644017639 on card
17385) | dropped; the ruling's date stays, and the sentence states the
split |
| `translation-per-app-settings-platform-only` (33) | 15178, batch 132
item 2 | ②: the bundle type splits, and the per-app bundle refuses
`settings` (ruling 5653315643) | "maintainer ruling 2026-09-13: settings
copy belongs to the platform" |
| same | 19620, batch 210 item 2 | B: `settings` leaves the item door
too, one app metadata type, one shape (ruling 5770445203) | "maintainer
ruling 2026-09-22: one app metadata type, two authoring doors, one
accepted shape" |
| `object-tenancy-organization-field-retired` (34) | 19054 | off the
authorable surface, kept as a platform fact (body) | dropped |
| `page-component-filter-record-to-rule-array` (36) | objectui 6206 | B:
one filter spelling platform-wide, the rule array (ruling 5406409590) |
"ruled 2026-08-25: one filter spelling platform-wide, the rule array" |
| same | 17321 | B: a partial conversion of the lossless subset;
combinators stay as stored (ruling 5644018752) | "ruled 2026-09-12"; the
sentence states the rest |
| `view-item-owner-hidden-retired` (37) | 20085 | retire both keys
(triage 5826969296) | dropped |
| `ui-report-joined-chart-retired` (38) | 20161 | retire (triage
5852548444) | dropped |
| `view-overlay-owner-hidden-retired` (39) | 20230, 20085 | follow the
view item's disposition for the same pair (triage 5856621469) | "the
view item's disposition for the same key pair, followed here as triage
directed" |
| `ui-form-layout-inline-grid-retired` (40) | 20221 | retire `inline` /
`grid` (triage 5855767378) | dropped |
| same | 18900 | A′: a capability mainstream platforms have gets its
consumer once; one they lack retires (ruling 5727134555); triage applied
it: multi-column already exists, as `columns` | "the maintainer's family
criterion (a capability mainstream platforms have is served once, here
by `columns`)" |
| `currency-config-precision-retired` (41) | 19992 | retire (triage
5817146460) | dropped |
| `permission-rls-tags-retired` (42) | 20321 | RETIRE by the criterion
(triage 5860425529) | dropped; the sentence states the criterion |
| `flow-decision-edge-branching-first-match` (45) | 15429 | 「跟主流对齐」:
first match, inclusive only when declared (ruling 5793803317,
2026-09-23) | dropped; the ruling's date added |
| `ui-object-master-detail-form-details-closed` (56) | 20928 | a strict
detail entry (ACCEPT 5936904192) | dropped |
| `ui-record-line-items-props-closed` (57) | 21142 | the row and the
producer fix (ACCEPT 5941249538) | dropped |
| `ui-object-grid-export-options-closed` (59) | 21229 | the object form
only, refused, not lifted (triage 5939380297) | dropped |
| `translation-widget-sub-caption-retired` (60) | 21257, objectui 11389
| C: retire both ends (ruling 5942430353, 2026-10-01) | "maintainer
ruling 2026-10-01" |
| same | 5428 item 4 | the sub-caption got its own key beside
`widget.description` (ruling 5200254075, 2026-08-06), which C reverses |
"which reverses the 2026-08-06 ruling that gave it a translation key of
its own" |
| `object-grid-resizable-columns-retired` (62) | 21445 | retire the
alias (triage 5958164933) | dropped |
| same | objectui 6152 | `resizable` is canonical, and the alias retires
with no window (seat answer 5957903936) | "objectui's ruling that
`resizable` is canonical" |
| `ui-object-grid-row-members-typed` (63) | 21445 | type the members
(triage 5958164933) | dropped |
| `ui-object-map-gantt-tree-navigation-typed` (64), list members (66),
form members (67), metric aggregate / trend (69), compare-to (71),
drill-down (72), grid columns (73) | 21464 | the staged close-out (seat
answer 5963787404) | dropped ×7; each sentence names its stage |
| `ui-ai-chat-window-retired` (65) | 21504 | retire (triage 5963897014)
| dropped |
| `element-text-variant-heading-subheading-retired` (68) | 21015 |
release 2 of objectui's ruled two-release split (body) | dropped; the
sentence names the split |
| `object-master-detail-form-detail-sort-field-retired` (70) | 21589 |
retire (triage 5969870827) | dropped |
| same | objectui 11070 (round 9) | the authored override retires; the
sort field is derived from the child object (seat answer 5928627070,
landed 5930904717) | "the spec half of objectui's own retirement of the
override" |
## Readers
- **Searched by substring, not by guess.** A TypeScript-AST pass over
all 9878 tracked code and text files collected every string literal (8+
characters) and regex literal, and tested each against each fragment's
old and new text, and against the joined rationale. A reader is owed a
move only if it matched the old text and not the new. In the files that
read the registry or the rationale, the only literals that lose a match
are generic words and patterns that match some fragment by coincidence
(`'decision'`, `'objectui'`, `/100/`), plus the tracker-id detectors,
which are absence checks that now pass. **No pin needed moving.**
- **The step-18 readers that exist, and why they hold:**
- `ui/component.test.ts` pins the phrase "retires `object-kanban`'s
`quickAdd`" and pins that the rationale never says `kanban-ui`. That
second pin shaped the rewrite: objectui's ruling kept the control on
`kanban-ui`, a node type objectui has since retired, so the new text
states the ruling without naming it.
- `system/compliance-families-retirement.test.ts` pins `/retires those
three compliance-shaped families WHOLE/`, kept verbatim.
- `data/cube-metric-expression-types-retirement.test.ts` reads a
fragment this stage does not touch.
- `scripts/step18-rationale-merge.test.ts` pins the fragments' sort and
that the rationale equals their joined text. Neither moves.
- **No reverse verification was owed:** no pin moved, so there is no
moved assertion to turn red. The text-only proof's controls and the
census's lit control stand in its place.
- **Parallel copies, not readers:** the spec CHANGELOG, the 17.1 release
notes and three liveness-ledger notes carry their own copies of some old
phrases. They are release-owned or ledger notes, and nothing compares
them with the rationale.
## Tests and gates (at `9b8c7f38d7`)
All builds and tests ran through `scripts/pm/os-verify-lock.sh`, with
each exit code written to a file before reading.
- `pnpm --filter @objectstack/spec build`: exit 0, 38/38 dts.
- `pnpm --filter @objectstack/spec check:generated`: exit 0, all 15
artefacts up to date.
- The step-18 readers, local project: `migrations.test.ts`,
`component.test.ts` and the cube-metric test, 561 / 561. Repo project:
`step18-rationale-merge.test.ts` and the compliance test, 19 / 19. The
other 19 repo-project tests that import the registry: 314 / 314.
`conversions-major18-merge.test.ts`: 12 / 12.
- Full spec suite, local project: 610 files, 18113 passed, 1 todo.
- `pnpm --filter @objectstack/spec run typecheck`: exit 0.
- A turbo build of 71 / 71 packages, then the derived union: **84 / 84
exit 0**, reconciled by `dispatch-gates --ran` (84 run, 0 NOT-MEASURED,
0 UNRUN). Three gates first answered PREREQUISITE NOT MET (exit 3,
nothing measured) before the 71-package build, and passed after it.
- ESLint on `registry.ts`, a proven narrowing: 1 file, 0 errors, 0
warnings. The population comes from ESLint's own config
(`calculateConfigForFile` returns a config, `isPathIgnored` is false).
Invariance: `parserOptions.project` and `projectService` are null, so no
type-aware rule exists and an untouched file's verdict cannot move.
- `dist`: the new phrases are in `dist/migrations/index.js` and
`index.mjs`; the old runtime phrases probed are in no `dist/migrations`
file. The ids still present in `dist` are code comments, which this
stage does not touch.
- NOT MEASURED locally: the spec repo project as a whole (stopped at the
foreground limit with no output; its registry readers ran above), and
`scripts/build-schemas-check-mode.test.ts`, which reads semantic
`surface` strings and never the rationale. Both are CI's, as is the
repo-wide `pnpm lint`.
## Overlap
#21654's PR #21687 (draft, this seat) adds one `STEP18_RATIONALE`
fragment (order 74, no tracker id in its text) and one D3 entry. A local
`git merge-tree` of this head with its head `a9d2d5d453` is clean, and
`registry.ts` has no merge driver, so that result is the plain text
merge GitHub runs. Whichever lands later merges `origin/main` through
`scripts/pm/os-regen-merge.sh`. `origin/main` was re-fetched just before
opening this PR (`38bef8cf95`): the four commits since the base touch
neither this file, the conversions registry nor the guide, and a local
`git merge-tree` with it is clean.
## Acceptance notes
- **Out of this stage's reach, noted:** `registry.ts`'s code comments
still carry many ids (the step-18 docblock and the generated entries'
comments), and so does `dist`, which keeps comments. They are the
comment lane's share.
- **Kept on purpose:** `decision-inbox batch 4` and `batch 5` name a
batch without a `#` number and are not ids. Present-tense clauses whose
follow-through has since landed (for example "held up today only by
objectui's fallback arms") were left as written: form D asks for the
decision, and the decision is stated.
- **Ledger notes:** `packages/spec/liveness/dashboard.json`, `page.json`
and `view.json` cite some of the same decision batches in their notes.
They are not runtime strings and sit outside this census; nothing here
touched them.
## Clause-② and changeset
`Clause-②: no`. Nothing authorable, exported or typed is added, removed
or renamed; only the value of `MIGRATIONS_BY_MAJOR[18].rationale`
changes. It ships in `dist` and `os migrate meta` prints it, so one
`@objectstack/spec` patch changeset is owed, and `skip-changeset` does
not apply.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8843505 commit f623e2f
2 files changed
Lines changed: 119 additions & 89 deletions
File tree
- .changeset
- packages/spec/src/migrations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
0 commit comments