fix(spec): os migrate meta guidance for the remaining migration-entry families states each lesson in words, not tracker numbers (stage 9) - #20630
Conversation
…n words, not tracker numbers (stage 9) Each ADR-0087 semantic entry that still cited a tracker, pull-request, decision-batch or cross-repository number in its replacement / reason / acceptanceCriteria prose now says what the cited ruling, measurement or fix decided. Verbatim quotes that carried a card or batch number keep only their operative words. ADR ids and contributor-guide rule references stay; the address entry's dangling AGENTS.md rule number is stated as the rule. Text only: no entry id, surface, conversion or matcher changes. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
… the upgrade guide Generator output only (gen:migration-registry, gen:spec-changes, gen:upgrade-guide) for the stage-9 entry prose. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…patch changeset With the remaining families rewritten, no semantic entry's printed guidance carries a tracker id, so the pin covers the whole directory instead of a prefix list: an entry added later, in any family, is held on arrival. REWRITTEN gains the 44 entries this stage rewrote (203 -> 247). Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 619ee3e31cc279c3993f4aec32a7e1bb26781a9a && git checkout 619ee3e31cc279c3993f4aec32a7e1bb26781a9a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b80ab579d8bdd7499805612104ce95d931b0f03e 96b994e472ba988aa9fae42e3b3c1fde2437e591 && git checkout -B drift-repro b80ab579d8bdd7499805612104ce95d931b0f03e && git merge --no-ff 96b994e472ba988aa9fae42e3b3c1fde2437e591
node scripts/docs-audit/affected-docs.mjs --json b80ab579d8bdd7499805612104ce95d931b0f03e
|
Contract reviewServed-tier: Read-only review of PR #20630 (stage 9 of #20233) at the head above, against ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…les.ts to the commits that decided them (objectstack-ai#20631) Part of objectstack-ai#20597 Clause-②: no Stage 2 of the `packages/lint` dead-citation sweep (claim `5888191846`). Stage 1 (PR objectstack-ai#20612) left `packages/lint/src/authoring-rules.ts` at its base blob while PR objectstack-ai#20593 held the file. That PR has landed (`e651556e2d`). Each of the file's five comment and docblock lines that cited a tracker number answering 404 now cites, in ruling C+D's form C, the commit in this repository's history that decided what the line states. Each line still says what was decided. Comments only: 5 lines out, 5 in, in one file, plus one `@objectstack/lint` `patch` changeset. This PR says `Part of`: the form-D finding-message string at `validate-react-page-props.ts:1198` (`(objectstack-ai#11284)`, shown to authors) stays on the card as a separate decision. It is byte-identical here (see Acceptance notes). ## Measurement The instrument is the gate's own `node scripts/check-issue-citations.mjs --census --json`, filtered to `packages/lint/`. - **Before:** base `1322cc72c9`, 2026-09-29T10:18:50Z to 10:22:33Z, board enumerated (185 pages, frontier objectstack-ai#20627). Repo-wide `allocated-but-absent` 2,209. - **After:** head `2e1955b492`, 11:13:21Z to 11:16:47Z (185 pages, frontier objectstack-ai#20630). Repo-wide `allocated-but-absent` 2,204, exactly 5 fewer. No finding at head is absent at base. | site in `authoring-rules.ts` | before | after | anchor | |---|---|---|---| | `:201` (the `AuthoringFinding.path` docblock) | objectstack-ai#10064 | commit | `def0d3e63` | | `:1123` | objectstack-ai#16659 | commit | `ecdfc9411` | | `:1722` | `(PR objectstack-ai#8546)` | commit | `ba5e957ef` | | `:1748` | `[objectstack-ai#19370]` | commit | `a227afa41` | | `:1768` | `[ADR-0090 D3 / objectstack-ai#8310 → objectstack-ai#19370]` | commit | `a227afa41` (ADR-0090 D3 and objectstack-ai#8310 stay) | | **`packages/lint` total** | **5** | **0** | 4 numbers, to 4 distinct shas | The five lines on `origin/main` `e651556e2d` are the same lines at the base: `packages/lint` is byte-identical between `e651556e2d` and `1322cc72c9`. ## Why each anchor decides its line Each sha resolves uniquely (`rev-parse --disambiguate` gives 1 object). Each is single-parent. `merge-base --is-ancestor` exits 0 against `origin/main` and against the base, and the repository is not shallow. Each commit's own diff was read for the rule its line states. - **objectstack-ai#10064 to `def0d3e63`**: "key collection-resident publish-gate finding paths by name, not the private snapshot index". Its body names objectstack-ai#10064 as the card it lands, "(maintainer ruling 2026-08-20: Option A)". Its own diff wrote this very docblock: the positional-as-rules-emit-it sentence, the `objects.acme_invoice.sharingModel` example and the pointer to `nameKeyFindingPath`, which the same commit introduced in `runtime-gate.ts`. - **objectstack-ai#16659 to `ecdfc9411`**: the squash commit that declares a time-triggered flow's acting organization. Its diff adds `FLOW_SCHEDULE_ORGANIZATION_MISSING` (`flow-schedule-organization-missing`, at `warning`) to `validate-flow-trigger-readiness.ts`. It also wrote the `authoring-rules.ts` sentence "... added a sixth id, `flow-schedule-organization-missing`, at `warning`" that this line opens, and its sub-commits name objectstack-ai#16659. The live objectstack-ai#17396 retirement beside it stays. - **`(PR objectstack-ai#8546)` to `ba5e957ef`**: PR objectstack-ai#8546's own squash commit, "permission/book cross the runtime publish gate; object measured dirty stays behind". Its `authoring-rules.ts` diff changes `runtimeTypes: ['seed']` to `['seed', 'permission', 'book']`, which is objectstack-ai#8310 slice 1 as the line states. The live objectstack-ai#8310 stays. - **objectstack-ai#19370 to `a227afa41`** (two sites): "`security-role-word` crosses to the runtime publish gate, whole". Its body names objectstack-ai#19370 as the card it lands. Its own `authoring-rules.ts` diff wrote both lines: "[objectstack-ai#19370] It has since crossed, also whole, on its own entry" and the `[ADR-0090 D3 / objectstack-ai#8310 → objectstack-ai#19370]` marker. Stage 1 cited the same commit for the same number in `runtime-gate.ts` and `validate-security-posture.ts`. Rung: no file under `docs/adr/**`, `docs/NORTH-STAR.md` or `scripts/adr-anchors/` names any of the four numbers. So the commit rung is right, as in stage 1. ADR-0090 D3 already stands on `:1768` and is kept. ## Mechanical proof - **Token and residue guard.** A scratch instrument on the TypeScript 6.0.3 parser compares the base blob with the head blob at two levels. The first is leaf AST tokens, with JSDoc nodes excluded. The second is the non-comment residue: every comment range dropped, everything else compared byte for byte. The controls mutate the head text in memory only. - Real run: 3,543 tokens at base and at head, tokens EQUAL, residue EQUAL (exit 0). - Dark control, a whole comment line inserted: tokens EQUAL, residue EQUAL, comment ranges 957 to 958 (exit 0). - Lit control, a code statement inserted: DIFFER at token 0, residue DIFFER (exit 1). - Lit control, one character inserted into a parser-located string literal: DIFFER at token 5, residue DIFFER (exit 1). The first string control was a no-op and is void: it searched by text and landed in a comment, reading EQUAL. It was re-anchored on a parser-located literal and re-run. The mutation was confirmed landed. - **Line balance**: +5/−5, and every changed line is comment-shaped. The file has 2,011 lines at base and at head. - **Tracker numbers**: removed objectstack-ai#10064, objectstack-ai#16659, objectstack-ai#8546 and objectstack-ai#19370 ×2. The added lines carry only the live objectstack-ai#8310 ×2, which stands on both the removed and the added side of `:1722` and `:1768`. So added-not-removed is empty, and no `PR #N` stands on an added line. There are 215 `#N` tokens at base and 210 at head. - **Shas**: 4 distinct on added lines (`a227afa41` ×2), none on removed lines. - **Literal readers**: `scripts/doc-authoring-prose-id.baseline.json` pins this file's string sites as objectstack-ai#4463, objectstack-ai#4716, objectstack-ai#4717, objectstack-ai#7220, objectstack-ai#8309 and objectstack-ai#9698, none of them these four numbers. `check-docs-transcript-drift` loads the registry module, not its comments. ## Tests and gates (at head `2e1955b492`) - Build under `os-verify-lock`: `pnpm exec turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*`. The last run printed Tasks 71 successful, 71 total, and VERDICT command-exit 0. It took three attempts inside a 270 s timeout on a shared box. The first two were cut off at 39 of 46 and 66 of 68 tasks, and turbo's cache carried their finished tasks forward. - `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2` under the lock: Test Files 115 passed (115), Tests 5379 passed (5379), VERDICT command-exit 0. - `pnpm --filter @objectstack/lint typecheck` under the lock: exit 0. `check:test-typecheck` OK (2 files, 6 errors, 2 pinned signatures held). VERDICT command-exit 0. - Lint, as a proven narrowing: `eslint --no-inline-config --format json packages/lint/src/authoring-rules.ts` reports 1 file, 0 errors, 0 warnings. `isPathIgnored` is false, read through eslint's API. `eslint.config.mjs:327-328` says type-aware linting is never enabled, so a comment edit cannot move an untouched file's verdict. The repo-wide `pnpm lint` is CI's. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 54 families, and all 54 ran with exit 0. `--ran` reads "54 derived, 54 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero. Among them: - `node scripts/check-issue-citations.mjs`, the live diff-scoped run, judged 2 citations in 1 file, the kept objectstack-ai#8310 ×2, and both answer as issues. `pnpm check:issue-citations` passes its self-test (114 cases in 8 batteries). - `pnpm check:doc-authoring`: the sibling prose-id baseline holds, 810 pinned sites across 230 files, no growth. - `pnpm check:nul-bytes`: OK over 9,253 tracked text files. A control-byte scan of both changed files finds none. - Generated pages: none to regenerate. No page under `content/docs/references/` names the four numbers, `AuthoringFinding` or `nameKeyFindingPath`, and no generator reads `packages/lint/src`. - Changeset: `patch` for `@objectstack/lint`, a new file (stage 1's `lint-provenance-anchors.md` is untouched). `files[]` ships `dist`, and the rewritten comments reach it: - `commit def0d3e` is in the `AuthoringFinding` docblock of `dist/runtime-*.d.ts`, beside the unchanged "Positional as RULES emit it", the positive control. - `commit ba5e957` (cited only here) and `commit a227afa` are in `dist/index.js` and `dist/index.cjs`. - None of the four numbers remains in `dist`. - Merge probe: a no-driver `merge-tree` of the head onto `origin/main` `542670da6d`, from a bare shared clone with no `merge.*` config, exits 0. None of the three commits `main` gained since the base touches `packages/lint`. - No ablation or reverse verification: the change is comment-only, so there is no behaviour to invert. ## Hypotheses (measured first) 1. **Holds.** At the base the census reads exactly 5 dead sites in `packages/lint`, all in `authoring-rules.ts`, and after the change it reads 0. The card's sixth site, the `(objectstack-ai#11284)` string at `validate-react-page-props.ts:1198`, is outside the census because the census blanks string literals. It was read directly: still present, and the file is byte-identical from base to head. 2. **Holds.** The five sites read `:201` objectstack-ai#10064, `:1123` objectstack-ai#16659, `:1722` `(PR objectstack-ai#8546)`, `:1748` and `:1768` objectstack-ai#19370, on `e651556e2d` and at the base alike. All four anchors were re-verified above from their own diffs, not copied. 3. **Holds.** PR objectstack-ai#20593's new lines cite objectstack-ai#20553, objectstack-ai#20611 and objectstack-ai#20552 (and ADR-0041). All three answer 200, and the census finds no dead site on them. None of the five lines' sentences changed in meaning. The one adjacency is described under Acceptance notes. ## Deviations - Commit trailers follow AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`), not the model-named trailer the harness reminder suggested. The pre-push trailer check passed on both pushes. - The first lit string control was a no-op: it searched by text and landed in a comment. It is reported void above and was re-run on a parser-located literal. ## Acceptance notes **Form D, not touched (why this PR says `Part of`):** `validate-react-page-props.ts:1198` is the `react-prop-deprecated` finding `message`. It ends "...is removed after the deprecation window (objectstack-ai#11284)." An author sees it, so it takes ruling D (no number). That is a string change, outside this comment-only claim. `scripts/doc-authoring-prose-id.baseline.json` pins it (`objectstack-ai#11284: 1` for that file), and that baseline is shrink-only. **An ordinal beside PR objectstack-ai#20593's insertion, kept verbatim:** the paragraph above `:1123` now ends "objectstack-ai#20553 made it five", counting the rules that emit `error`. `:1123` reads "Commit ecdfc94 added a sixth id", an ordinal that commit wrote itself. The two count different things: rules that emit `error`, and ids in the rule file. The ordinal is also imprecise on its own terms, because `validate-flow-trigger-readiness.ts` exported six ids before `ecdfc9411`, so the new one was its seventh. This PR moves only the tracker number, so the word stays as written. **Outside the census's surface (noted, not swept):** stage 1 notes that lint test titles and hand-written docs still cite these numbers. `content/docs/deployment/validating-metadata.mdx` cites objectstack-ai#19370 at `:472`, `:483` and `:515`. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…s that decided them (objectstack-ai#20634) Part of objectstack-ai#20596 Clause-②: no ## What changed This is the third stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/plugins/plugin-auth/src/**` and nothing else. By census, it is the largest package in the lane that no open PR or in-flight claim holds (the claim, `5888562941`, gives the order). Later stages cover the other packages, so this PR says `Part of` and the card stays open. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), by the method of stages 1 and 2 (PR objectstack-ai#20609 as `422db788a`, PR objectstack-ai#20626 as `b80ab579d`). That is **95 sites on 95 lines in 31 files, covering 16 numbers**: - the 52 census sites (all of this package's census sites); - 38 sites in test comments, which the census defers; - 5 sites in the hyphen-joined spelling `objectstack-ai#13398-class`, which the gate's extractor does not match at all (see Acceptance notes). Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and it says in its own words what that commit decided. No ADR or ruling-record file records the decision behind any of the 16 numbers, so every anchor is a commit: **15 distinct shas** (`objectstack-ai#11477` and `objectstack-ai#12029` share one, because `objectstack-ai#12029` was the pull request that settled `objectstack-ai#11477`). No number was dropped. Only comments changed. Every touched source file keeps its line count (107 lines out, 107 in, over 31 files), so no line citation into these files moves. 12 of those 107 lines hold no dead citation; they are reflow or a lost referent, listed under Wordings below. No code token moves (see the guard below). **No citation number is added.** Every tracker number on an added line was already on the line it replaces. Over the whole diff, added minus removed is 0 or negative for every number (the gate's own `extractCitations` over the diff: 103 citations removed, 13 added, all 13 kept resolving numbers), and no number is new to the diff. No PR number stands on an added line. Twenty-one dead sites are left on purpose, all of them test titles (see the list below). One more file: a `patch` changeset for `@objectstack/plugin-auth`, because the rewritten docblocks ship (see Changeset below). ## Census: `plugin-auth`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count below is its `allocated-but-absent` findings under `packages/plugins/plugin-auth/`. Each run counts as a reading only because its board frontier equals the newest issue number, read by a separate request just before and just after the run. | reading | tree | board | whole-repo `allocated-but-absent` | plugin-auth sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `b80ab579d`, run 2026-09-29T10:43:31Z to 10:47:03Z | enumerated, 185 pages, frontier objectstack-ai#20629 (newest objectstack-ai#20628 before, objectstack-ai#20629 after), 18,456 numbers | 1,955 | **52** | 52 | 13 | 12 | | after | head `5ae64e8b8`, run 11:12:05Z to 11:15:37Z | enumerated, 185 pages, frontier objectstack-ai#20630 (newest objectstack-ai#20630 before and after), 18,457 numbers | 1,903 | **0** | 0 | 0 | 0 | The before count matches the 52 that census `5884031174` read at `f11b5f20`. The whole-repo drop is 52, exactly this diff's census sites. The `resolves` tally is 32,832 in both runs, and `resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did not move either. No run was truncated or discarded: all three enumerations in this stage (two census runs and the supplementary board below) read 185 pages at the newest frontier. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `classifyCitation` over every `.ts` file under `plugin-auth/src` (178 files). It uses one board, enumerated by the gate's own `enumerateBoard` at 10:50:47Z (185 pages, frontier objectstack-ai#20629, equal to the newest). | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `b80ab579d` | 2,150 | **111** | 52 | 38 | 0 | 21 | | after, `9fd0ebf10` | 2,060 | **21** | 0 | 0 | 0 | 21 | Its src-comment column equals the census's 52, which is the control on the second instrument. The 1,966 resolving, 46 pull-request and 27 cross-repo citations are the same in both readings. Neither instrument sees the 5 `objectstack-ai#13398-class` sites; a plain grep for the 16 numbers over `plugin-auth/src` at the head finds only the 21 test titles (and the digits `11477` inside test fixture e-mail addresses and a password, which are code tokens, not citations). ## Per-number table Sites and files count all dead sites the gate sees in scope at the base (comments and strings, tests included). `rewritten / left` counts the sites rewritten and the sites left. Each anchor was read in its message and diff, not only its subject. | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#8676` | 22/6 | 18/4 | `d6e80b28b`: `sys_account.password` and `previous_password_hashes` are flagged `internal: true`, and every reader is recovered through the engine's privileged accessor (the adapter readback table gains `password`; plugin-auth's own raw-engine reads get `recoverInternalFieldsForSystemRead`). Its subject names `objectstack-ai#8676` | | `objectstack-ai#8734` | 4/2 | 3/1 | `f8eb73601`: the last-admin guard's standing-key lists are bound to what `resolveAuthzContext` actually reads (`STANDING_KEYS_BY_TABLE` / `STANDING_KEY_EXCLUSIONS` and the correspondence gate). Its subject names `objectstack-ai#8734` | | `objectstack-ai#10165` | 1/1 | 1/0 | `801296050`: lifecycle `ttl` gains an `onlyWhen` row filter (maintainer ruling option A on `objectstack-ai#10165`, quoted in its message). The same anchor the spec stages gave this number | | `objectstack-ai#10366` | 3/2 | 2/1 | `bbe643c08`: the localhost trusted-origin substitution is gated to non-production. Its diff writes both rewritten lines and its changeset names `objectstack-ai#10366` | | `objectstack-ai#11343` | 19/8 | 18/1 | `c0714eb5d`: walled platform-admin elevation requires a VERIFIED owner-email match (a fail-closed allow-list over `email_verified`), the bootstrap replays on the verifying `sys_user` update, and the dev-admin seed stamps its account verified. Its message names `objectstack-ai#11343` as the card it completes | | `objectstack-ai#11477` | 6/3 | 3/3 | `6dd3e6968`: `/admin/remove-user` gets the raw-mount shading `/admin/ban-user` has, so authorization runs before the break-glass guard (ruled option A on `objectstack-ai#11477`, as its message records) | | `objectstack-ai#11626` | 1/1 | 1/0 | `a6eca9223`: `check:engine-double-contract` admits a single-verb engine double on the contract it DECLARES, a second admission route beside sibling inference. Its diff names that route `objectstack-ai#11626` | | `objectstack-ai#11640` | 11/6 | 7/4 | `bf8d129b5`: a walled deployment whose declared owner has no verification path gets a loud, named warning at boot, and boot proceeds (maintainer ruling 2026-08-25, option A). Its subject names `objectstack-ai#11640` | | `objectstack-ai#11741` | 4/2 | 2/2 | `b706af987`: `SendEmailInput` gains an optional `organizationId`, threaded from the producers that hold one (the invitation among them). The same anchor stages 1 and 2 and the spec stages gave this number | | `objectstack-ai#11757` | 4/4 | 4/0 | `4d25d22d4`: the rc.1-era `sys_scim_provider` platform object is retired. Every `objectstack-ai#11757` site in the tree before it says the object "retires under objectstack-ai#11757" | | `objectstack-ai#12029` | 2/2 | 2/0 | `6dd3e6968`: `objectstack-ai#12029` was the pull request itself; this is its squash commit, the gate-then-delegate mount on `/admin/remove-user` | | `objectstack-ai#13398` | 6/2 | 3/3 | `e238c79f0`: the published-sink ruling, that raising a log level must never widen a published sink. No record of the ruling exists in the repo; this commit's pin is the earliest text in history that records it (see Wordings) | | `objectstack-ai#14762` | 21/4 | 19/2 | `35e94c96b`: auth OTP SMS and auth mail read the recipient's own `sys_user.locale`, one rung above the request and the deployment default, in the order ruled for `objectstack-ai#14788`. Its diff carries `objectstack-ai#14762` 24 times | | `objectstack-ai#14902` | 3/2 | 3/0 | `61821e54c`: a plain unique index over duplicate rows is loud and non-fatal (the boot continues), and `os migrate plan` stops calling it `safe`. Its message names `objectstack-ai#14902` as the card it ends | | `objectstack-ai#14998` | 2/1 | 2/0 | `f1e91595f`: the batch-6 admin endpoint graphs load at module top, not inside each clocked case, which removed the cold-import timeout flake | | `objectstack-ai#15092` | 2/1 | 2/0 | `9e9f03abe`: `settleSelfRegistrationGrant`'s trailing filter no longer silently DROPS a malformed permission-set row; it refuses. The only commit in history that names `objectstack-ai#15092` | Plus 5 `objectstack-ai#13398-class` sites the gate does not extract, anchored like the other `objectstack-ai#13398` sites: `boot-sign-in-reachability.ts:109`, `:512`, `boot-sign-in-reachability.test.ts:595`, `tenancy-service.ts:249`, `:257-258`. Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1 for each), and every one is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 15; the history is complete, `--is-shallow-repository` false, 15,083 commits). A line-origin pickaxe (`git log -S` on each dead line's exact text) found each line entering either in its anchor commit or in a later commit that cites that commit's decision: for example `4d5b4f832` (the operator-provisioned stamp) and `4f65837a7` (the L3 re-anchor) cite `c0714eb5d`'s verified-owner rule, `f074616e6` (invitation locale) cites `35e94c96b`'s stored rung, `8064e6da1` (the has-permission mount) cites `6dd3e6968`'s seam, and `9bd4344e4` carries the `account-identity-preflight` text that cites `61821e54c`. ## Wordings to check - **`objectstack-ai#13398` → `e238c79f0`, and not stage 2's `953a81f4a`.** Stage 2 anchored its one `objectstack-ai#13398` site at `953a81f4a` (2026-09-02) as the earliest application of the published-sink ruling. In this package, `e238c79f0` (2026-08-31) already records it: its pin in `durability-swallow-repair.test.ts` says raising the level "means widening a published sink — refused as actively harmful by the maintainer's" ruling. It is earlier, and it is in this package, so it is the anchor here. Its own commit message still calls the level "objectstack-ai#13398's question", which is why the lines say "the published-sink ruling (commit e238c79)" rather than claiming that commit made the ruling. - **Reflow, 11 lines with no dead site** (every file keeps its line count): - `auth-manager.ts:7554-7557`: 「routes that LEVEL question to the published-sink ruling (commit e238c79) and tells this batch to fix the SILENCE only」, the rest of the paragraph reflowed unchanged (3 lines). - `durability-swallow-repair.test.ts:36-40` (4 lines) and `:527-529` (2 lines): the same substitution, and 「which routes that question there」 became 「which keeps that question」, because "there" pointed at the number. - `tenancy-service.ts:257-258`: 「exactly what the sink ruling (commit e238c79) forbids」 (1 line). - `find-envelope-limb-removal.test.ts:47-48`: 「also carried the silent-DROP shape, and commit 9e9f03a fixed it in the OPPOSITE direction」 (1 line). - **A lost referent, 1 line.** `auth-plugin.ts:2738-2739`: 「(the objectstack-ai#12029 worked reading — a shadow is accounted for …)」 became 「(as it read commit 6dd3e69's remove-user mount — a shadow is accounted for …)」. `check:auth-mount-ledger` has counted a shadowing mount since `26dea1495`; the "worked reading" was that PR's application of it to `/admin/remove-user`, which `6dd3e6968` mounts. - `sys-session-ttl-sweep.test.ts:230`: 「the naive policy commit 8012960 existed to make avoidable」, where `801296050` is the `ttl.onlyWhen` filter the ablation removes. - `durability-swallow-repair.test.ts:62`: the flake report became a pointer to the commit that removed the flake (`f1e91595f`), with `objectstack-ai#15603` kept beside it. - `auth-manager.ts:5629`: 「the pre-objectstack-ai#14762 deployment-default behaviour」 became 「the deployment default, as before commit 35e94c9」. ## The 21 sites left - **Test titles (21 sites).** `describe` / `it` titles, which are string tokens: `admin-remove-user-gate-ordering.test.ts:207`, `:263`, `:298` (`objectstack-ai#11477`), `auth-email-locale.test.ts:528` and `auth-manager.test.ts:2545` (`objectstack-ai#14762`), `auth-manager.test.ts:1562` (`objectstack-ai#10366`), `:2866`, `:2880` (`objectstack-ai#11741`), `:4105` and `internal-field-readback.test.ts:219`, `:230`, `:286` (`objectstack-ai#8676`), `auth-plugin-walled-owner-verification-path.test.ts:87`, `:193`, `:317`, `:384` (`objectstack-ai#11640`), `durability-swallow-repair.test.ts:159`, `:567`, `:670` (`objectstack-ai#13398`), `last-admin-standing-keys.test.ts:61` (`objectstack-ai#8734`) and `walled-owner-operator-stamp.test.ts:355` (`objectstack-ai#11343`). Tokens, left as they were, as stages 1 and 2 left theirs. - There is no non-test string, no generated file and no quoted ruling carrying a dead number in this package. ## Mechanical guard: no code token moves The guard compares the TypeScript parser's leaf nodes, with comments as trivia and JSDoc nodes excluded, base `b80ab579d` against head. Template literals are therefore read in context. It ran over all 31 touched `.ts` files. - Real run: 158,646 base tokens, **0 files with a token change** (exit 0). - Comment control in `auth-manager.ts` (`As above — the flagged column` to `Likewise — the flagged column`): 0 files changed, as expected (exit 0). - Positive control, a code token changed in `auth-manager.ts` (a fourth element added to the `fields` projection of the password-reuse read): DIFFER (exit 1). - Positive control, one digit changed inside a kept test title (`admin-remove-user-gate-ordering.test.ts:207`): DIFFER (exit 1). Every mutation went through `scripts/ablation-replace.mjs`, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blob (`c0bdef025a39`, `ec83f09f556e`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/plugin-auth` (`.changeset/20596-plugin-auth-provenance-anchors.md`) is included. It says only that the provenance comments were re-anchored. Measured on the built package (A3): `files[]` is `dist`, `README.md` and `CHANGELOG.md`. After the build, the rewritten comments reach `dist`: `35e94c96b` appears 8 times in each of `dist/index.d.ts`, `index.d.mts`, `index.js` and `index.mjs`; `f8eb73601` twice in each declaration file; `bf8d129b5` and `e238c79f0` once in each of the four; `d6e80b28b` and `4d25d22d4` twice in each runtime file; `c0714eb5d` and `61821e54c` once in each declaration file; `b706af987` once in each runtime file. Positive control: the unchanged line 「read best-effort off the identity row.」 beside a shipped rewrite is found once in `index.d.ts` and once in `index.js`. A never-written negative phrase appears nowhere. No dead number of the 16 is left anywhere in `dist`. ## Gates (head `5ae64e8b8`) - **Citation judging, as CI runs it:** `pnpm check:issue-citations` (self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged 5 citations across 14 files, and all 5 resolve. - **Doc authoring:** `pnpm check:doc-authoring` exits 0, with the sibling-package prose ids at their baseline and no growth. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `5ae64e8b8` derived 65 commands: all 57 derived at dispatch, plus `check:duration-unit-keys`, `check:engine-double-contract`, `check:logger-receiver-detach`, `check:objectql-double-limit`, `check:query-options-erasure`, `check:type-check-coverage`, `check:type-check-debt` and `check:where-matcher`. It was re-derived after a fresh `git fetch` (`origin/main` `a918fe7fd`, 2 commits ahead, neither touching `plugin-auth`): the same 65. Each ran with its exit code captured before any pipe, and all 65 exit 0. `--ran`, fed each command with its exit code, reports 65 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*` and `./packages/*/*` ran first under the shared verify lock (71 of 71 tasks, exit 0), so no gate hit an unbuilt workspace. - **Tests and typecheck, under the verify lock:** - `pnpm --filter @objectstack/plugin-auth test`: 115 files and 2,464 tests pass. That is every test file in the package, the 17 touched ones included. - `pnpm --filter @objectstack/plugin-auth typecheck` exits 0 (`tsc` main, `tsconfig.examples.json`, and `check:test-typecheck` held at its ledger). The main program reads 63 non-test files; the `tsconfig.test.json` program reads all 178 files under `src/`, the 115 test files included, and all 31 touched files are in it (`--listFiles`). - **Lint, as a proven narrowing:** `eslint --no-inline-config --format json` over the 31 touched `.ts` files gives 31 files, 0 errors and 0 warnings. All 31 are in eslint's own population (`isPathIgnored` is false for each). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as its own line 328 states), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of the 32 changed files for control bytes finds none. ## Acceptance notes - **The gate's extractor does not see a hyphen-joined number.** `CITATION_RE` ends in a lookahead that refuses a following hyphen, so `objectstack-ai#13398-class` is not a citation to either the diff gate or the census, dead or alive. This stage rewrote the 5 such sites in `plugin-auth` because they are the same dead number in the same comment prose. At the head, 10 dead `#N-word` sites remain in `packages/**/src` (a raw line scan of `.ts` files against the cached board): `service-automation` 5 (all `objectstack-ai#13398-class`), `rest` 2, `plugin-security` 1, `runtime` 1, `spec` 1. The census cannot count them, so a later stage reaching those packages has to look for them by hand. No instrument change here. - **The census instrument did not truncate in this stage.** Three enumerations read 185 pages each at the newest frontier. - **Anchors the next stages can reuse.** These numbers stand elsewhere on the census at the head: `objectstack-ai#11343` in `plugin-security` (6) and `types` (2), anchor `c0714eb5d`; `objectstack-ai#14902` in `driver-sql` (7) and `cli` (1), anchor `61821e54c`; `objectstack-ai#13398` in `service-automation` (4, plus the 5 hyphen-joined sites), anchor `e238c79f0`; `objectstack-ai#8734` in `core` (2), anchor `f8eb73601`; `objectstack-ai#10165` in `objectql` (2) and `platform-objects` (1), anchor `801296050`; `objectstack-ai#11757` in `platform-objects` (2), anchor `4d25d22d4`; `objectstack-ai#11741` in `plugin-email` (2), anchor `b706af987`; `objectstack-ai#8676` in `platform-objects` (1), anchor `d6e80b28b`. - **Base.** The branch is 2 commits behind `origin/main` (`a918fe7fd`, read at 11:20Z). Neither touches `plugin-auth`, this changeset or any of these 16 numbers, so there was no merge. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… to the commits that decided them (objectstack-ai#20632) Part of objectstack-ai#20594 Clause-②: no ## What changed This is stage 2 of the `domain:cli` lane of the dead-citation sweep: `packages/rest/src/**`. Every comment or docblock site in scope that cited a tracker number answering 404 now cites, in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), the commit in this repository's history that decided what the line describes, and says in its own words what that commit decided. PR objectstack-ai#20533 is the method and PR objectstack-ai#20624 (stage 1, `packages/runtime`) the precedent this follows line for line. Later stages cover `cli`, `types` and the rest of the lane, so this PR says `Part of` and the card stays open. That is **457 comment sites on 445 lines in 85 files, covering 74 numbers**: the census's 191 sites, 256 more in test comments (which the census defers), and 10 sites whose dead number is the second half of a slash-joined pair the citation grammar does not read (`objectstack-ai#3984/objectstack-ai#6241`, `objectstack-ai#9901/objectstack-ai#10255` four times, `objectstack-ai#10993/objectstack-ai#11235/objectstack-ai#11292`, `objectstack-ai#11235/objectstack-ai#11242` twice, `objectstack-ai#10993/objectstack-ai#11242`, `objectstack-ai#7543/objectstack-ai#15071`). Each rewritten line cites one of **70 distinct commits**. ADR-0076 D11 is the only ADR that records any of these numbers, and it records objectstack-ai#8850 only as the extraction it names as landed in `8664a2c99`, so that commit is the anchor there. No other ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records the decision behind any of these numbers, so every anchor is a commit. The anchors the landed stages already gave the same numbers are reused where the rest sites describe the same decision (30 numbers, for example `79c46da90` for objectstack-ai#9934, `7986d973f` / `311433f6b` for the compound-name retirement, `6a180e42d` for objectstack-ai#13279 and `cf6e0a193` for objectstack-ai#15071), so each number carries one anchor across the tree. Only comments changed. Every touched file keeps its line count (451 lines out, 451 in, over 85 files), so no line citation into these files moves. Six of the 451 lines held no dead site; each is the other half of a sentence that had to change: - `discovery-schema-conformance.test.ts:343` (「(reaffirmed by」 to 「(which commits」, because line 344 now names the two commits that landed the ruling), - `package-door-16019-raw-statement-fault-code.test.ts:51` and `error-response.ts:1485` (a trailing 「PR」 whose number wrapped onto the next line), - `error-response-structured-arm-door-parity.test.ts:463` (「That card added the limb」 to 「That commit」, because line 459's tag now names the commit), - `rest-hook-script-fault-envelope.test.ts:331` (「both sides of that card」 to 「that fix」), - `rest-server.ts:908` (「(objectstack-ai#14409, landed」 to 「(landed as commit」, the sha `3ecb7dc1a` already standing on line 909). **No citation number is added.** Every tracker number on an added line was already on the line it replaces. No PR number stands on an added line. One of the 70 shas is on a removed line, and it was there before: `rest-14078-invalid-date-total-arm.test.ts:19` read 「PR objectstack-ai#14409 (landed `3ecb7dc1a`)」 and now reads 「Commit 3ecb7dc drove」. No code token moves (see the guard below). Three dead comment sites are left on purpose, listed under "The sites left". One more file: a `patch` changeset for `@objectstack/rest`, because the rewritten docblocks ship (see Changeset below). ## Census: `packages/rest`, before and after **Instrument.** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged, run with the fleet token. Its surface is comment prose in `packages/**/src/**/*.ts` with string literals blanked, and it defers `*.test.ts`. The count is its `allocated-but-absent` findings under `packages/rest/`. Both runs enumerated the whole board (185 pages), so neither read a truncated board. | reading | tree | board | whole-repo `allocated-but-absent` | rest sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `a186aea996`, run 2026-09-29T10:28:18Z to 10:36:06Z | enumerated, 185 pages, frontier objectstack-ai#20628, 18,455 numbers | 2,015 | **191** | 186 | 14 | 51 | | after | head `93e4d69ba6`, run 11:11:30Z to 11:17:37Z | enumerated, 185 pages, frontier objectstack-ai#20630, 18,457 numbers | 1,764 | **0** | 0 | 0 | 0 | The before count equals the card's 191 at `f11b5f20a2`. The whole-repo drop is 251: this diff's 191, plus the 60 of PR objectstack-ai#20626 (`packages/plugins/plugin-sharing`, 63 to 3), which landed on `main` in between and came in with the merge. No other package moved. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `classifyCitation` over every `.ts` file under `packages/rest/src` (256 files), against the board enumerated through the gate's own `enumerateBoard`. The lit controls objectstack-ai#20594, objectstack-ai#19123 and objectstack-ai#20624 answered 200 and are on both boards; the dead controls objectstack-ai#13214, objectstack-ai#14541 and objectstack-ai#15071 answered 404 and are on neither. | reading | tree | board | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---|---|---| | before, 10:29Z | `a186aea996` | 185 pages, frontier objectstack-ai#20628 | 4,620 | **577** | 191 | 259 | 1 | 126 | | after, 11:21Z | `93e4d69ba6` | 185 pages, frontier objectstack-ai#20631 | 4,174 | **130** | 0 | 3 | 1 | 126 | Its src-comment column equals the census's 191 and 0, which is the control on the second instrument, and a site-by-site comparison of the two before-readings is identical. Resolving comment citations move by one (1,364 to 1,365 in src): `(objectstack-ai#10993/objectstack-ai#11235/objectstack-ai#11292)` became `(objectstack-ai#10993, commit 376c70f, objectstack-ai#11292)`, so the grammar now reads the live `objectstack-ai#11292` that the slash hid. The drop is 447 grammar-read sites; the other 10 rewritten sites are the slash-joined ones the grammar never read. Separately, every one of the 77 numbers was probed on its web endpoint: 76 answer 404 (deleted) and one, #14026, answers 302 to objectstack-ai/objectui#10102 (transferred), which is why it is left (see below). ## Per-number table Sites and files are the dead comment sites in scope at the base, tests and slash-joined halves included. `left` is a site with no deciding commit (see below). `strings kept` counts string-literal sites, which are tokens and stay as they were. Every anchor was read in its message or its diff, not only in its subject: it is the commit that made the change the line describes, and its own message or diff names the number it replaces or adds the citation the line carries. | number | comment sites / files | rewritten | left | strings kept | anchor | |---|---|---|---|---|---| | `objectstack-ai#6037` | 5/3 | 5 | 0 | 0 | `18189983d` | | `objectstack-ai#6122` | 2/2 | 2 | 0 | 0 | `64cd01082` | | `objectstack-ai#6206` | 1/1 | 1 | 0 | 0 | `8e13ca876` | | `objectstack-ai#6216` | 6/2 | 6 | 0 | 2 | `f586f1a89` | | `objectstack-ai#6241` | 10/3 (1 slash-joined) | 10 | 0 | 1 | `83a3b1f2e` | | `objectstack-ai#6259` | 2/1 | 2 | 0 | 0 | `6968885ef` | | `objectstack-ai#6303` | 1/1 | 1 | 0 | 0 | `465c5fc14` | | `objectstack-ai#6306` | 9/5 | 9 | 0 | 3 | `fec784863` | | `objectstack-ai#6307` | 4/2 | 4 | 0 | 0 | `293476148` | | `objectstack-ai#6349` | 4/2 | 4 | 0 | 4 | `2443bb4c4` | | `objectstack-ai#6474` | 1/1 | 1 | 0 | 0 | `18189983d` | | `objectstack-ai#6535` | 3/2 | 3 | 0 | 0 | `a92b1793c` | | `objectstack-ai#6640` | 1/1 | 1 | 0 | 1 | `2ab1257c9` | | `objectstack-ai#6704` | 5/1 | 5 | 0 | 1 | `c3f491626` | | `objectstack-ai#8641` | 1/1 | 0 | 1 | 0 | — | | `objectstack-ai#8850` | 3/3 | 3 | 0 | 0 | `8664a2c99` | | `objectstack-ai#8885` | 6/3 | 6 | 0 | 3 | `30b1c636a` | | `objectstack-ai#8919` | 7/3 | 7 | 0 | 7 | `b5378550e` | | `objectstack-ai#9741` | 12/1 | 12 | 0 | 0 | `2a29caa53` | | `objectstack-ai#9805` | 1/1 | 1 | 0 | 0 | `45862a53d` | | `objectstack-ai#9934` | 19/10 | 19 | 0 | 4 | `79c46da90` | | `objectstack-ai#9967` | 2/2 | 2 | 0 | 4 | `8f266f1cd` | | `objectstack-ai#10063` | 2/2 | 2 | 0 | 1 | `9e04c3e35` | | `objectstack-ai#10178` | 1/1 | 1 | 0 | 0 | `38cf397ea` | | `objectstack-ai#10179` | 0/0 | 0 | 0 | 1 | | | `objectstack-ai#10255` | 18/4 (4 slash-joined) | 18 | 0 | 2 | `6ce58a735` | | `objectstack-ai#10340` | 13/3 | 13 | 0 | 2 | `26f3588fb` | | `objectstack-ai#10345` | 13/6 | 13 | 0 | 6 | `cad8b42f0` | | `objectstack-ai#10350` | 1/1 | 1 | 0 | 0 | `490879ad0` | | `objectstack-ai#10485` | 2/1 | 2 | 0 | 1 | `35ad101bc` | | `objectstack-ai#10537` | 9/3 | 9 | 0 | 1 | `e634ecf6a` | | `objectstack-ai#10888` | 2/2 | 2 | 0 | 0 | `d806081dd` | | `objectstack-ai#11006` | 3/1 | 3 | 0 | 0 | `cccbe51bf` | | `objectstack-ai#11130` | 1/1 | 1 | 0 | 0 | `851909530` | | `objectstack-ai#11235` | 4/2 (1 slash-joined) | 4 | 0 | 0 | `376c70f98` | | `objectstack-ai#11242` | 3/2 (3 slash-joined) | 3 | 0 | 0 | `98ea3443f` | | `objectstack-ai#12144` | 1/1 | 1 | 0 | 0 | `3a04b0125` | | `objectstack-ai#12176` | 11/7 | 11 | 0 | 2 | `7986d973f` | | `objectstack-ai#12194` | 15/5 | 15 | 0 | 4 | `311433f6b` | | `objectstack-ai#12195` | 35/16 | 35 | 0 | 7 | `7986d973f` | | `objectstack-ai#13182` | 2/2 | 2 | 0 | 0 | `5b3ff63cc` | | `objectstack-ai#13197` | 1/1 | 1 | 0 | 0 | `56c093c4d` | | `objectstack-ai#13213` | 2/1 | 2 | 0 | 0 | `4801296e7` | | `objectstack-ai#13214` | 18/6 | 18 | 0 | 14 | `cc837dbfe`, `889ec5b42`, `3d10755f0` | | `objectstack-ai#13244` | 5/2 | 5 | 0 | 1 | `889ec5b42` | | `objectstack-ai#13255` | 4/1 | 4 | 0 | 6 | `43028a8f8` | | `objectstack-ai#13258` | 1/1 | 1 | 0 | 0 | `3d10755f0` | | `objectstack-ai#13279` | 23/5 | 23 | 0 | 5 | `6a180e42d` | | `objectstack-ai#13280` | 13/4 | 13 | 0 | 2 | `add6a1b1c` | | `objectstack-ai#13282` | 1/1 | 1 | 0 | 0 | `43028a8f8` | | `objectstack-ai#13377` | 3/2 | 3 | 0 | 0 | `e10cf3444` | | `objectstack-ai#13378` | 2/1 | 2 | 0 | 0 | `82faea03f` | | `objectstack-ai#13454` | 1/1 | 1 | 0 | 0 | `7ad57e17a` | | `#14026` | 1/1 | 0 | 1 | 0 | — | | `objectstack-ai#14365` | 1/1 | 0 | 1 | 0 | — | | `objectstack-ai#14366` | 14/4 | 14 | 0 | 2 | `53cbad9f7` | | `objectstack-ai#14369` | 3/2 | 3 | 0 | 0 | `a3d5724c8`, `53cbad9f7` | | `objectstack-ai#14389` | 7/3 | 7 | 0 | 7 | `10220a7bf` | | `objectstack-ai#14390` | 1/1 | 1 | 0 | 0 | `9d7f7259f` | | `objectstack-ai#14409` | 2/2 | 2 | 0 | 0 | `3ecb7dc1a` | | `objectstack-ai#14541` | 27/4 | 27 | 0 | 5 | `6d178a408` | | `objectstack-ai#14613` | 2/2 | 2 | 0 | 0 | `81208086a` | | `objectstack-ai#14677` | 1/1 | 1 | 0 | 0 | `a4e4d2d78` | | `objectstack-ai#14683` | 8/2 | 8 | 0 | 0 | `96326040f` | | `objectstack-ai#14691` | 15/2 | 15 | 0 | 2 | `b3a63d32c` | | `objectstack-ai#14704` | 9/3 | 9 | 0 | 2 | `1c7adc73d` | | `objectstack-ai#14723` | 7/4 | 7 | 0 | 4 | `65846bc46` | | `objectstack-ai#14725` | 3/3 | 3 | 0 | 2 | `f5cc78b63` | | `objectstack-ai#14849` | 3/1 | 3 | 0 | 0 | `226e72443` | | `objectstack-ai#14907` | 1/1 | 1 | 0 | 0 | `e1d4f9e3f` | | `objectstack-ai#14908` | 1/1 | 1 | 0 | 0 | `d5cbb44f3` | | `objectstack-ai#15021` | 2/1 | 2 | 0 | 8 | `cc238db8b` | | `objectstack-ai#15034` | 6/2 | 6 | 0 | 0 | `abf9101f1` | | `objectstack-ai#15065` | 1/1 | 1 | 0 | 0 | `1c7adc73d` | | `objectstack-ai#15071` | 23/4 (1 slash-joined) | 23 | 0 | 3 | `cf6e0a193` | | `objectstack-ai#16650` | 1/1 | 1 | 0 | 0 | `001a83b04` | | `objectstack-ai#17058` | 3/1 | 3 | 0 | 4 | `94c930248` | | `objectstack-ai#18546` | 3/2 | 3 | 0 | 3 | `58f60e37e` | | **total** | **460** | **457** | **3** | **127** | **70 distinct commits** | Every cited sha matches exactly one object (`git rev-parse --disambiguate`, count 1 for each of the 70), is a commit, has one parent, and is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 70). The checkout is not shallow (`--is-shallow-repository` false); the control leg `13a6cb4ad` exits 0 and the negative control (this branch's first WIP commit, not on `main`) exits 1. Several numbers are the PR number of their own anchor commit (objectstack-ai#6122, objectstack-ai#6303, objectstack-ai#6474, objectstack-ai#11242, objectstack-ai#13213, objectstack-ai#13244, objectstack-ai#13258, objectstack-ai#13282, objectstack-ai#14409, objectstack-ai#14677, objectstack-ai#14908, objectstack-ai#15065, objectstack-ai#16650), so the sha is the same object the number named. **Numbers with more than one anchor, by site:** - `objectstack-ai#13214` (18 sites) was one card with three commits. `cc837dbfe` (the ownership gate, the 2026-08-30 ruling) for the 11 sites that describe the gate; `889ec5b42` for the 5 in `ui-view-route-identity.measurement.test.ts`, the identity measurement it created; `3d10755f0` for the tenancy file's header, the measurement it created; and `rest-server.ts:2247`, 「Driven and reported on objectstack-ai#13214 (PRs objectstack-ai#13244, objectstack-ai#13258)」, now reads 「Measured in commits 889ec5b (identity) and 3d10755 (tenancy)」: those PRs are exactly those two commits. - `objectstack-ai#14369` (3 sites): `a3d5724c8` (the liveness census it recorded) for `rest-server.ts:1172` and `rest-sub-config-parse-not-cast.test.ts:48`. `rest-server.ts:4092` said the zero read sites of `api.documentation` / `api.responseFormat` came from 「the objectstack-ai#14369 census」, but `a3d5724c8` explicitly left `api` out of that census; the zero was measured by `53cbad9f7` (its changeset: no other read site for either key), which is the anchor there. - `objectstack-ai#11235` / `objectstack-ai#11242` / `objectstack-ai#10993`: `376c70f98` derives the discovery `version` in metadata-protocol (objectstack-ai#11235), and `98ea3443f` is objectstack-ai#11242's own squash, which landed the objectstack-ai#10993 ruling on `/health` and the dispatcher's `/discovery`. So 「the objectstack-ai#10993 ruling … reaffirmed by objectstack-ai#11235/objectstack-ai#11242」 now reads 「the objectstack-ai#10993 ruling, landed by commits 98ea344 and 376c70f」 (`rest-server.ts:4528`, `discovery-schema-conformance.test.ts:343-344`). `objectstack-ai#10993`, `objectstack-ai#11292` and `objectstack-ai#11297` answer 200 and stay. - `objectstack-ai#6037` / `objectstack-ai#6474`: one commit, `18189983d` (objectstack-ai#6474 is its PR number), so 「(objectstack-ai#6037 / PR objectstack-ai#6474)」 became 「(commit 1818998)」. **Wordings to check, each true of its commit:** - A commit does not rule. Where a line said a number ruled, it now says what the commit did with the ruling: 「the ruling commit 79c46da landed says it does」, 「the ruling commit cf6e0a1 implemented fences it」, 「the ruling commit 10220a7 implemented」, 「the 2026-08-20 ruling, landed as commit 6ce58a7」, 「recorded in commit 6ce58a7's message (option A)」 (its message reads 「Ruled on objectstack-ai#10255 (2026-08-20, option A)」), and 「question was ruled on 2026-08-20 and landed as commit 6ce58a7」 where the line said 「filed as objectstack-ai#10255」. - `objectstack-ai#14541`'s contract review: 「the objectstack-ai#14541 contract review (condition N)」 now reads 「the contract review of commit 6d178a4 (condition N)」; that commit's message lists the conditions it carries. 「objectstack-ai#14541's §4」 and 「objectstack-ai#14541 §5」 in `error-response-generic-passthrough-object-parity.test.ts` are sections of `error-response-structured-arm-door-parity.test.ts` (the file `6d178a408` created), so they now name that file. 「measured on the objectstack-ai#14541 branch」 reads 「on the branch that landed as commit 6d178a4」. - A line that named a DEFECT by its number now says so: 「Before commit 9e04c3e the draft→active promotion door could not…」, 「Before commit 26f3588 the `/meta` doors decided ORGANIZATION SCOPE from the RAW url」, 「the defect commit 2443bb4 fixed」 and 「would be the defect commit 26f3588 fixed」. - `objectstack-ai#13255`: 「As written for objectstack-ai#13255 this file repaired nothing」 reads 「As first written (commit 43028a8)」, the commit that created the file and answered the measurement; 「CONTEXT-LOST family (objectstack-ai#13255), still unruled」 reads 「first measured by commit 43028a8」 (the ruling on that family never landed, which the line still says). - `objectstack-ai#13214` in the identity file: 「the half objectstack-ai#13214 marks UNMEASURED」 reads 「the half left UNMEASURED until commit 889ec5b」, and 「objectstack-ai#13214 asks for an INDEPENDENT reproduction」 reads 「commit 889ec5b is an INDEPENDENT reproduction」. - 「the objectstack-ai#8885 sweep」 reads 「the sweep behind commit 30b1c63」, the commit that registered the 9 codes the sweep found; 「objectstack-ai#14849 predicted」 reads 「The card behind commit 226e724 predicted」; 「the hazard objectstack-ai#13377 names」 reads 「the hazard commit e10cf34 was written to remove」; 「The concrete harm objectstack-ai#6704 names」 reads 「removed」. - Quoted ruling: `error-response-sandbox-arm-message.test.ts:340` sits inside a verbatim ruling quote, so the commit stands in an editorial bracket (「not from [commit 1c7adc7]'s list」), as PR objectstack-ai#20624 did. - Two markdown tables in comments (`meta-state-route-engine-outage.test.ts:76`, `objectql-slot-consumer-census.test.ts:43`): the rewritten cell is wider than its column, and its padding is reduced rather than widening the four sibling rows. ## The sites left **No deciding commit (3 sites, all in test files, so the census does not see them):** - `meta-object-owd-gate.test.ts:516` (objectstack-ai#8641): 「whether it should stay is objectstack-ai#8641's question」, an open decision. The commit that added the citation calls it a pointer to the open decision card, and no commit decides it. - `rest-sub-config-parse-not-cast.test.ts:321` (objectstack-ai#14365): the `z.partialRecord` question 「deferred to objectstack-ai#14365」 was never taken (`git log -S partialRecord`); `b3a63d32c` made it moot by retiring the record, which the other half of the same line now cites. - `import-integration.test.ts:1043` (#14026): not deleted, TRANSFERRED. The web endpoint answers 302 to objectstack-ai/objectui#10102, the REST read follows the redirect, and the board enumeration does not list it, so the census and the supplementary reading both class it `allocated-but-absent`. The line says how an issue was raised; no commit decides that, so form C has nothing to cite. **String sites kept as tokens (127).** 126 are test titles and test-code strings in 41 files. One is a non-test string: the `note` field of the REST route ledger's `GET /api/v1/meta/object/:name/state/:field` row at `rest-route-ledger.ts:290`, which ends 「(objectstack-ai#10179)」 (see Acceptance notes). ## Mechanical guard: no code token moves The check compares the TypeScript parser's leaf tokens (TypeScript 6.0.3, JSDoc nodes excluded, comments being trivia) of each touched file at base `a186aea996` against the working tree at `93e4d69ba6`, over all 85 touched `.ts` files. Controls mutate the head text in memory only, so nothing on disk moved for them. - Real run: 272,653 base tokens, **0 files with a token change** (exit 0). - Comment-insertion control (`error-response.ts`): 0 files changed (exit 0). - Code-insertion positive control (a declaration in the same file): DIFFER at token 0 (exit 1). - String positive control (the first string literal past offset 2000 of the same file, one character added inside it): DIFFER at token 26 (exit 1). Line balance: every touched file is +N/−N (451/451), and every line count is equal at base and head. A raw scan of the 86 changed files for control bytes finds none (its positive control on a scratch file with a U+0001 byte matches). ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/rest` is included, in PR objectstack-ai#20624's form and level. It says only that the provenance comments were re-anchored. Measured on the built package: `files[]` is `dist`, `README.md` and `CHANGELOG.md`. After `pnpm --filter @objectstack/rest build`, the rewritten docblocks reach `dist`: for example `53cbad9f7` appears 4 times in `dist/index.d.ts`, and `26f3588fb` 8 times and `b3a63d32c` 5 times in `dist/index.js`. The positive control, the unchanged sentence 「It was VALIDATE-ONLY from objectstack-ai#11637」 of the same `rest-server.ts` docblock whose first line now reads 「[commit 53cbad9] The parsed output is CONSUMED」, is in `dist/index.d.ts` beside it; a negative control phrase appears nowhere. ## Gates (head `93e4d69ba6`) This host has no `flock`, so `os-verify-lock.sh` ran in its declared unlocked mode. Its disclosure, verbatim, from each locked run at this head: ```text os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 47s · declare it in the PR body · pnpm --filter '@objectstack/rest...' build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 102s (1m42s) · declare it in the PR body · pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=4 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 76s (1m16s) · declare it in the PR body · pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 9s · declare it in the PR body · pnpm --filter @objectstack/rest typecheck ``` The branch merged `origin/main` once (`93e4d69ba6`, merging `542670da6d`) before these runs, as the dispatch orders; `origin/main` has not moved since (read at 11:19Z). The merge brought PR objectstack-ai#20626 and PR objectstack-ai#20587 and touched none of this diff's files. The dependency closure was built first (`pnpm --filter '@objectstack/rest...' build`, 26 packages), then the whole workspace (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 71 tasks, 71 successful). - **Tests:** `vitest run --project local`: 227 files, 4,382 tests passed, 50 skipped. `--project repo` (which holds the touched `meta-state-route-doc-spelling.test.ts`): 1 file, 8 tests passed. Together they are all 228 test files of the package, so every touched test file ran. - **Typecheck:** `pnpm --filter @objectstack/rest typecheck` exits 0. `tsc --listFiles` counts 28 `src` files (no tests) under `tsconfig.json` and all 228 test files under `tsconfig.test.json`, which `check:test-typecheck` judges: 0 files, 0 errors, 0 pinned signatures in the ledger. - **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`) exits 0 at `93e4d69ba6` (2026-09-29T11:19:30Z to 11:20:00Z). Not narrowed. - **Citation judging:** `node scripts/check-issue-citations.mjs --base origin/main` exits 0: 19 citations judged across 14 files (18 resolve, 1 resolves as a pull request). These are the live numbers that stay on rewritten lines. It defers `*.test.ts`, so the added-minus-removed count over the whole diff covers the rest: 0 numbers added. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `93e4d69ba6` derived 68 families. All 68 exit 0, and `--ran` over a record carrying each exit code reads 「68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero). - `check:dual-build-cjs-loads` and `check:type-check-debt` first exited 3 (PREREQUISITE NOT MET, nothing measured) on the closure-only build; after the whole-workspace build both exited 0. - Among them: `check:doc-authoring`, `check:nul-bytes`, `check:rest-log-declared`, `check:route-envelope`, `check:system-context-census` (106 elevation read sites, the page's 102 symbols held) and `check:issue-citations` (self-test). - **Artifact rosters:** 33 of the 36 non-self-test roster rows exit 0 at `93e4d69ba6`, `check-changeset-fixed` (the one whose roster sits under `.changeset/`) and `check:route-ledger-census` among them. The other three, `check-closing-target-claim`, `check-partof-closing-keyword` and `check-single-claim-paths`, answer 「NOT WIRED」 (exit 2) without a pull request's context; they are run against this PR once it exists and reported on the card. ## Hypotheses (measured first) - **H0 holds.** The filtered census answers 191 dead sites at `a186aea996` (186 lines, 14 files, 51 numbers), equal to the card's count at `f11b5f20a2`: no net drift, although PR objectstack-ai#20601 (merged as `eb4b17c346`, before this base) touches four files in `packages/rest`. - **H1 holds.** After the rewrite the filtered census answers 0. The supplementary reading leaves 3 test-comment sites, the three listed above: an open decision, an untaken option and a transferred issue, none with a deciding commit. No site was held for an open PR: the claim's read and this stage's two reads of the open PRs' file lists (10:27:35Z, 7 open PRs; 11:30:34Z, 8 open PRs) found none touching `packages/rest`. - **H2 holds, by the token guard.** A comment-stripped comparison of every touched file (the parser's leaf tokens, JSDoc excluded) is empty, and its code and string controls fire. The emitted `dist` is not byte-identical, because the docblocks ship, which is why the changeset is `patch`. ## Acceptance notes - **Form D, not touched here.** 127 dead numbers stand inside string literals: 126 in test titles and test-code strings, and one in the `note` of the REST route ledger's legal-next-state row (`rest-route-ledger.ts:290`, 「(objectstack-ai#10179)」), which is ledger data, not an author-shown refusal. Ruling D (no number, the lesson in words) is a string change outside this comment-only scope; the card already carries a form-D stage for the lane. - **A transferred issue among the 404s.** #14026 answers 302 to objectstack-ai/objectui#10102 on its web endpoint. The census classes it `allocated-but-absent` (deleted and transferred are only told apart under `--probe-cause`), and `scripts/check-issue-citations.mjs`'s header says the `transferred` arm has no positive specimen on this tree; this is one. Noted, not filed. - **The grammar does not read a slash-joined number.** `CITATION_RE` refuses a `#` preceded by `/`, so the second number of `#A/#B` is never judged. In `packages/rest/src` six such dead numbers stood at 10 comment sites, all rewritten here; one more, `objectstack-ai#14389` in `objectstack-ai#14095/objectstack-ai#14389`, stands inside a string (`error-response-structured-arm-door-parity.test.ts:187`) and is kept. The same shape PR objectstack-ai#20624 and PR objectstack-ai#20612 reported. Noted, not filed. - **Outside the scope and the census surface.** `packages/rest/vitest.config.ts:21` cites objectstack-ai#17853, which answers 404; `packages/rest/test-typecheck-debt.json`, written by `gen:test-typecheck-debt`, carries objectstack-ai#13470, objectstack-ai#13454, objectstack-ai#13377 and objectstack-ai#13378 in its prose, all 404. Neither is under `src/**`. The other numbers in `vitest.config.ts`, `tsconfig.json` and `tsconfig.test.json` answer 200. - **Two comments stale on their own, not touched.** The anchor research found `rest-server.ts`'s `api` docblock near `:1115` and the 「zero read sites」 sentence at `:4092` both overtaken by `80153f5a4`, whose own acceptance notes record it. This PR re-anchors their citations and leaves their claims alone. - **An attribution corrected by the anchor.** `rest-server.ts:4092` credited its zero-read-site count to 「the objectstack-ai#14369 census」, which (`a3d5724c8`) excluded `api`; it now cites `53cbad9f7`, the commit that measured it. - **Base.** One merge of `origin/main` (`93e4d69ba6`) before the `--base origin/main` run, as the dispatch orders. ## Deviations - Ten sites beyond the census's read grammar carry a slash-joined dead number and are rewritten; six more lines are the other half of a rewritten sentence (listed under What changed). - The whole-workspace build ran with `--concurrency=4`, not 2, to stay inside the ten-minute foreground cap on this host; it took 1m42s. - Anchor research for 33 of the 77 numbers ran in three read-only research subagents; every proposal was verified here against the commit's message or diff, and the wording of each changed line was reviewed and corrected by hand in a second pass. - Commit trailers are AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`), and the pre-push trailer check passed on every push. The merge commit carries git's default message. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ --------- Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Part of #20233
Stage 9: the remaining semantic-entry families, meaning every ADR-0087 semantic entry that still cited a tracker number.
Clause-②: no
os migrate metaprints each ADR-0087 semantic entry'ssurface,replacement,reasonandacceptanceCriteriato the author. This stage covers the 44 entries that still cited a tracker, pull-request, decision-batch or cross-repository number: 39 with a four- or five-digit id, and 5 with a decision-batch number only. In each of them, every sentence now says what the cited ruling, measurement or fix decided (form D). ADR ids stay, and so do the contributor-guide rule references, which are not tracker ids. Theaddress-location-value-unknown-keys-refusedreplacement citedAGENTS.md #0.1, which names nothing in AGENTS.md today. It now states the rule it copied from the 2026-09-01 ruling: a consumer-side alias for an off-spec key stays forbidden.registry.tsfinds no change outsidereplacement/reason/acceptanceCriteria. That is 48 prose fields per copy, and nosurface, id, comment, import or token-skeleton change.682873f201has 76 prose sites in 39 entries (replacement 3 / reason 73 / acceptanceCriteria 0), 0 surface sites and 28 short numbers. Head has 0 / 0 / 10. The 10 short numbers left are contributor-guide rule references.packages/cli/test/migrate-meta-engine-guidance.test.tsnow holds every semantic entry instead of a prefix list, so an entry added later in any family is held on arrival.REWRITTENgoes from 203 to 247. Ablation: putting one removed id back into theturso-entry (a family the pin did not cover before) turned the pin red; restoring it turned the pin green.registry.ts,spec-changes.jsonanddocs/protocol-upgrade-guide.md, by their generators.patchchangeset.Verification (head
96b994e472)--project local: 575/575 files (16917 passed, 1 todo).--project repo: 42/42 files (745 passed).--project unit: 234/234 files (3342 passed).--project integration, the three migrate-meta files: 3/3 (13 passed, 1 skipped by the default-range file's ownskipIf).dispatch-gates.mjs --commandsderives 89 families. 88 exit 0, among themcheck:doc-authoring,check:generated,check:migration-registry,check:spec-changes,check:upgrade-guide,check:issue-citations,check:nul-bytes,check:api-surfaceandcheck:authorable-surface.check:dual-build-cjs-loadsis NOT MEASURED: its prerequisite is not met, because packages outside the CLI closure have nodist/. Build Core runs it.--ranreconciliation: 89 accounted, 88 run, 1 NOT MEASURED.merge-treeontoorigin/mainb80ab579d8is clean. Main's new commits touch none of these paths.The census method and controls, the source of every citation, and the ablation record are in the dev report on #20233.
Acceptance notes
main: they arrive with PR feat(spec)!: $empty joins FILTER_OPERATORS, and is_empty / is_not_empty lower to it (#20446) #20570 and PR feat(spec)!: retire the connector triggers array — the ConnectorTrigger shape nothing registered, polled or received (#20287) #20587. The two onmaincarry no site. PR feat(spec)!: $empty joins FILTER_OPERATORS, and is_empty / is_not_empty lower to it (#20446) #20570's incomingfilter-is-empty-lowers-to-empty-operatorcarries six tracker ids inreason, and thefilter-family was already held by this pin before this stage.ui-list-view-groupbyfield-padded-refusedandui-list-view-grouping-field-padded-refusedstill citeAGENTS.md #0.1inreason. That is the same dangling number theaddress-entry had. Both entries are outside this stage's claim and are untouched.ui-notification-action-embed-config-retirednames "its ui/ batch 14" of the strictness sweep, a batch number without#. It is outside this stage and untouched.semantic/, 839 acrossentries/. They are form C's, under packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234, and are untouched.Generated by Claude Code