Skip to content

fix(spec): os migrate meta guidance for the engine-* migration entries states each lesson in words, not tracker numbers (stage 1) - #20285

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20233-migrate-meta-tracker-free-stage-1
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20233-migrate-meta-tracker-free-stage-1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20233

Clause-②: no

Stage 1 of a staged card. The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, surface, from / to, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before.

What this does

os migrate meta prints every ADR-0087 semantic entry it crosses as one block: ⚠ [protocol N] surface → replacement, then why: (the entry's reason) and verify: (its acceptanceCriteria). Those three fields are text an author is shown, and AGENTS.md's runtime-string rule applies to them: 「Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number (pnpm check:doc-authoring): the lesson goes into the text.」 Form D of ruling C+D on the parent card (comment 5749154545, 「同意」) sets the shape: the lesson in words, and no number, dead or alive.

This stage rewrites the busiest entry family, the five engine-* entries: 67 sites → 0. Each site now says what the cited ruling, measurement or fix decided. ADR ids stay, because an ADR lives in this repository. registry.ts, spec-changes.json and docs/protocol-upgrade-guide.md are regenerated from the entries (gen:migration-registry, gen:spec-changes, gen:upgrade-guide), never hand-edited. One new pin holds the printed output tracker-free.

Census — tracker ids in the three author-shown fields

Instrument. A TypeScript-AST walk over every packages/spec/src/migrations/entries/**/*.ts. For each entry object literal it evaluates the string value of replacement, reason and acceptanceCriteria (string literals joined with +), then counts # followed by 4 or 5 digits at a word boundary. Tree: objectstack-ai/objectstack at 443b2f4fdc (this branch's base).

Controls.

  • Lit: 17.aggregation-node-distinct-retired.ts reads 7 sites (replacement 1, reason 6), the ids a reader sees in its text.
  • Dark (comment lines): 733 // lines in entry files carry a tracker id, and none is counted. For example, 18.client-envelope-convergence-analytics-automation.ts has 5 such comment lines and counts 0. Those comment sites belong to the sibling card, and ⛔ this PR does not touch them.
  • Dark (other fields): surface is not in the three-field count. 17.authoring-schemas-strict-unknown-keys.ts carries one id in surface, and it is absent from the table. The surface field is counted separately under Acceptance notes.
  • Dark (other tables): the 413 retired-keys/ and retired-defs/ files have none of the three fields. They count 0, while carrying 687 comment-line hits.

Totals at 443b2f4fdc: 1,016 sites in 266 semantic entries, 460 distinct ids, split major 17: 417 and major 18: 599. By field: replacement 61, reason 901, acceptanceCriteria 54. The line-level upper bound over all entry files is 1,524 lines in 679 files.

Why this is not the relayed 2,060. That figure scanned packages/spec/src/migrations/**, where the generated registry.ts repeats every entry's prose. At the base, registry.ts alone holds 2,113 tracker-shaped occurrences. The entry files, which are the source the generator copies, hold 1,016 sites in these three fields.

Dead ids. All 22 distinct ids in the chosen family answer 200, so none is dead. The other 438 ids are not re-probed at this stage.

After this PR: 1,016 → 949 (the engine- family 67 → 0, every other family unchanged).

Families are grouped by the entry-id prefix: the first word of the id, which is the name family. All three-field sites live in the one semantic/ directory, so the directory does not separate them.

# family (entry-id prefix) entries sites major 17 / 18 replacement / reason / acceptanceCriteria distinct ids
1 engine- (this PR) 5 67 50 / 17 11 / 55 / 1 22
2 ui- 17 65 29 / 36 6 / 58 / 1 42
3 plugin- 11 46 12 / 34 1 / 39 / 6 31
4 driver- 7 44 19 / 25 0 / 44 / 0 25
5 kernel- 9 44 0 / 44 1 / 41 / 2 11
6 system- 10 44 0 / 44 0 / 42 / 2 6
7 datasource- 9 43 10 / 33 3 / 38 / 2 14
8 filter- 10 30 8 / 22 1 / 28 / 1 23
9 field- 8 28 9 / 19 5 / 20 / 3 20
10 action- 5 26 24 / 2 0 / 23 / 3 19
11 element- 4 25 0 / 25 2 / 19 / 4 17
12 data- 6 24 15 / 9 0 / 24 / 0 15
13 export- 3 21 15 / 6 0 / 21 / 0 15
14 hook- 3 17 14 / 3 0 / 17 / 0 12
15 rest- 4 17 2 / 15 0 / 17 / 0 14
16 api- 4 16 9 / 7 0 / 14 / 2 11
17 metadata- 7 16 0 / 16 1 / 15 / 0 11
18 view- 7 15 7 / 8 0 / 13 / 2 10
19 analytics- 5 14 1 / 13 2 / 12 / 0 10
20 audit- 2 14 14 / 0 2 / 12 / 0 8
21 sharing- 2 14 14 / 0 1 / 13 / 0 11
22 actor- 1 13 13 / 0 0 / 13 / 0 9
23 http- 2 13 13 / 0 0 / 13 / 0 11
24 object- 4 13 0 / 13 1 / 11 / 1 11
25 dataset- 3 12 0 / 12 2 / 8 / 2 9
26 hot- 2 12 0 / 12 0 / 10 / 2 5
27 external- 1 11 11 / 0 0 / 11 / 0 8
28 package- 5 11 3 / 8 0 / 10 / 1 8
29 query- 6 11 11 / 0 4 / 7 / 0 6
30 delete- 1 10 10 / 0 0 / 10 / 0 4
31 stack- 2 10 0 / 10 3 / 4 / 3 9
32 etl- 1 9 9 / 0 1 / 8 / 0 7
33 flow- 3 9 1 / 8 0 / 9 / 0 8
34 storage- 1 9 9 / 0 1 / 5 / 3 7
35 apimethod- 1 8 8 / 0 0 / 7 / 1 5
36 dashboard- 5 8 1 / 7 0 / 8 / 0 8
37 notification- 1 8 8 / 0 0 / 7 / 1 7
38 record- 2 8 6 / 2 0 / 8 / 0 4
39 runtime- 1 8 8 / 0 0 / 8 / 0 6
40 scim- 1 8 0 / 8 2 / 5 / 1 4
41 aggregation- 1 7 7 / 0 1 / 6 / 0 6
42 automation- 2 7 0 / 7 0 / 5 / 2 4
43 cache- 1 7 0 / 7 1 / 5 / 1 4
44 tenant- 2 7 0 / 7 0 / 7 / 0 3
45 authoring- 1 6 6 / 0 0 / 6 / 0 5
46 cli- 1 6 0 / 6 0 / 5 / 1 5
47 client- 4 6 4 / 2 0 / 6 / 0 4
48 evaluated- 1 6 0 / 6 2 / 3 / 1 3
49 identity- 1 6 0 / 6 0 / 6 / 0 6
50 spec- 1 6 6 / 0 0 / 6 / 0 6
51 advanced- 1 5 0 / 5 1 / 4 / 0 5
52 cloud- 1 5 0 / 5 2 / 3 / 0 5
53 import- 1 5 5 / 0 0 / 4 / 1 5
54 startup- 1 5 0 / 5 0 / 5 / 0 5
55 sys- 1 5 0 / 5 0 / 3 / 2 5
56 tool- 1 5 5 / 0 0 / 4 / 1 3
57 address- 1 4 0 / 4 0 / 4 / 0 4
58 declarative- 1 4 4 / 0 0 / 4 / 0 3
59 packages- 1 4 0 / 4 0 / 4 / 0 3
60 platform- 1 4 0 / 4 0 / 4 / 0 3
61 session- 2 4 0 / 4 0 / 4 / 0 3
62 sort- 1 4 4 / 0 0 / 4 / 0 2
63 strategy- 1 4 0 / 4 1 / 2 / 1 3
64 admin- 2 3 0 / 3 0 / 3 / 0 3
65 ai- 1 3 0 / 3 0 / 3 / 0 2
66 assembled- 1 3 0 / 3 0 / 3 / 0 3
67 auth- 1 3 3 / 0 0 / 3 / 0 2
68 change- 2 3 0 / 3 0 / 3 / 0 2
69 device- 1 3 0 / 3 0 / 3 / 0 2
70 epoch- 1 3 0 / 3 0 / 3 / 0 2
71 incident- 2 3 0 / 3 0 / 3 / 0 2
72 logging- 1 3 0 / 3 0 / 3 / 0 2
73 memory- 1 3 0 / 3 0 / 3 / 0 2
74 rls- 2 3 0 / 3 0 / 3 / 0 2
75 send- 1 3 0 / 3 1 / 2 / 0 2
76 standard- 2 3 0 / 3 0 / 3 / 0 2
77 training- 2 3 0 / 3 0 / 3 / 0 2
78 turso- 1 3 0 / 3 0 / 3 / 0 3
79 websocket- 1 3 0 / 3 0 / 3 / 0 2
80 autonumber- 1 2 0 / 2 0 / 2 / 0 2
81 batch- 2 2 2 / 0 0 / 2 / 0 2
82 cbp- 1 2 0 / 2 1 / 1 / 0 1
83 schedule- 1 2 0 / 2 1 / 1 / 0 2
84 structured- 1 2 0 / 2 0 / 2 / 0 2
85 time- 1 2 0 / 2 0 / 2 / 0 2
86 workflow- 1 2 2 / 0 0 / 2 / 0 2
87 approval- 1 1 1 / 0 0 / 1 / 0 1
88 audience- 1 1 0 / 1 0 / 1 / 0 1
89 branded- 1 1 0 / 1 0 / 1 / 0 1
90 cluster- 1 1 0 / 1 0 / 1 / 0 1
91 connector- 2 1 1 / 0 0 / 1 / 0 1
92 enhanced- 1 1 1 / 0 0 / 1 / 0 1
93 esignature- 1 1 0 / 1 0 / 1 / 0 1
94 event- 1 1 0 / 1 0 / 1 / 0 1
95 job- 1 1 1 / 0 0 / 1 / 0 1
96 position- 1 1 1 / 0 0 / 1 / 0 1
97 ups- 1 1 1 / 0 0 / 1 / 0 1
zero-site families: cel- (2), cube- (1), execution- (1), inline- (1), list- (1), manifest- (2), observability- (1), page- (1), saved- (1), screen- (1), translation- (1), wait- (1) 14 0 0
total 266 1016 417 / 599 61 / 901 / 54 460

Stage 1 = the engine- family

It is the busiest family: 67 sites in 5 entries, 22 distinct ids. It also fits a reviewable stage, at 112 changed lines in entry files (+64 / −48) against the ≈400 budget, with generated registry.ts excluded. The five entries are the data engine's query and write option refusals:

entry sites (replacement / reason / acceptanceCriteria)
17.engine-dotted-projection-refused 17 (3 / 14 / 0)
17.engine-find-formula-filter-refused 17 (4 / 13 / 0)
17.engine-find-formula-order-by-refused 14 (2 / 12 / 0)
17.engine-update-upsert-retired 2 (0 / 1 / 1)
18.engine-dotted-filter-refused 17 (2 / 15 / 0)

Every citation read, and what the text now says

I read each cited issue or PR myself: the body, and the comments where a ruling or a measurement lives. The ids are in code spans so that this body does not post 22 cross-references. There are no unresolved sites: every citation's decision was established from what it says.

cited what it decided (read) how the text now carries it
#3821 The SQL driver's find must not turn an unknown column into "no rows": it retries without the projection or ORDER BY. That is the unknown-column recovery ladder. The GitHub title names the sharing-rule recipient picker, which is where the defect surfaced. The driver's sql-driver-unknown-column-recovery.test.ts header ties the two together. "the driver's unknown-column recovery ladder — there so an unknown column never reads as "no rows"", and later "the recovery ladder" / "the unknown-column backstop"
#4226 At the REST list route, a sort naming a nonexistent field answers 400 INVALID_SORT instead of being dropped. "The SORT axis is closed at the REST ingress for an unknown field, …"
#4256 A dotted sort path is refused at the ingress, rather than silently unsorted. "… a dotted path …" / "SORT refuses it" / "the SORT axis prescribes when it refuses the dotted spelling"
#5918 Maintainer ruling, 2026-08-07: the analytics ad-hoc path refuses a relation-traversing dotted measure loudly, with a 400 naming the caller's spelling. It had silently aggregated a base-table column. "the line analytics already takes for a relation-traversing dotted measure, refused with a 400 naming the caller's spelling rather than computed against the wrong column"
#6674 A formula field declared in searchableFields is refused loudly, never admitted as search coverage. "SEARCH refuses it by name" / "the SEARCH axis prescribes when it refuses a formula search field"
#6924 The dotted-sort hint prescribes a STORED field, not a "formula or rollup" field. A formula has no column. "the ingress sort hint's stored-field prescription" / "the sort axis when it refuses a dotted sort"
#6994 A non-dotted orderBy on a formula field is refused at the ingress (400 INVALID_SORT). "… and a formula field alike" / "at the REST ingress"
#7095 Maintainer ruling, 2026-08-10: the engine refuses a formula ORDER BY it cannot apply, at the public boundary. The internal tolerance survives only on a measured caller, and none was found. Its PR registered the change as a semantic entry (disposition registered) although no stored row is rewritten. "Ruled by the maintainer on 2026-08-10: …" / "The sweep of every in-tree orderBy …" / "the formula-sort refusal (engine-find-formula-order-by-refused)" / "Registered on the ruling inherited from the SORT axis — its engine refusal was registered in this ledger although no stored row needs rewriting …"
#7532 The REST ingress refuses a dotted projection with 400 INVALID_FIELD instead of widening the response to every field. Resolving the path was explicitly not authorised. "The REST ingress closed the PROJECTION axis' dotted leg first, refusing a dotted entry instead of widening the response to every field"
#7534 An unknown field inside where / $filter / a filter AST is refused with 400 INVALID_FIELD. "cleared the unknown-name check (which refuses a key naming no field of the object)"
#7537 A nested expand projection that omitted id was a silent no-op. The engine now keeps the join key in the sub-read and strips it from the output. "the same silent no-op a nested projection omitting the related id produced before the engine began keeping that join key itself"
#7588 The PR that implemented #7532. folded into the #7532 sentence
#7589 Maintainer ruling, 2026-08-12 (Option B): the engine refuses a dotted projection at its own head-only filter. The unknown-plain-column tolerance is kept, and a driver-side carve-out waits for measured need. The flow get_record chain was measured end to end. "Ruled by the maintainer on 2026-08-12: …" / "that caller set was measured, not assumed:" / "PROJECTION refuses it at both doors"
#7601 A measurement found that no populate step exists. The spec and docs stop prescribing a dotted fields path. "a measurement found that NO populate step exists"
#7617 The PR that made the spec and docs stop prescribing the dotted path. "once the spec and docs stopped prescribing a dotted fields path"
#7867 A by-id update whose id names no row throws RECORD_NOT_FOUND: the engine's not-found gate. "the engine's not-found gate — a by-id update whose id names no row throws RECORD_NOT_FOUND"
#8057 options.upsert is removed (ADR-0049). One prescription constant is quoted by the engine gate and by both schemas. "the engine gate and both schemas quote one removal prescription"
#8296 The FILTER axis gets its formula verdict at both doors: 400 INVALID_FIELD, judged by the spec's own virtual-field predicate. "Both doors now refuse it with 400 INVALID_FIELD" / "The formula verdict deliberately skipped dotted keys" / "the one-source move the formula verdict made with isVirtualSearchField"
#8369 The PR that implemented #8296. folded into the #8296 sentence
#8370 Triage, 2026-08-13: register the FILTER refusal as a semantic entry, inheriting the SORT-axis answer. "re-affirmed for this axis at triage on 2026-08-13"
#8371 Maintainer ruling (delegated), 2026-08-15: refuse a dotted filter key whose head is a relation, a formula or a plain scalar, at both doors. A structured/JSON head is left unjudged. The ruling was preceded by a three-driver measurement. "Measured across all THREE drivers before ruling" / "per the maintainer's ruling"
#8790 Maintainer ruling, 2026-08-15: an unresolvable WHERE column is refused by both the list and the count half, with INVALID_FILTER / 400. It has its own entry. "a divergence with its own entry, driver-sql-unresolvable-where-column-refused, that now refuses it on both"

The only call-shaped token the rewrite touches is find(): one is removed and one is added, so textual call-spelling ratchets that read registry.ts count the same.

Pin — packages/cli/test/migrate-meta-engine-guidance.test.ts

The test spawns the real CLI (os migrate meta --from 16 --to 18) over a stack authoring the shapes the family is about: a lookup and a virtual formula field. It locates each engine-* block verbatim in the printed output, then asserts that the printed block carries no # plus 4 or 5 digits. Three things keep it from passing vacuously:

  • The family is derived from the registry by id prefix, with the five rewritten ids as its floor.
  • Presence in stdout is asserted before cleanliness.
  • The detector is exercised on both sides first: it is lit on 4 and 5 digits, and dark on 3 digits, 6 digits and ADR-0112.

The file follows the existing migrate-meta-default-range.test.ts pattern: queue tier by name (not .e2e, which is nightly-only), integration project by behaviour.

Ablation — the pin can fail

The ablation ran from committed state, HEAD 1fa8251067, with scripts/ablation-replace.mjs in wrap mode and scripts/ablation-dist-preflight.mjs gating each leg.

  • Attempts 1 and 2 are VOID and are not readings.
    • In attempt 1, the spec build never got the verify lock (queue-timeout, exit 99). The preflight reported the marker ABSENT from dist/, so the green pin run after it measured the pre-mutation build.
    • Attempt 2 mutated the entry file, and the build ran. But the published bundle is built from the generated registry.ts, not from the entry files, so the preflight again reported ABSENT and the pin was not run.
  • Attempt 3, the reading:
    • Mutation. The mutation went into registry.ts, the same line the generator emits for the entry: anchor quote one removal prescription becomes quote the #8057 removal prescription. Anchor went 1 → 0 and marker 0 → 1, and the blob changed from b956ae88 to cef8c6e2.
    • Mutate leg. The spec build ran (command-exit 0). The preflight found the marker in 4 built files. The pin went red, 1 failed | 2 passed: engine-update-upsert-retired: the printed guidance cites a tracker id: expected '#8057' to be undefined.
    • Restore. The tool proved the restore: blob b956ae88 == HEAD, and git diff HEAD is empty.
    • Restore leg. The spec build was rerun (command-exit 0). --absent found the marker in none of 222 built files, and the tree was clean. The pin went green, 3 passed.

Verification (all at HEAD 1fa8251067)

  • Pin: pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/migrate-meta-engine-guidance.test.ts, with Tests 3 passed (3). That is the restore-leg run, after the spec rebuild.
  • Spec migrations: pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/migrations, with Test Files 3 passed (3) and Tests 149 passed (149).
  • Typecheck:
    • pnpm --filter @objectstack/spec typecheck exits 0.
    • pnpm --filter @objectstack/cli typecheck exits 0. The test layer holds its recorded 28 errors in 3 files, unchanged. tsc --listFiles -p packages/cli/tsconfig.test.json puts the new file in the program with 0 errors in it.
  • CLI tiers: test/vitest-tiers-partition.test.ts passes, 22 tests. The rest of the CLI unit / integration suites are declared to CI: the diff touches no CLI source, and adds only this one integration-tier file.
  • Build: the build closure is pnpm exec turbo run build --filter="@objectstack/cli^..." --concurrency=2, with 55/55 tasks.
  • Gate families: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derives 88 families from this change set. --ran over the recorded exit codes reads 88 derived, 87 run, 1 NOT MEASURED, 0 unrun.
    • The 87 exit 0. They include check:migration-registry, check:spec-changes, check:upgrade-guide, check:generated (15 artifacts up to date), check:doc-authoring, check:issue-citations, check:nul-bytes, check:adr-0087-registration and check:changeset-no-major.
    • NOT MEASURED: check:dual-build-cjs-loads, PREREQUISITE NOT MET (exit 3). It reads every package's dist/, and this worktree built only the CLI's dependency closure. CI builds the whole repo.
  • Lint (a proven narrowing, not the repo-wide run, which is CI's): eslint --no-inline-config --format json over the 7 changed .ts files reports 7 files, 0 errors and 0 warnings.
    • The population is read from eslint.config.mjs, which lints **/*.{ts,tsx,mts,cts,js,…} minus NEVER_LINTED, and all 7 files are in it.
    • Invariance: the config never enables type-aware linting (its own header says so: no parserOptions.project, no typed rules). A text-only edit therefore cannot move the verdict on any file it does not touch.
  • Mergeability: the branch is 7 commits behind origin/main 89f87f2344. git merge-tree --write-tree HEAD origin/main exits 0, and registry.ts auto-merges. Main's one new semantic entry is not an engine-* entry.

Acceptance notes

  • surface is printed too, and it is outside this card's three fields. The block header ⚠ [protocol N] surface → … shows the surface text to the author. By the same instrument, 9 tracker-id sites sit in the surface of 6 entries: authoring-schemas-strict-unknown-keys (1), dataset-measure-aggregate-field-type-refused (2), evaluated-expression-slots-source-required (2), flow-edge-condition-evaluated-slot-source-required (2), plugin-manifest-contributes-routes-retired (1) and ui-react-list-view-binding-aliases-retired (1). None is in the engine- family, and the pin already holds the whole printed block, surface included. This is noted for the card's later stages, not filed.
  • The pin selects its family by id prefix. A later stage can widen the same file to its own family, rather than add a second spawn.
  • Generated projections regenerated beyond the claim's listed surface: packages/spec/spec-changes.json and docs/protocol-upgrade-guide.md carry the same entry text, and their --check gates are red until regenerated. Both are generator output only.

Line budget

Entry files: 112 changed lines (+64 / −48) across 5 files, against ≈400. The whole diff is 471 lines (+346 / −125) in 10 files. Of the rest, registry.ts is 114, the two projections are 56, the pin is 169 and the changeset is 20.


Generated by Claude Code

…on in words, not tracker numbers

The five `engine-*` ADR-0087 semantic entries carried 67 tracker-id sites in
the three fields `os migrate meta` prints to the author (replacement, why,
verify). Each site now says what the cited ruling, measurement or fix
decided, and carries no number; ADR ids stay. Entry ids, surfaces, from/to
and matching logic are untouched. registry.ts, spec-changes.json and the
protocol upgrade guide are regenerated from the entries.

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-Authored-By: Claude <noreply@anthropic.com>
…; changeset

Spawns `os migrate meta` over a stack authoring a lookup and a virtual
formula field, locates each `engine-*` semantic block verbatim in the
printed output, and holds it free of a tracker id. The family is derived
from the registry by id prefix with the five rewritten ids as its floor,
and the detector is exercised lit and dark before it is trusted.

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/spec-changes.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via account.name (literal, a string literal in reason; a string literal in semantic))
  • content/docs/permissions/permission-metadata.mdx (via account.name (literal, a string literal in reason; a string literal in semantic))
  • content/docs/permissions/permissions-matrix.mdx (via account.name (literal, a string literal in reason; a string literal in semantic))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/spec-changes.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3f86dc52f22668dd92de00f604148e04d3d048da → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6b42f2c11ecfd3596e71551696f4413042990949 — the merge of head 1fa8251067e914c758ab533cd8c1fbc96a6556fb into base 3f86dc52f22668dd92de00f604148e04d3d048da, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6b42f2c11ecfd3596e71551696f4413042990949 && git checkout 6b42f2c11ecfd3596e71551696f4413042990949
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3f86dc52f22668dd92de00f604148e04d3d048da 1fa8251067e914c758ab533cd8c1fbc96a6556fb && git checkout -B drift-repro 3f86dc52f22668dd92de00f604148e04d3d048da && git merge --no-ff 1fa8251067e914c758ab533cd8c1fbc96a6556fb

node scripts/docs-audit/affected-docs.mjs --json 3f86dc52f22668dd92de00f604148e04d3d048da

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3f86dc52f22668dd92de00f604148e04d3d048da → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 91/91 CONTRACT_REVIEW_TIER
Head-sha: 1fa8251067e914c758ab533cd8c1fbc96a6556fb

① Derived judgments

  1. Faithfulness. PR re-read: head unmoved at 1fa8251067, open, draft, merge-base 443b2f4fdc. All 22 removed ids fetched by single REST reads (issue body + all comments): 3821, 4226, 4256, 5918, 6674, 6924, 6994, 7095, 7532, 7534, 7537, 7588, 7589, 7601, 7617, 7867, 8057, 8296, 8369, 8370, 8371, 8790. Every replacement / acceptanceCriteria site and every reason site was checked (no sampling needed). No unfaithful sentence found. Evidence for the load-bearing claims: "Ruled by the maintainer on 2026-08-10" = engine.find() still drops a formula ORDER BY silently — decide whether the engine refuses or keeps its internal-caller tolerance #7095 comment 5236143229 ("Maintainer ruling (2026-08-10 …): refuse at the public boundary … 4xx with guidance prose … never a silent drop"); "Ruled by the maintainer on 2026-08-12" = finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 comment 5266074163 ("Provenance: maintainer, 2026-08-12 … Option B … unknown-plain tolerance stays … C on measured need"); "per the maintainer's ruling" / three-driver measurement = [finding] The FILTER axis has no DOTTED-path verdict — where: { project_id.name: 'x' } rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371 comments 5299941258 + 5300373151 (relation/formula/scalar heads refused at both doors, JSON head unjudged, "no new mechanism, no new error class"); "re-affirmed … at triage on 2026-08-13" = The FILTER-axis formula refusal (#8296) shipped without the ADR-0087 semantic entry its SORT-axis twin (#7095) carries — the upgrade guide will not mention it #8370 comment 5279029729 (sibling ruling inherited, "register it anyway"); "the ledger is the one channel…" = The FILTER-axis formula refusal (#8296) shipped without the ADR-0087 semantic entry its SORT-axis twin (#7095) carries — the upgrade guide will not mention it #8370 body ("the ledger IS the notification") and The FILTER axis has no unmaterializable verdict: a where on a virtual formula field returns 0 rows silently, while sort and search refuse the same field with a 400 #8296 ruling 5278734062; analytics 400 naming the caller's spelling = analytics 自动推断路径:measures 上的关系穿越点号 member 仍被剥成基表列 —— owner.region_count_distinct 静默聚合基表 region(#5739 裁决未覆盖的第四个铸造点) #5918 ruling 5211845846 (option 3); "unknown-column recovery ladder — there so an unknown column never reads as no rows" = fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821 fix scope C and finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 body (ladder's documented trade); "engine's not-found gate … RECORD_NOT_FOUND" = Action-body writes have no not-found gate: ctx.api.object().update() against a nonexistent id answers 400 (or worse) instead of 404, while the protocol and callData paths both gate correctly #7867 report (gate in ObjectQL.update()/delete() by-id branch, thrown before hooks); "quote one removal prescription" = [finding] options.upsert is accepted by engine.update()'s option surface and never read — a declared-but-unenforced key (ADR-0049) #8057 report (both schemas + engine share ENGINE_UPDATE_UPSERT_REMOVED); "NO populate step … last place in the repo" = finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 comment 5251335475 relaying spec/docs still prescribe a dotted fields path that no driver implements and #7532 now refuses #7601; join-key add-and-strip = expand is a silent no-op when the nested fields omits id — the exact spelling two retirement messages prescribe #7537 report; "refusing a dotted entry instead of widening" and its expand/denormalise remedy = PR fix(metadata-protocol): refuse a dotted projection instead of widening the response to every field (#7532) #7588 body; find/count divergence with its own entry refusing both = driver-sql: one unresolvable WHERE column, two answers — find() silently returns [] while count() throws a raw dialect error with no ADR-0112 envelope #8790 ruling 5302931807 and driver-sql-unresolvable-where-column-refused present in head registry (:7844).
  2. Nothing else moved. Mechanical block compare of the five files base vs head: only replacement / reason / acceptanceCriteria blocks differ; id and surface byte-identical; semantic entries carry no from/to or matcher. find() token count unchanged per file (1/1, 0/0, 1/1, 0/0, 1/1). #\d{4,5}\b at head in all five files: 0 hits in any position.
  3. Generated projections. Generators: packages/spec/scripts/build-migration-registry.ts (gen:/check:migration-registry --self-test --check), build-spec-changes.ts, build-upgrade-guide.ts. registry.ts: 11 hunks, all inside the five engine ids (step17 :2520-2743, step18 :8261-8339); spec-changes.json and docs/protocol-upgrade-guide.md each 14/14 lines, only the same entries' replacement/rationale text. All three checks run in the lint job ("Lint & Repo Gates", lint.yml :384 armed by select-gate-families for packages/spec/src/migrations/*, :5501, :5504): completed success on head. Final poll: 35 check-runs, 32 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball opt-in), 0 failed, 0 in progress (Test Core 1-6 and Type Check · workspace all finished green after the first poll showed 6 in progress).
  4. Pin. Spawns tsx bin/run-dev.js migrate meta --from 16 --to 18 once in a mkdtemp cwd with childEnv({NO_COLOR:'1'}); FAMILY derived from MIGRATIONS_BY_MAJOR by engine- prefix with the five ids as a floor; each block rebuilt exactly as meta.ts :523-525 prints it, asserted present in stdout, then tracker-free. chain.ts :103 emits every semantic entry unconditionally, so presence does not depend on the fixture. Tier: no .e2e/.live name so queue population; isIntegration fires (imports node:child_process, run-dev.js, .bin/tsx) so integration project; cli test is vitest run over both projects, run by Test Core on pull_request and merge_group. Red-on-return is real: presence-then-cleanliness makes the block the printed text, and the regex is exercised lit/dark. No network, no clock, single spawn, order-independent includes.
  5. Changeset .changeset/20233-engine-migration-guidance-tracker-free.md: every sentence true against the diff; @objectstack/spec patch is right (exported registry prose only, no accept set or schema change); Clause-②: no is its own line, matching CLAUSE2_KEY_LINE and the closed value set in scripts/pm/clause2-line.mjs. Check Changeset, Part-of guard, both claim guards: green.

② Semver level

@objectstack/spec patch, Clause-②: no. Only author-shown prose printed by os migrate meta moves; no id/surface/conversion/matcher change, no ADR-0087 disposition (check-adr-0087-registration in the green lint job), check-changeset-no-major satisfied.

③ Boundary flags

Implemented-by: claude/issue-20233-migrate-meta-tracker-free-stage-1
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 19:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 2aa25ef Sep 27, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20233-migrate-meta-tracker-free-stage-1 branch September 27, 2026 19:31
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…nd the 25 missing entries (objectstack-ai#20201) (objectstack-ai#20255)

Fixes objectstack-ai#20201
Clause-②: no

## What

Ruling B on objectstack-ai#17152 (director `5615360777`, restated `5634031140`, on the
maintainer's objectstack-ai#15954 authority `5559778263`): every retirement family
carries ONE ADR-0087 D3 (`semantic`) entry, even when a lossless D2
conversion repairs its data; D2 carries the mechanical repair only. This
PR takes the major-18 family census the card asks for, adds the 25 D3
entries it found missing, corrects the prose that justified their
absence, and pins the census in the registry's own test.

- **25 new D3 entries** under
`packages/spec/src/migrations/entries/semantic/18.*.ts`, one per
D2-backed family that had none. Each names its family and its D2
conversion id, says what D2 already repairs, and says what judgment the
consumer still owes (the `reason`), with an `acceptanceCriteria` the
consumer can check. None is a placeholder: every one states a residue
specific to its family (a unit only the author knows, a belief the
platform never honoured, a shape the conversion deliberately leaves
alone, code the chain cannot reach).
- **Prose corrected** at the sites of `5854110917` and `5854456343`,
plus step 18's rationale and step 17's docblock fact (list below).
- **Census pin** in the existing
`packages/spec/src/migrations/migrations.test.ts` (registry integrity).
No new check script.
- `MIGRATIONS_BY_MAJOR[18].semantic` 186 → 211 entries at the census
base; after merging `main` (four sibling D3 entries landed meanwhile,
none with a D2 conversion) the generated region holds 215.

## The census (the card's main deliverable)

**Tree.** `objectstack-ai/objectstack` at `3cb84d084` (this branch's
fork point; it already contains objectstack-ai#20227's view-item retirement). Major-18
population there: **185** `retired-keys`, **146** `retired-defs`,
**186** `semantic` entry files, and **36** D2 conversions graduated into
step 18. (The card measured 181 / 130 / 175 at `d7c024133e`.)

**Grouping rule (ruling B's unit, held constant).** Where a D2
conversion exists, the conversion is the family: every retired key or
def it repairs belongs to it, and a D3 entry may cover more than one
conversion only where one judgment covers them (the pre-existing
`element-filter-and-form-node-refused` covers `element-filter-removed`
and `element-form-removed`). Every new entry here covers exactly one
conversion. Where no conversion exists, the records are grouped by the
D3 entry that names them.

**Method.**
1. Mechanical pass (scratch scripts, not committed): for each of the 36
conversions, the major-18 `semantic/` entry files that name its id as a
whole id (comment or field); for each retired key, the conversion its
own comment names, else the major-18 entries naming it as `cat/Def:key`,
`Def.path` / `Def:path`, or the def name plus the leaf key; for each
retired def, the entries naming the def.
2. Reading pass, where a string match cannot decide: (a) ownership: an
entry that names a conversion only in passing is not that family's entry
(this is how `metric-filters-removed` was classed missing although
`analytics-authorable-unknown-keys-refused` names it); (b) 14 records
matched by more than one entry, each placed by its own comment (for
example `kernel/PluginStartupResult:plugin`, which goes to
`startup-orchestrator-retired`); (c) 9 records whose comment names no
conversion but which belong to a D2 family
(`integration/DeclarativeConnectorEntry:connectionTimeoutMs` and
`:errorMapping`, the three error-mapping defs, the four responsive-shape
defs), plus `integration/Connector:connectionTimeoutMs`, whose comment
names two conversion ids and belongs to
`connector-connection-timeout-ms-removed` (it names the permission
conversion only as a comparison; round 1's Table 1 placed it wrongly,
corrected in patch round 1); (d) 5 theme sub-block defs, named by the
theme family's entry as its sub-blocks.

**Control.** The pairing sees a family that has its entry: 11 of the 36
conversions pair with a pre-existing entry, among them
`cube-join-sql-and-relationship-removed` with
`cube-join-sql-and-relationship-retired` (which the pin's own control
test also asserts), and the whole-id matcher refuses a prefix
(`record-chatter-position-vocabulary` is a prefix of its entry's own id
and matches only the entry's real citation). Evaluated at the base with
the pin's logic: **24** conversions named by no major-18 entry; the
reading pass adds `metric-filters-removed` for **25**. Evaluated at this
head: **0**.

**Result.** 331 records (185 keys + 146 defs) plus 36 conversions:

- **36 D2-backed families** covering 58 records: 11 had their D3 entry,
**25 had none**. Table 1.
- **273 D2-less records** in 68 groups: every one is named by an
existing D3 entry. Table 2. None missing, as expected: before ruling B,
a retirement with no conversion needed a D3 entry anyway.

The card's grep for 「lossless」 found the `tenancy.organizationField`
site and step 17. The census finds 25 major-18 families, most of them
with no 「lossless」 wording at all.

### Table 1 — D2-backed families (step 18 `conversionIds`, in order)

| # | D2 conversion (the family) | registered records | D3 entry at base
| D3 entry after |
|---|---|---|---|---|
| 1 | `field-malformed-scale-precision-removed` | none (value or
strict-key retirement, not in the two tables) |
`field-scale-precision-integer-refused` | unchanged |
| 2 | `record-chatter-position-vocabulary` | none (value or strict-key
retirement, not in the two tables) |
`record-chatter-position-vocabulary-converged` | unchanged |
| 3 | `element-input-target-variable-removed` |
`ui/ElementRecordPickerProps:targetVariable`,
`ui/ElementTextInputProps:targetVariable` | MISSING |
`element-input-target-variable-retired` (new) |
| 4 | `element-filter-removed` | `ui/ElementFilterProps:aria`,
`ui/ElementFilterProps:fields`, `ui/ElementFilterProps:layout`,
`ui/ElementFilterProps:object`, `ui/ElementFilterProps:showSearch`,
`ui/ElementFilterProps:targetVariable` |
`element-filter-and-form-node-refused` | unchanged |
| 5 | `element-form-removed` | `ui/ElementFormProps:aria`,
`ui/ElementFormProps:fields`, `ui/ElementFormProps:mode`,
`ui/ElementFormProps:object`, `ui/ElementFormProps:onSubmit`,
`ui/ElementFormProps:submitLabel` |
`element-filter-and-form-node-refused` | unchanged |
| 6 | `field-column-lists-canonicalized` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`field-inline-and-related-list-columns-closed` (new) |
| 7 | `metric-filters-removed` | `data/Metric:filters` | MISSING (named
only in passing by `analytics-authorable-unknown-keys-refused`) |
`cube-metric-filters-retired` (new) |
| 8 | `cube-sub-day-granularities-removed` | none (value or strict-key
retirement, not in the two tables) |
`time-update-interval-sub-day-retired` | unchanged |
| 9 | `cube-join-sql-and-relationship-removed` |
`data/CubeJoin:relationship`, `data/CubeJoin:sql` |
`cube-join-sql-and-relationship-retired` | unchanged |
| 10 | `record-highlights-field-icon-removed` |
`ui/RecordHighlightsField:icon` | MISSING |
`record-highlights-field-icon-retired` (new) |
| 11 | `mapping-lookup-params-removed` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`mapping-lookup-params-retired` (new) |
| 12 | `translation-component-submit-label-removed` | none (value or
strict-key retirement, not in the two tables) | MISSING |
`translation-component-submit-label-retired` (new) |
| 13 | `page-component-responsive-removed` |
`ui/PageComponent:responsive`, `ui/BreakpointColumnMap`,
`ui/BreakpointName`, `ui/BreakpointOrderMap`, `ui/ResponsiveConfig` |
MISSING | `page-component-responsive-retired` (new) |
| 14 | `object-grid-default-sort-removed` |
`ui/ObjectGridProps:defaultSort` | MISSING |
`object-grid-default-sort-retired` (new) |
| 15 | `object-kanban-quick-add-removed` |
`ui/ObjectKanbanProps:quickAdd` | MISSING |
`object-kanban-quick-add-retired` (new) |
| 16 | `permission-allow-restore-purge-removed` |
`security/EffectiveObjectPermission:allowPurge`,
`security/EffectiveObjectPermission:allowRestore`,
`security/ObjectPermission:allowPurge`,
`security/ObjectPermission:allowRestore` | MISSING |
`permission-restore-purge-bits-retired` (new) |
| 17 | `form-view-option-default-removed` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`form-view-option-default-retired` (new) |
| 18 | `field-reference-to-alias` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`field-reference-to-spelling-retired` (new) |
| 19 | `connector-error-mapping-removed` |
`integration/Connector:errorMapping`,
`integration/DeclarativeConnectorEntry:errorMapping`,
`integration/ConnectorErrorCategory`, `integration/ErrorMappingConfig`,
`integration/ErrorMappingRule` | MISSING |
`connector-error-mapping-retired` (new) |
| 20 | `connector-connection-timeout-ms-removed` |
`integration/Connector:connectionTimeoutMs`,
`integration/DeclarativeConnectorEntry:connectionTimeoutMs` |
`connector-provider-context-connection-timeout-ms-retired` | unchanged |
| 21 | `hook-timeout-to-timeout-ms` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`hook-timeout-unit-in-key` (new) |
| 22 | `job-timeout-to-timeout-ms` | `system/Job:timeout` | MISSING |
`job-timeout-unit-in-key` (new) |
| 23 | `api-endpoint-cache-ttl-to-cache-ttl-seconds` |
`api/ApiEndpoint:cacheTtl` | MISSING |
`api-endpoint-cache-ttl-unit-in-key` (new) |
| 24 | `dashboard-refresh-interval-to-refresh-interval-seconds` |
`ui/Dashboard:refreshInterval` | MISSING |
`dashboard-refresh-interval-unit-in-key` (new) |
| 25 | `connector-health-and-trigger-durations-unit-in-key` |
`integration/CircuitBreakerConfig:monitoringWindow`,
`integration/ConnectorTrigger:interval` | MISSING |
`connector-resilience-durations-unit-in-key` (new) |
| 26 | `memory-persistence-auto-save-interval-to-ms` |
`data/AutoPersistenceConfig:autoSaveInterval`,
`data/FilePersistenceConfig:autoSaveInterval` | MISSING |
`memory-persistence-auto-save-interval-unit-in-key` (new) |
| 27 | `turso-config-timeout-to-timeout-ms` | `data/TursoConfig:timeout`
| MISSING | `turso-config-timeout-unit-in-key` (new) |
| 28 | `view-page-mount-removed` | `ui/ListView:pageName`,
`ui/ObjectListView:pageName` | MISSING | `list-view-page-mount-retired`
(new) |
| 29 | `list-view-sort-string-clause-to-array` | none (value or
strict-key retirement, not in the two tables) | MISSING |
`list-view-sort-string-clause-retired` (new) |
| 30 | `page-assigned-profiles-removed` | `ui/Page:assignedProfiles` |
`page-assigned-profiles-audience-to-permission-set` | unchanged |
| 31 | `chart-config-aria-removed` | `ui/ChartConfig:aria`,
`ui/ReportChart:aria` | MISSING | `chart-config-aria-retired` (new) |
| 32 | `dashboard-widget-chart-config-structure-removed` |
`ui/DashboardWidgetChartConfig:series`,
`ui/DashboardWidgetChartConfig:type`,
`ui/DashboardWidgetChartConfig:xAxis`,
`ui/DashboardWidgetChartConfig:yAxis` |
`dashboard-widget-chart-config-structure-refused` | unchanged |
| 33 | `translation-per-app-settings-removed` | none (value or
strict-key retirement, not in the two tables) |
`translation-per-app-settings-platform-only` | unchanged |
| 34 | `object-tenancy-organization-field-removed` |
`data/TenancyConfig:organizationField` | MISSING |
`object-tenancy-organization-field-retired` (new) |
| 35 | `page-component-filter-record-to-rule-array` | none (value or
strict-key retirement, not in the two tables) |
`element-data-source-and-object-block-filter-rule-array`,
`object-grid-default-filters-rule-array` | unchanged |
| 36 | `view-item-owner-hidden-removed` | `ui/ViewItemWire:hidden`,
`ui/ViewItemWire:owner`, `ui/ViewItem:hidden`, `ui/ViewItem:owner` |
MISSING | `view-item-owner-hidden-retired` (new) |

### Table 2 — D2-less records, grouped by the existing D3 entry that
names them

| D3 entry (existing) | records it names |
|---|---|
| `advanced-plugin-lifecycle-config-retired` |
`kernel/AdvancedPluginLifecycleConfig`, `kernel/GracefulDegradation`,
`kernel/PluginUpdateStrategy` |
| `ai-conversation-analytics-duration-unit-in-key` |
`ai/ConversationAnalytics:duration` |
| `api-error-retry-after-unit-in-key` |
`api/EnhancedApiError:retryAfter` |
| `api-runtime-config-durations-unit-in-key` |
`api/DataLoaderConfig:cacheTtl`, `api/RouteDefinition:timeout` |
| `automation-flow-list-route-retired` | `api/FlowSummary`,
`api/ListFlowsRequest`, `api/ListFlowsResponse` |
| `automation-runs-cursor-retired` | `api/ListRunsRequest:cursor` |
| `branded-identifier-schemas-retired` | `shared/AppName`,
`shared/FieldName`, `shared/FlowName`, `shared/ObjectName`,
`shared/RoleName`, `shared/ViewName` |
| `change-management-duration-keys-retired` |
`system/ChangeImpact:downtime.durationMinutes`,
`system/ChangeRequest:implementation.steps.estimatedMinutes`,
`system/RollbackPlan:steps.estimatedMinutes` |
| `change-management-family-retired` | `system/ChangeImpact`,
`system/ChangePriority`, `system/ChangeRequest`, `system/ChangeStatus`,
`system/ChangeType`, `system/RollbackPlan` |
| `cli-command-contribution-retired` | `kernel/CLICommandContribution` |
| `cloud-subpath-retired` | 62 records, all `cloud/` defs |
| `data-file-value-duration-unit-in-key` | `data/FileValue:duration` |
| `data-nosql-query-options-timeout-unit-in-key` |
`data/NoSQLQueryOptions:timeout` |
| `device-request-response-interval-unit-in-key` |
`api/DeviceRequestResponse:interval` |
| `driver-options-timeout-to-timeout-ms` | `data/DriverOptions:timeout`
|
| `epoch-instant-keys-renamed` | `api/SimplePresenceState:lastSeen`,
`api/WebSocketEvent:timestamp`, `kernel/HealthStatus:timestamp`,
`kernel/KernelContext:startTime`,
`kernel/TenantRuntimeContext:startTime` |
| `esignature-config-deadline-keys-retired` |
`data/ESignatureConfig:expirationDays`,
`data/ESignatureConfig:reminderDays` |
| `event-name-schema-retired` | `shared/EventName` |
| `export-job-family-retired` | 13 records, all `api/`, `automation/`
defs |
| `hot-reload-inert-state-strategies-retired` |
`kernel/DistributedStateConfig` |
| `hot-reload-watch-placeholder-retired` |
`kernel/HotReloadConfig:watchPatterns` |
| `identity-api-key-schema-retired` | `identity/ApiKey` |
| `incident-response-deadline-keys-retired` |
`system/IncidentNotificationMatrix:escalationTimeoutMinutes`,
`system/IncidentNotificationRule:regulatorDeadlineHours`,
`system/IncidentNotificationRule:withinMinutes`,
`system/IncidentResponsePhase:targetHours`,
`system/IncidentResponsePolicy:retentionDays`,
`system/IncidentResponsePolicy:triageDeadlineHours` |
| `incident-response-family-retired` | `system/Incident`,
`system/IncidentCategory`, `system/IncidentNotificationMatrix`,
`system/IncidentNotificationRule`, `system/IncidentResponsePhase`,
`system/IncidentResponsePolicy`, `system/IncidentSeverity`,
`system/IncidentStatus` |
| `kernel-compatibility-matrix-estimated-migration-time-unit-in-key` |
`kernel/CompatibilityMatrixEntry:estimatedMigrationTime` |
| `kernel-context-preview-mode-retired` |
`kernel/KernelContext:previewMode`, `kernel/PreviewModeConfig`,
`kernel/TenantRuntimeContext:previewMode` |
| `kernel-event-bus-retention-unit-in-key` |
`kernel/EventPersistence:retention`,
`kernel/EventSourcingConfig:retention` |
| `kernel-health-check-and-hot-reload-durations-unit-in-key` |
`kernel/HotReloadConfig:debounceDelay`,
`kernel/PluginHealthCheck:interval`, `kernel/PluginHealthCheck:timeout`
|
| `kernel-package-lifecycle-durations-unit-in-key` |
`kernel/MultiVersionSupport:rollout.duration`,
`kernel/PackageDependencyResolutionResult:resolvedIn`,
`kernel/UpgradePlan:estimatedDuration` |
| `kernel-plugin-health-report-durations-unit-in-key` |
`kernel/PluginHealthReport:metrics.responseTime`,
`kernel/PluginHealthReport:metrics.uptime` |
| `kernel-plugin-security-durations-unit-in-key` |
`kernel/KernelSecurityPolicy:auditLog.retention`,
`kernel/KernelSecurityPolicy:authentication.tokenExpiration`,
`kernel/PluginSecurityManifest:vulnerabilityDisclosure.responseTime` |
| `kernel-runtime-config-timeout-unit-in-key` |
`kernel/RuntimeConfig:resourceLimits.timeout`,
`kernel/SandboxConfig:process.timeout` |
| `kernel-startup-orchestrator-durations-unit-in-key` |
`kernel/PluginStartupResult:duration`, `kernel/StartupOptions:timeout`,
`kernel/StartupOrchestrationResult:totalDuration` |
| `list-view-navigation-view-retired` | `ui/NavigationConfig:view` |
| `logging-durations-unit-in-key` |
`system/HttpDestinationConfig:batch.flushInterval`,
`system/HttpDestinationConfig:retry.initialDelay`,
`system/HttpDestinationConfig:timeout`,
`system/LoggingConfig:buffer.flushInterval` |
| `metadata-changed-event-payload-retired` |
`kernel/MetadataChangeOperation`, `kernel/MetadataChangedEventPayload` |
| `metadata-customization-protocol-retired` | 13 records, all `api/`,
`kernel/` defs |
| `metadata-manager-config-cache-ttl-unit-in-key` |
`kernel/MetadataManagerConfig:cache.ttl` |
| `metadata-manager-config-inert-cache-keys-retired` |
`kernel/MetadataManagerConfig:cache.enabled`,
`kernel/MetadataManagerConfig:cache.maxSize`,
`kernel/MetadataManagerConfig:cache.ttlSeconds` |
| `metadata-plugin-additional-types-retired` |
`kernel/MetadataPluginConfig:additionalTypes` |
| `package-rollback-response-retired` | `api/PackageRollbackResponse` |
| `packages-list-pagination-retired` |
`api/ListInstalledPackagesRequest:cursor`,
`api/ListInstalledPackagesRequest:limit` |
| `plugin-auto-restart-never-reinitialised` |
`kernel/PluginHealthCheck:autoRestart`,
`kernel/PluginHealthCheck:maxRestartAttempts`,
`kernel/PluginHealthCheck:restartBackoff` |
| `plugin-manifest-contributes-dead-members-retired` |
`kernel/Manifest:contributes.actions`,
`kernel/Manifest:contributes.commands`,
`kernel/Manifest:contributes.drivers`,
`kernel/Manifest:contributes.events`,
`kernel/Manifest:contributes.fieldTypes`,
`kernel/Manifest:contributes.functions`,
`kernel/Manifest:contributes.menus`,
`kernel/Manifest:contributes.themes`,
`kernel/Manifest:contributes.translations` |
| `plugin-manifest-contributes-routes-retired` |
`kernel/Manifest:contributes.routes` |
| `plugin-manifest-dead-containers-retired` |
`kernel/Manifest:capabilities`, `kernel/Manifest:configuration`,
`kernel/Manifest:extensions` |
| `plugin-manifest-kind-globs-retired` |
`kernel/Manifest:contributes.kinds.globs` |
| `plugin-security-scan-result-surface-retired` |
`kernel/KernelSecurityScanResult`, `kernel/KernelSecurityVulnerability`,
`kernel/PluginQualityMetrics:securityScan`,
`kernel/PluginSecurityManifest:scanResults`,
`kernel/PluginSecurityManifest:vulnerabilities` |
| `rest-api-endpoint-handler-status-retired` | `api/HandlerStatus`,
`api/RestApiEndpoint:handlerStatus`, `api/RouteCoverageEntry`,
`api/RouteCoverageReport` |
| `rest-api-plugin-durations-unit-in-key` |
`api/RestApiEndpoint:cacheTtl`, `api/RestApiEndpoint:timeout`,
`api/RestApiPluginConfig:performance.defaultCacheTtl` |
| `rest-server-config-dead-keys-retired` | 11 records, all `api/` defs |
| `session-user-language-retired` | `api/SessionUser:language` |
| `stack-themes-carrier-retired` | `ui/BorderRadius`, `ui/ColorPalette`,
`ui/Shadow`, `ui/Theme`, `ui/ThemeMode`, `ui/Typography` |
| `startup-orchestrator-retired` | `kernel/HealthStatus`,
`kernel/PluginStartupResult:health`,
`kernel/PluginStartupResult:plugin`,
`kernel/PluginStartupResult:startTime`, `kernel/StartupOptions`,
`kernel/StartupOrchestrationResult` |
| `system-cache-durations-unit-in-key` |
`system/CacheAvalanchePrevention:circuitBreaker.resetTimeout`,
`system/CacheTier:ttl` |
| `system-collaboration-durations-unit-in-key` |
`system/CollaborationSessionConfig:idleTimeout`,
`system/CollaborationSessionConfig:snapshot.interval` |
| `system-failover-health-check-interval-unit-in-key` |
`system/FailoverConfig:healthCheckInterval` |
| `system-metrics-jsdoc-durations-unit-in-key` |
`system/MetricDefinition:summary.maxAge`,
`system/MetricExportConfig:interval`,
`system/MetricsConfig:collectionInterval`,
`system/MetricsConfig:retention.period`,
`system/ServiceLevelObjective:errorBudget.burnRateWindows.window` |
| `system-metrics-window-durations-unit-in-key` |
`system/MetricAggregationConfig:window.size`,
`system/ServiceLevelIndicator:window.size`,
`system/ServiceLevelObjective:period.duration` |
| `system-object-storage-durations-unit-in-key` |
`system/AccessControlConfig:maxAge`, `system/StorageConnection:timeout`
|
| `system-registry-config-durations-unit-in-key` |
`system/RegistryConfig:cache.ttl`,
`system/RegistryUpstream:syncInterval`,
`system/RegistryUpstream:timeout` |
| `system-tracing-otel-exporter-durations-unit-in-key` |
`system/OpenTelemetryCompatibility:exporter.batch.exportTimeout`,
`system/OpenTelemetryCompatibility:exporter.batch.scheduledDelay`,
`system/OpenTelemetryCompatibility:exporter.timeout`,
`system/TracingConfig:performance.exportInterval` |
| `system-tracing-span-duration-unit-in-key` | `system/Span:duration` |
| `system-worker-queue-rate-limit-duration-unit-in-key` |
`system/QueueConfig:rateLimit.duration` |
| `tenant-schema-cache-ttl-unit-in-key` |
`system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL` |
| `training-deadline-keys-retired` |
`system/TrainingCourse:durationMinutes`,
`system/TrainingCourse:validityDays`,
`system/TrainingPlan:gracePeriodDays`,
`system/TrainingPlan:recertificationIntervalDays`,
`system/TrainingPlan:reminderDaysBefore` |
| `training-family-retired` | `system/TrainingCategory`,
`system/TrainingCompletionStatus`, `system/TrainingCourse`,
`system/TrainingPlan`, `system/TrainingRecord` |
| `websocket-durations-unit-in-key` |
`api/WebSocketConfig:pingInterval`,
`api/WebSocketConfig:reconnectInterval`, `api/WebSocketConfig:timeout`,
`api/WebSocketServerConfig:heartbeatInterval` |

## Prose corrected (the single-entry sites of `5854110917` /
`5854456343`, and the rationale sentences)

| site (at this head) | was | now |
|---|---|---|
| `packages/spec/src/migrations/registry.ts:78–86` (step 17 docblock,
fact correction only) | 「Mechanical, and mechanical only … there is no
semantic residue and the `semantic` list is deliberately empty」 | the
three renames replay losslessly as D2; they carry no D3 entry because
step 17 shipped before the rule and was not back-filled; the `semantic`
list is NOT empty. ⛔ No step-17 entry added. |
| `packages/spec/src/migrations/registry.ts:5256` (step 18 rationale,
`tenancy.organizationField`) | 「The conversion is a lossless delete and
there is no semantic residue」 | a lossless delete still leaves the
author a judgment, carried by
`object-tenancy-organization-field-retired` |
| `packages/spec/src/conversions/registry.ts:3460`
(`datasource-driver-mongo-to-mongodb`, protocol 17) | 「Why D2 and not
D3」 | 「Why the data repair is D2」, plus: losslessness does not decide
whether a family owes D3; this one is protocol 17 and has none |
| `packages/spec/src/conversions/registry.ts:9312`
(`api-endpoint-cache-ttl-to-cache-ttl-seconds`) | 「gets a conversion
rather than a semantic entry」 | 「also gets a conversion」, and names its
D3 entry |
| `packages/spec/src/conversions/registry.ts:9804`
(`list-view-sort-string-clause-to-array`) | 「which is why this is a D2
conversion rather than a semantic TODO」 | the data repair is D2; the
family's D3 entry carries the clauses the rewrite leaves alone |
|
`packages/spec/src/migrations/entries/retired-keys/18.api__ApiEndpoint__cacheTtl.ts:11–19`
| 「a D2 CONVERSION rather than a semantic entry」 | also a D2 conversion,
and names the D3 entry |
|
`packages/spec/src/migrations/entries/semantic/18.metadata-endpoints-switch-radius-repartitioned.ts:11–13`
| 「exactly the residue D2 cannot express, which is why this is a
semantic entry」 | that residue is why there is no D2 at all; the D3
entry is owed either way |
| `packages/spec/scripts/build-migration-registry.ts:276` | 「a major
whose semantic residue is genuinely nil (protocol 14)」 | an empty region
is a real state (a freshly opened step, or protocol 14's, which predated
the rule) |

⛔ Not touched: the governed texts (ADR-0087,
`.claude/skills/spec-property-retirement/SKILL.md` §3), which are
objectstack-ai#20188's.

## The census pin

**Where:** `packages/spec/src/migrations/migrations.test.ts` › `registry
integrity`: `from protocol 18 on, every graduated D2 conversion is named
by a D3 entry of its own step (ruling B)`, plus a control test. It reads
the major's `entries/semantic/` files (comment and literal) inside its
own package; `check:migration-registry` already proves those files and
the generated region are one set.

**What it asserts:** for every step whose `toMajor` is 18 or later,
every id in `conversionIds` appears as a whole id in at least one
`semantic/` entry of that major. A new major-18 (or later) retirement
that lands a D2 conversion with no D3 entry naming it goes red, naming
the conversion.

**What it cannot see**, stated so a green run is not over-read: (1)
whether the naming entry is that family's OWN (a passing mention
satisfies it; the census judged ownership by reading); (2) a family
retired with no conversion at all (no machine-readable link joins a
retired key or def to its D3 entry; the census paired those by reading,
and found none missing). Protocol 17 is outside the pin by design:
measured with the same logic, 53 of its 57 graduated conversions are
named by no step-17 entry, and step-17 backfill is out of scope (triage
`5854164872`).

**Reverse verification (one-shot, no permanent test file).** At
`c8656ad35`, with the entries committed: deleted
`18.object-tenancy-organization-field-retired.ts` (absence confirmed on
disk before the run), ran the pin: `× from protocol 18 on …` with `+
"protocol 18: object-tenancy-organization-field-removed"`, `Tests 1
failed | 140 skipped`. Restored with `git checkout HEAD -- PATH` (that
path) inside a `trap … EXIT INT TERM`: blob `2cf3007d9fff` equals
HEAD's, `git diff HEAD` empty. Direction observed: red, the expected
one. No build involved: the test imports `src/` and reads the entry
files directly.

## Patch round 1 (contract review `5857834457`: FAIL at `3197fce29`)

**Blocking: fixed.** `Lint & Repo Gates` step 189
(`check-issue-citations.mjs`, judging pass) was red. Four bare citations
this PR added answer 404 on the board: objectstack-ai#10329, objectstack-ai#10926, objectstack-ai#12868 and
objectstack-ai#14676. Each was in an entry's leading comment, and again in the
regenerated region. `--probe-cause` classes all four as **deleted** (the
web endpoint also answers 404, so none was transferred), so none of them
is a reference to another repository to qualify. Each comment now
anchors to the commit in this repository's history that retired the
family, and says in words what that commit decided. That is the
precedent of commit `66e266c93` (ruling C+D on objectstack-ai#19123). Every sha is an
ancestor of `origin/main`:

| entry | was | now anchored to |
|---|---|---|
| `mapping-lookup-params-retired` | objectstack-ai#10329 | commit `15d58dbf1` (the
import path never read the four lookup steering params) |
| `translation-component-submit-label-retired` | objectstack-ai#10926 | commit
`d173125fb` (the copy key left with its only declarer, `element:form`) |
| `form-view-option-default-retired` | objectstack-ai#12868 | commit `c459da6bc` (the
ruled narrowing: the form-view face drops per-option `default`, the
object-field face keeps it enforced) |
| `connector-error-mapping-retired` | objectstack-ai#14676 | commit `13c48c2a5`
(eleven inert keys, one spelled like the live `userMessage` channel) |

Only the comments changed; no string an author is shown moves. The
region was regenerated with `gen:migration-registry`. The round-1
report's `pnpm check:issue-citations :: exit 0` was the package script,
which runs only the `--self-test`. The judging pass CI runs was exit 2
at `3197fce29` (8 findings = 4 numbers × 2 sites) and is exit 0 now
(below).

**Pin message.** The census pin's assertion now names the unnamed
`protocol N: conversion-id` pairs and the remedy: add a D3 `semantic`
entry of that step whose text names the conversion id as a whole word.
Its logic and scope are unchanged. Shown firing at `21418c4d2` with one
entry removed (trap-guarded restore, blob equal to HEAD's, `git diff
HEAD` empty): `AssertionError: graduated D2 conversion(s) named by no D3
entry of their own step: protocol 18:
object-tenancy-organization-field-removed. Remedy: add a D3 semantic
entry of that step …`, `Tests 1 failed | 140 passed`.

**Body.** Table 1: `integration/Connector:connectionTimeoutMs` moved
from row 16 to row 20. Its own comment names
`connector-connection-timeout-ms-removed`; the permission id appears
there only as a comparison. The code was already right.

## Sibling PRs

- **PR objectstack-ai#20238** (objectstack-ai#20161) has since LANDED as `6a6a17b62`, with the D2
conversion `report-joined-chart-removed` and
`18.ui-report-joined-chart-retired.ts`, which names that id. The union
of this head with `main` at `6a6a17b62` is clean and passes the pin (37
pairs, 0 unnamed; delta review `5859315908`).
- `main` was merged three times with `os-regen-merge.sh`, and never by
hand in a generated region: at `a70cd62e5` (objectstack-ai#20223 and objectstack-ai#20245), at
`21418c4d2` (`cel-predicate-one-value-comparand-refused` and
`filter-query-face-comparands-refused-at-save`) and at `a930cacea`
(step-17 rationale prose from objectstack-ai#20268). Each is D3-only or prose, with no
new step-18 conversion. Regeneration produced a commit only after the
first merge (`3197fce29`) and changed nothing after the other two. Every
sibling entry id was verified present.

## Verification (head `a930cacea`)

- `pnpm check:issue-citations && node
scripts/check-issue-citations.mjs`, exactly as CI runs it (base
`origin/main`): **exit 0**, 112 citations across 29 files: 106 resolve,
6 cross-repo unjudged, 0 findings. At `3197fce29` the same command
exited 2.
- `pnpm --filter @objectstack/spec build` under the verify lock: ok.
`check:generated`: all 15 generated artifacts up to date.
`check:migration-registry`: current (292 semantic, 214 retired-key, 199
retired-def). `spec-changes.json` and `docs/protocol-upgrade-guide.md`
do not move: they project up to the current protocol major, and step 18
is beyond it.
- `pnpm --filter @objectstack/spec exec vitest run --project local`:
**552 files, 16257 passed, 1 todo**. The `src/migrations/` directory
alone: 3 files, 151 passed.
- `pnpm --filter @objectstack/spec typecheck` (tsc, scripts, test layer)
at `21418c4d2`, the head before the last merge, which brought only
another PR's prose into this diff's files: exit 0, test-typecheck debt
unchanged (53 files / 255 errors / 142 signatures).
- `node scripts/pm/dispatch-gates.mjs --commands` (no paths) at
`a930cacea`, every command run and its exit recorded, reconciled with
`--ran`: **89 derived, 87 run (all exit 0), 2 NOT MEASURED**.
`check:dual-build-cjs-loads` and `check:type-check-debt` exited 3
(PREREQUISITE NOT MET: the full 86-package workspace build does not fit
the foreground cap on this shared box). CI runs both.
`check:pm-dispatch-gates` finished this time: exit 0, in 907.6 s.
- ESLint, narrowed and proven: all 31 changed `.ts` files,
`--no-inline-config --format json`: 0 errors, 0 warnings, none reported
ignored. `eslint.config.mjs` enables no type-aware linting (its own
statement at `eslint.config.mjs:326–328`), so this diff cannot move any
untouched file's verdict.
- Changeset: `@objectstack/spec` `patch`. The published registry text
changes; no accept set moves.

## Acceptance notes (observed, not filed)

- `registry.ts:108` (released step-17 text) still says the sharing-rule
`full` conversion 「leaves no semantic residue」: triage scoped step-17
backfill out, so it is left as is.
- New entries keep tracker numbers in their `//` comments only, never in
the strings an author is shown (AGENTS.md runtime-strings rule). Several
older entries do cite numbers in `reason`; not touched.
- `dashboard-refresh-interval-unit-in-key` states the console renderer's
release lag as a verification step, not as a present fact: this
container has no objectui checkout at the pin to measure it.
- `main` moved after the last merge (`a930cacea`). objectstack-ai#20285 (`2aa25efb4`,
prose in five semantic entries) and objectstack-ai#20238 (`6a6a17b62`, a new step-18
conversion with its entry) landed under `migrations/` and
`conversions/`. The delta review merged this head onto `6a6a17b62`:
clean, with all six regions still mirrored. The queue verifies the
merged generation. (Corrected by the seat at 2026-09-27T19:57Z; the
earlier wording said nothing under those paths had moved.)

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants