Skip to content

docs(service-storage): re-anchor the dead tracker citations to the commits that decided them - #20708

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20596-service-storage-citations
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20596-service-storage-citations

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20596
Clause-②: no

What changed

This is the sixth stage of the domain:services lane of the dead-citation sweep. It covers packages/services/service-storage/src/** and nothing else. By the seat's census at the claim (5896394242), it is the largest package in the lane that no in-flight work holds. 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 #19123), by the method of stages 1 to 5 (PR #20609 as 422db788a, PR #20626 as b80ab579d, PR #20634 as 4d04b6be3, PR #20658 as 9a4b2bb38, PR #20693 as 0e9ad74fb). That is 42 sites on 41 lines in 15 files, covering 8 numbers:

  • 27 census sites (every census site this package has);
  • 15 sites in test comments, which the census defers.

Each rewritten line now cites the commit in origin/main history that decided what the line describes, and says in its own words what was decided: 7 distinct shas. No number in this package has an ADR or ruling record of its own in the repository (a grep of docs/adr/ for all 8 finds none, and the repository keeps no other ruling-record file for them), so every anchor is a commit, per ruling C's order. No number was dropped.

Only comments changed. Every touched source file keeps its line count (43 lines out, 43 in, over 15 files), so no line citation into these files moves. 2 of those 43 lines hold no dead citation; they are reflow, 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: #12069 (translations/index.ts:29), #10246 (storage-service-plugin.ts:392) and the cross-repo cloud#1395 (backfill-sys-file-organizations.ts:86). Over the whole diff, added minus removed is 0 or negative for every number, and no number is new to the diff. No PR number stands on an added line.

Eleven dead sites are left on purpose, all of them test titles (see the list below).

One more file: a patch changeset for @objectstack/service-storage, because the rewritten docblocks and inline comments ship (see Changeset below).

Census: service-storage, 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/services/service-storage/. 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 service-storage sites lines files numbers
before base 31ed06763, run 2026-09-29T18:42:47Z to 18:46:12Z enumerated, 186 pages, frontier #20702 (newest #20702 before and after), 18,529 numbers 1,254 27 27 8 8
after head 5db5155a2, run 18:55:34Z to 18:58:50Z enumerated, 186 pages, frontier #20702 (newest #20702 before and after), 18,529 numbers 1,227 0 0 0 0

The before count matches the seat's census at the claim (27 sites in 8 files, at 6bff748b). The whole-repo drop is 27, exactly this diff's census sites. The resolves tally is 32,967 in both runs, and resolves-as-pull-request (1,984) and cross-repo-unjudged (994) did not move either. The after run was taken on 5db5155a2; the head 09d2ecc96 adds only the changeset. No run was truncated or discarded: both enumerations read 186 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 namesThisRepository over every .ts file under service-storage/src (71 files). It takes its verdicts from the before census's own board reading rather than from a second enumeration: a number is dead when that census reported it allocated-but-absent, and alive when that census judged it on this board anywhere (its --list extraction, 4,943 numbers) and did not report it. The three numbers the census never saw, because they stand only in test files (#13996, #15607, #17571), were read one by one on the issues endpoint, and each answers 200.

reading citations dead src comment test comment src string test string
before, 31ed06763 608 53 27 15 0 11
after, 5db5155a2 566 11 0 0 0 11

Its src-comment column equals the census's 27, which is the control on the second instrument. The 554 live citations and the 1 cross-repo citation are the same in both readings, and the drop of 42 citations is exactly the rewritten sites. A third, raw reading (every # followed by 2 to 6 digits, whatever surrounds it) finds 53 dead occurrences before and 11 after, and its residue equals the gate's residue site for site.

Per-number table

Sites and files count every dead occurrence 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, and git blame at the base puts each rewritten line in that commit or in a later one that applied it.

number sites / files rewritten / left anchor: what it decided
#13178 19/5 14/5 f087c376f: the sys_file / sys_upload_session update and delete doors take the acting organization and scope the statement to it (they stamp nothing), and the upload routes bind the session they had resolved and discarded. New to the sweep
#13279 11/4 11/0 6a180e42d: a failed permission-store read raises AuthzStoreUnavailableError (503) instead of reading as zero grants, and the transports' fail-closed nets, this package's file-read authorizer among them, re-raise it. The anchor of stages 2 and 5 and of the rest, runtime and types stages
#10091 9/3 5/4 da891e0ef: sys_attachment beforeUpdate gated by the uploader-or-parent-editor rule, the attach rule on a re-point, and the update-verb refusal of an unscoped multi-update. New to the sweep
#11427 6/3 4/2 c3c72a4bc: record file-field hydration asks the reap guard's held-file question, through the batched findHeldFiles this package adds, so hydration and the download path agree about a tombstoned sys_file. Its message ends with a reference to #11427. New to the sweep
#6206 3/2 3/0 aa4b90d9a: the full-envelope ruling applied to the sharing contract; ISharingService takes the whole ExecutionContext, and its docblock says callers "MUST NOT rebuild a subset of it". Stage 2's anchor, named there as the full-envelope ruling
#6523 3/2 3/0 aa4b90d9a: the same commit, which was #6523's change (its subject names it). Stage 2's and the spec stage's anchor
#8778 1/1 1/0 7901b2dd2: stamp-only tenancy.organizationField, with its consumers scope-pinned by the maintainer's ruling (the pin text is in its diff). The spec and plugin-security stages' anchor
#11671 1/1 1/0 09b4f4e4e: the source-hashes provenance companion. The identical translations/index.ts line in service-messaging, plugin-sharing and plugin-security already cites it

Every cited sha matches exactly one commit (git rev-parse --disambiguate, count 1 for each of the 7), and every one is an ancestor of the base (merge-base --is-ancestor, exit 0 for all 7; the history is complete, --is-shallow-repository false, 15,120 commits). Each of the 8 numbers answers 404 on the issues endpoint, read one by one before the rewrite.

Wordings to check

The 11 sites left

  • Test titles, 11 sites. describe / it titles, which are string tokens, left as stages 1 to 5 left theirs: attachment-access-hooks.test.ts:232, :314, :640, :932 (#10091); tenant-audit-update-delete-half-repairs.test.ts:151, :224, :345, :552, :664 (#13178); tombstone-hydration-download-agreement.test.ts:148, :326 (#11427).
  • There is no operator string, assertion message, generated header or quoted ruling carrying a dead number in this package. The generated *.source-hashes.generated.ts headers are untouched and carry none. The verbatim maintainer quotations in scope (5 lines: 「同意」 three times, 「12745 A回,其他同意。」 and 「批 Remove explicit pnpm version from workflows to fix version conflict #7 同意」) carry no dead number and are untouched.

Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as trivia and JSDoc nodes never visited, base 31ed06763 against head. Template literals are therefore read in context. It ran over all 15 touched .ts files.

  • Real run: 20,143 base leaf tokens, 0 files with a token change (exit 0).
  • Comment control in storage-routes.ts (Bound, not discarded to Bound and not discarded): 0 files changed, as expected (exit 0).
  • Positive control, a code token added in storage-routes.ts (const { fileId, eTag } = req.body ?? {}; given a trailing ?? undefined): DIFFER (exit 1).
  • Positive control, one digit changed inside a kept test title (tombstone-hydration-download-agreement.test.ts:148): 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 (44ecc8e64ae0, ee84cf718a6f), with git diff HEAD empty and a clean tree afterwards.

Changeset

This change ships bytes, so a patch changeset for @objectstack/service-storage (.changeset/20596-service-storage-provenance-anchors.md) is included. It says only that the provenance comments were re-anchored, in stage 5's words.

Measured on the built package (A3): files[] is dist, README.md and CHANGELOG.md. After the build, the rewritten comments reach dist: f087c376f 6 times and da891e0ef once in each of dist/index.d.ts and index.d.cts; f087c376f 4 times and da891e0ef once in each of index.js and index.cjs. Positive controls: the unchanged line 「the parent record — the delete rule, applied to the verb that could」 beside the shipped rewrite at attachment-access-hooks.ts:28 is found once in each declaration file, and the unchanged line 「standard catalog code — the same both-verbs pairing the derived」 beside the shipped rewrite at :470 once in each JS file. A never-written negative phrase appears nowhere in dist. None of the 8 dead numbers is left anywhere in dist.

Gates (head 09d2ecc96)

  • 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 3 citations (#12069 and #10246 resolve; cloud#1395 is cross-repo), each already on the line it replaces.
  • Doc authoring: pnpm check:doc-authoring exits 0.
  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 09d2ecc96 derived 65 commands: all 56 derived at dispatch, plus check:duration-unit-keys, check:dispatcher-error-vocabulary, 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. 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.
  • Roster families the derivation lists outside its commands (their rosters sit in directories this diff touches): node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing and pnpm check:filter-alias-parity, each exit 0.
  • Tests and typecheck, under the verify lock:
    • pnpm --filter @objectstack/service-storage test: 40 files pass and 627 tests pass. That is every test file in the package, the 7 touched ones included.
    • pnpm --filter @objectstack/service-storage typecheck exits 0 (tsc on tsconfig.json, the scripts program, and the test layer on tsconfig.test.json). --listFiles on both tsconfig.json and tsconfig.test.json shows all 71 files under src/, the 40 test files included, and all 15 touched files in the program.
  • Lint, as a proven narrowing: eslint --no-inline-config --format json over the 15 touched .ts files gives 15 files, 0 errors and 0 warnings. All 15 are in eslint's own population (isPathIgnored is false for each; a dist file, as the control, is ignored). eslint.config.mjs never enables type-aware linting (no parserOptions.project, as its own lines 327-328 state), 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 16 changed files for control bytes finds none.

Acceptance notes

  • The gate-invisible spellings, grepped as the claim asked. CITATION_RE refuses a hyphen after the digits and a / before the # (check-issue-citations closeout (extractor spellings): CITATION_RE refuses a hyphen after the digits, so a dead #N-word citation (#13398-class) is invisible to the diff gate and to the census #20636). In this package there is no #N-word spelling at all. There are 11 #A/#B lines carrying 13 second numbers (attachment-access-hooks.ts:215, :217, :434; attachment-access-hooks.test.ts:217; attachment-lifecycle.ts:177; file-reference-lifecycle.test.ts:226; local-storage-adapter.test.ts:35; metadata-store.test.ts:41; storage-route-ledger.ts:74, which chains four; storage-routes.metadata-outage.test.ts:67; tombstone-download-live-reference.test.ts:50), and every second number on them is live: #5574, #9974, #5541, #5480, #3833 and #3847 by the census's own board, and #5197 and #3870 read one by one (200). So nothing there needed rewriting. The claim counted 12 such spellings on main; this reading is 11 lines and 13 second numbers, with nothing dead among them either way. The raw scan above, which sees both spellings, agrees.
  • The census instrument did not truncate in this stage. Both enumerations read 186 pages at the newest frontier.
  • Anchors the next stages can reuse, each checked here: #13178 → f087c376f; #10091 → da891e0ef; #11427 → c3c72a4bc; #13279 → 6a180e42d; #6206 / #6523 → aa4b90d9a; #8778 → 7901b2dd2; #11671 → 09b4f4e4e.
  • Base. The branch is 4 commits behind main (defc7f7b5, read at 19:31Z). None touches service-storage, scripts/check-issue-citations.mjs or .changeset/config.json, so there was no merge.

Generated by Claude Code

…mmits that decided them

42 comment and docblock sites under packages/services/service-storage/src
cited tracker numbers that answer 404. Each now cites the commit in this
repository's history that decided what the line describes, and says in its
own words what that commit decided: the sys_attachment beforeUpdate gate
(da891e0), the full-envelope ruling on the sharing contract (aa4b90d),
batched held-file hydration (c3c72a4), the stamp-only organizationField
scope pin (7901b2d), the source-hashes provenance companion (09b4f4e),
the update/delete doors scoped to the acting organization (f087c37) and the
loud permission-store outage (6a180e4).

Comments only: every file keeps its line count and no code token moves.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…e comments

The rewritten docblocks and inline comments ship in all four dist entry
files, so the package publishes changed bytes.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-storage, touching 5 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/services/service-storage/src/attachment-lifecycle.ts, packages/services/service-storage/src/backfill-sys-file-organizations.ts, packages/services/service-storage/src/file-reference-lifecycle.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/permissions/system-context.mdx (via installAttachmentAccessHooks (symbol, a top-level function))
What this run could not see
  • 4 changed file(s) yielded no anchor (packages/services/service-storage/src/attachment-lifecycle.ts, packages/services/service-storage/src/backfill-sys-file-organizations.ts, packages/services/service-storage/src/file-reference-lifecycle.ts, …) — pages documenting those are invisible to this run
  • 1 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 — 7 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 defc7f7b504e22b9e2c12a5374ebb255efe9b7da → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 04358c57f8a4227618405fc6769a6dcf753d2091 — the merge of head 09d2ecc96ad65f66dafc6fa91311fce800e81d2d into base defc7f7b504e22b9e2c12a5374ebb255efe9b7da, 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 04358c57f8a4227618405fc6769a6dcf753d2091 && git checkout 04358c57f8a4227618405fc6769a6dcf753d2091
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin defc7f7b504e22b9e2c12a5374ebb255efe9b7da 09d2ecc96ad65f66dafc6fa91311fce800e81d2d && git checkout -B drift-repro defc7f7b504e22b9e2c12a5374ebb255efe9b7da && git merge --no-ff 09d2ecc96ad65f66dafc6fa91311fce800e81d2d

node scripts/docs-audit/affected-docs.mjs --json defc7f7b504e22b9e2c12a5374ebb255efe9b7da

⚠️ 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 defc7f7b504e22b9e2c12a5374ebb255efe9b7da → 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: CONTRACT_REVIEW_TIER
Head-sha: 09d2ecc96ad65f66dafc6fa91311fce800e81d2d
Local-runs: none

① Derived judgments

Read against main at the merge-base 31ed06763. origin/main (fetched for this record) stands five commits past that base — 35587f76c, a8acee28d, ed5476870, defc7f7b5, 735594bea — and none of the five touches packages/services/service-storage, this PR's changeset, .changeset/config.json or scripts/check-issue-citations.mjs, so the net diff against main is the merge-base diff and the two-dot and three-dot diffstats agree: 16 files, +53/−43 — 15 source files under packages/services/service-storage/src/** (8 modules, 7 test files) and one changeset. The head 09d2ecc96 adds only the changeset on top of 5db5155a2, which holds every source line.

  • Accept-set: no change — right. No Zod schema, REST handler, query-parameter set, refusal text, log text or runtime string moves. 43 source lines out, 43 in; every one of the 86 changed source lines opens with a comment marker after whitespace (//, *, /**), 0 fall outside one. Each of the 15 touched source files has additions equal to deletions, so no line citation into these files moves. The dev's parser leaf-token guard (0 files with a token change; both positive controls DIFFER) says the same and is not repeated here.
  • Public surface: no change — right. No export added, removed or renamed (src/index.ts is untouched); no packages/spec file touched, so no generated artifact is owed.
  • Published bytes: changed — right, and it decides ②. @objectstack/service-storage (17.5.0, not private, files = dist, README.md, CHANGELOG.md, types = dist/index.d.ts, build = tsup then check-dts-emitted) emits declarations, and rewritten docblocks sit on declarations src/index.ts exports: StorageMetadataStore.updateFile / deleteFile / updateSession / deleteSession and the StorageWriteContext section in metadata-store.ts, the options docblock in storage-routes.ts (:42), and the module docblock over installAttachmentAccessHooks (attachment-access-hooks.ts:28). So dist/index.d.ts changes. The dev's A3 build reading (f087c376f six times and da891e0ef once in each declaration file, with a positive and a negative control) is consistent with that count of exported sites; this record does not repeat the build.
  • The 8 numbers are dead — right. Each of #6206 #6523 #8778 #10091 #11427 #11671 #13178 #13279 answers 404 on the issues endpoint (board read 2026-09-29T19:43Z). No ADR names any of them, per the PR's docs/adr grep, so ruling C's first rung is empty and a commit is the right anchor for every one.
  • The 7 anchors — each right. Each abbreviated sha resolves to exactly one commit (rev-parse --disambiguate, count 1 for all 7) and is an ancestor of the base (merge-base --is-ancestor, exit 0 for all 7), read from the shared object store after the fetch. For each, the commit's message or diff names the number it replaces, and the decision the rewritten line states is the commit's:
    • #13178 → f087c376f: the subject names it; its diff is metadata-store.ts and storage-routes.ts — the four update/delete doors given the StorageWriteContext (scoping, not stamping) and the upload routes binding the session they had resolved and discarded, which is what every rewritten line says.
    • #13279 → 6a180e42d: the message names #13279 four times beside the maintainer's verbatim ruling; it says the outage "answered a 403 byte-identical to a genuine capability denial", so 「the confusion commit 6a180e4 was made to prevent」 (four sites) and 「the relay that block has run since commit 6a180e4」 are faithful; the re-raise in the authorizer's catch (storage-service-plugin.ts:1229) is that commit's.
    • #10091 → da891e0ef: the message names #10091; the subject is the sys_attachment beforeUpdate uploader-or-parent-editor gate.
    • #11427 → c3c72a4bc: the message names #11427 and describes exactly the hydration/download divergence and the batched findHeldFiles its diff adds to attachment-lifecycle.ts; 「the divergence commit c3c72a4 fixed」 is the right tense.
    • #6206 and #6523 → aa4b90d9a: the subject names #6523, the body applies 「the 同族第三处组装:share-link 路由把授权信封裁成 4 个字段后直接当 enforcement context 喂给 engine.find —— group 租户姿态下 Layer 0 墙恒判否 #6206 ruling default (converge on the full envelope, keep no per-site subset contracts)」, and its diff writes the ISharingService docblock in packages/spec/src/contracts/sharing-service.ts — 「Callers MUST NOT rebuild a subset of it」, wrapped over two lines, which is why a single-line search misses it. The quoted words at attachment-access-hooks.ts:127-129 and the test's :766 and :914-916 are that docblock, so 「the full-envelope ruling … (commit aa4b90d)」 sits beside its own text. Stage 2's anchor for the same pair.
    • #8778 → 7901b2dd2: the subject names it (Option A per the maintainer ruling); its diff carries 「⛔ Scope-pinned by the spec: audit stamping needs a read-neutral organization declaration — tenancy.tenantField cannot serve sys_api_key without walling the credential table (#8707 remainder) #8778 ruling: this is ONE stamp-only declaration key … a consumer other than audit stamping needs its own ruling」, which is what backfill-sys-file-organizations.ts:86 now states as 「scope-pinned by its ruling (commit 7901b2d; …); a fourth needs its own maintainer ruling」.
    • #11671 → 09b4f4e4e: the seat's ACCEPT read this one as named in neither subject nor body and took it on content; the commit's diff names #11671 five times (its own changeset subject, the --source-hashes flag help, the generated-header producer), so the anchor holds on the same evidence the other six do. The anchor stages 1, 2 and 4 used for the identical translations/index.ts line.
  • Citation accounting — right. Over the diff (raw # plus digits): the 43 removed source lines carry 42 dead occurrences on 41 lines (#13178 14, #13279 11, #10091 5, #11427 4, #6206 3, #6523 3, #8778 1, #11671 1; attachment-access-hooks.test.ts:766 carries two) plus the live #10246, #12069 and cloud#1395; the 2 removed lines carrying none are the reflow lines the body lists (attachment-access-hooks.test.ts:916, storage-service-plugin.ts:1059), each the continuation of a rewritten sentence. Added lines carry exactly #10246, #12069 and cloud#1395, each on the line it already stood on (delta 0); no number is new to the diff, none grew, no PR #N stands on an added line, and 7 distinct shas stand on added lines.
  • The 11 sites left — right, and the list is exact. A grep of the 8 numbers over service-storage/src at the head returns exactly 11 lines, every one a describe or it title — attachment-access-hooks.test.ts:232, :314, :640, :932; tenant-audit-update-delete-half-repairs.test.ts:151, :224, :345, :552, :664; tombstone-hydration-download-agreement.test.ts:148, :326 — the same 11 the body lists. Titles are string tokens, left as stages 1 to 5 left theirs. The three *.source-hashes.generated.ts headers carry no dead number, and no operator string, assertion message or quoted ruling in this package does.
  • The gate-invisible spellings — right. At the head under service-storage/src: no #N-word spelling; 11 #A/#B lines carrying 13 second-number occurrences over 8 distinct numbers (#3833 #3847 #3870 #5197 #5480 #5541 #5574 #9974), and each of the 8 resolves (HTTP 200; #3870 as a pull request), board read 2026-09-29T19:43Z. The claim's count of 12 against the reading's 11 lines changes nothing: nothing dead stands there either way.
  • The remaining wordings — each right. file-reference-lifecycle.ts:111 (the update/delete halves 「in commit f087c37」 beside the live #12745 / #12928), storage-service-plugin.ts:1058-1059 (「that card」, whose referent left with the number, → 「since commit 6a180e4 landed」), the test heading at attachment-access-hooks.test.ts:621 (「Commit da891e0's gate」), and translations/index.ts:29 (the ruling #12069 kept live, the companion's commit beside it).
  • Form — consistent with the landed stages 1 to 5 (422db788a, b80ab579d, 4d04b6be3, 9a4b2bb38, 0e9ad74fb): the word commit plus the abbreviated sha in the position where the number stood, the decision carried in the sentence. The four reused anchors (6a180e42d, aa4b90d9a, 7901b2dd2, 09b4f4e4e) are the ones this card's thread records for the same numbers.
  • Check-runs on the head, the gate verdicts (read 2026-09-29T19:44Z): 34 check-runs, all completed — 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in): paths-filtered or opt-in, not verdicts against), 0 failure, 0 in progress. Every one of the seven required contexts is success: Lint & Repo Gates (which carries check:issue-citations and check:doc-authoring, the two gates this diff answers to), TypeScript Type Check, Test Core (the aggregate, with all six shards success), Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Check Changeset, Check PR Size, Part-of PR must not also close its card and The card this PR closes must claim this branch are success too. A read one minute earlier had caught the Test Core aggregate still in_progress behind six green shards; nothing was awaited, the read was simply repeated. Nothing was built, run or re-run locally.

② Semver level

  • .changeset/20596-service-storage-provenance-anchors.md declares '@objectstack/service-storage': patch — matches what the diff publishes. The package is released and dist/index.d.ts carries the rewritten docblocks, so bytes ship; skip-changeset would be wrong (it is for a diff that publishes nothing from any released package), and the PR carries no such label. Not minor: no accept set widens and no surface is added. The body is truthful (comments only; no type, schema, export, log or refusal text, or runtime behaviour change), carries no tracker number and no model identifier, follows stage 5's landed form, and the filename carries the card number. service-storage sits in the fixed group (.changeset/config.json:72) beside the five packages whose stages declared the same level.
  • Clause-②: no — right. It is line 2 of the PR body under Part of #20596, and the claim (5896394242) declares the same. The diff widens no accept set, so no arm is owed and no minor is owed. Nothing breaks, so no ADR-0087 marker is owed; Check Changeset on the head is success.
  • Not a governed-surface diff (no path under docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md); 96 changed lines, under the 5,000-line human-merge threshold; head repo equals base repo; Governed Surface Queue Guard on the head is success. A draft with Part of #20596 on line 1 and no closing keyword anywhere in the body, so the card stays open for the remaining stages.

③ Boundary flags

The dev report (5897256689) has open_questions: [] and out_of_scope_findings: []. Its eight deviations, each answered:

  1. 15 test-comment sites beyond the census's 27 — answered, in scope. The claim's surface is comment and docblock prose under service-storage/src/**; test comments are that, and stages 1 to 5 rewrote theirs. The head grep above confirms the residue is titles only.
  2. The supplementary and raw readings judged against the before census's own board reading rather than a second enumeration, after a scratch enumeration script was refused by the local permission layer and deliberately not re-routed — answered, immaterial to the verdict. The census instrument itself ran unchanged, twice, at the newest frontier (27 → 0, whole-repo drop exactly 27), and this record reproduces the residue independently: the head grep returns the 11 titles and nothing else, every kept second number resolves, and every replaced number is 404. Not re-routing a refused run is the right call, and it is disclosed.
  3. Lines reworded beyond the anchor, plus 2 reflow lines — answered, right (① above). Each rewording was checked against its anchor commit's message or diff; every file keeps its line count.
  4. The claim's 12 gate-invisible spellings against a reading of 0 #N-word and 11 #A/#B lines, all live — answered, right. Verified at the head (① above); nothing there needed rewriting.
  5. The harness attribution reminder versus AGENTS.md's trailer pair — answered, right. Both head commits end with the model-free pair AGENTS.md prescribes (the session trailer and the plain co-author line), no model identifier appears in either message, and the PR body's footer is the session-URL form the PR-body surface keeps.
  6. Labels — answered. documentation, size/s, tests, tooling are the labeler's; no skip-changeset, which is right.
  7. Branch base four commits behind main at the report, five at this read — answered, right. None of the five touches a path in this diff or an input the citation gate derives from (① above), so the queue's rebuild has nothing to reconcile by hand.
  8. The report comment posted through post-stamped.mjs from the shared checkout after the worktree was removed, nothing edited — answered, immaterial here. A read of the tool, not a write to the tree; the head's check-runs and this record's own reads are what the verdict rests on.

Two readings that are not flags on this PR: the Docs Drift Check advisory names content/docs/permissions/system-context.mdx through installAttachmentAccessHooks, whose only change is a docblock citation, so that page cannot have been falsified; and the ACCEPT's note that #11671 → 09b4f4e4e is 「named in neither」 understates the evidence (① above) — nothing to change. Nothing is escalated.

Implemented-by: claude/issue-20596-service-storage-citations
Reviewed-by: session_01XY5uCwTjZj7884yYtyur4H

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 19:49
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 9b384f6 Sep 29, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20596-service-storage-citations branch September 29, 2026 20:06
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…o the commits that decided them (objectstack-ai#20713)

Part of objectstack-ai#20594
Clause-②: no

## What changed

This is stage 7 of the `domain:cli` lane of the dead-citation sweep:
`packages/mcp/src`. Every comment site there 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 keeps saying in its own words what that commit
decided. PR objectstack-ai#20533 is the method, and stages 1 to 6 of this card (PR
objectstack-ai#20624, PR objectstack-ai#20632, PR objectstack-ai#20656, PR objectstack-ai#20673, PR objectstack-ai#20689, PR objectstack-ai#20703) are the
precedents. The card stays open for the lane's remaining packages, so
this PR says `Part of`.

That is **17 sites on 17 lines in 9 files, covering 9 numbers**,
rewritten to **9 distinct commits**:
- the census's **10 sites**, in `mcp-server-runtime.ts` (5), `plugin.ts`
(3) and `stdio-data-bridge.ts` (2), 7 numbers;
- **7 test-file comment sites** in 6 test files (the census defers
`*.test.ts`; stages 1 to 6 took test comments too).

One more line changed: `__tests__/plugin-execution-context.test.ts:7`,
the second half of the `:6` sentence ("this face was not in that card's
inventory" now reads "not in that commit's inventory", since the card it
pointed back to is now named as a commit).

Only comments changed: **18 lines out, 18 in**, and every touched file
keeps its line count, so no line citation into these files moves. **No
citation number is added**: over the 18 line pairs, added-minus-removed
numbers is empty, and no PR number stands newly on any line. No ADR or
ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records any
of these 9 decisions (a grep for the 9 numbers there reads 0 hits, with
a control number from the same tree reading 2), so every anchor is a
commit.

**No changeset, and `skip-changeset`:** none of the rewritten comments
reaches `dist` (measured below: base and head emit six byte-identical
files, and a code-mutation control changes four of them). That is stage
5's case (PR objectstack-ai#20689), not stage 6's.

## Census: `packages/mcp`, before and after

**Instrument.** The gate's own `node scripts/check-issue-citations.mjs
--census --json`, read-only and unchanged, run under `with-fleet.sh
--read` for the token. The count is its `allocated-but-absent` findings
under `packages/mcp/`. Both runs enumerated the whole board.

| reading | tree | board | whole-repo `allocated-but-absent` |
`packages/mcp` sites | lines | numbers | files |
|---|---|---|---|---|---|---|---|
| before | base `e4e5222b7b`, run 2026-09-29T19:45:33Z to 19:50:37Z |
enumerated, 186 pages, frontier objectstack-ai#20708, 18,535 numbers | 1,222 | **10**
| 10 | 7 | 3 |
| after | `459ff81088`, run 19:58:39Z to 20:02:47Z | enumerated, 186
pages, frontier objectstack-ai#20709, 18,536 numbers | 1,212 | **0** | 0 | 0 | 0 |

The whole-repo drop of 10 is exactly these sites: a site-by-site diff of
the two JSON outputs has 10 findings gone, all under `packages/mcp/src`,
and none added. The other three tallies (`resolves` 32,968,
`resolves-as-pull-request` 1,984, `cross-repo-unjudged` 994) are equal
in both runs. `packages/mcp/src` is byte-identical at `459ff81088` and
at the head.

**Supplementary scan (test files included).** The gate's exported
`extractCitations` and `classifyCitation` over all 43 `.ts` files under
`src/`, with the board from the gate's own `probeBoard`: 365 citations
and 21 dead before (src comments 10, test comments 7, src strings 0,
test strings 4), 348 and 4 after (0, 0, 0, 4). Its before list of src
comment sites is identical to the census's. The 4 left are test titles,
the form-D stage (see Acceptance notes).

## Per-site table

`git blame` at the base ties each line to the commit that wrote it, and
each anchor was read in its message, changeset or diff, not only its
subject. Where the pull request that landed an anchor still answers, its
body's first line names the dead number, which is noted.

| number | sites (base line) | anchor: what it decided |
|---|---|---|
| `objectstack-ai#13318` | `mcp-server-runtime.ts:272` | `3ec8646f1`: the bridged
tools' `readOnlyHint` / `destructiveHint` come from what the definition
declares, and a tool that declares nothing is served neither hint
(omit-when-unsourced). The line blames to `c39369d12`, the
`openWorldHint` sibling, whose changeset calls this the repair "that
preceded it". The PR that landed `3ec8646f1` answers 404 too. |
| `objectstack-ai#6724` | `mcp-server-runtime.ts:625`;
`mcp-server-runtime.metadata-outage.test.ts:289` | `4f3d2322e`: corrects
`diagnoseEmptyRead`'s falsified claim that `MetadataFacade.getObject`
differs from `get('object', n)`, in the TSDoc and in the outage test's
restatement of it. Both lines blame to it; PR objectstack-ai#6948, which landed it,
names objectstack-ai#6724. |
| `objectstack-ai#6745` | `mcp-server-runtime.ts:636` | `7a5ef0008`: adds
`metadata-service-getobject-equivalence.test.ts`, pinning `getObject(n)`
equal to `get('object', n)` across all three implementations. The line's
"PR objectstack-ai#6839 for objectstack-ai#6745" named this commit's PR (answers 200), which stays
beside the sha as a convenience link. The spec lane gave the number this
anchor. |
| `objectstack-ai#6723` | `mcp-server-runtime.ts:637`, `:652`;
`mcp-server-runtime.metadata-outage.test.ts:293` | `8ad609c69`: declares
on `IMetadataService.getObject` that it answers the same as
`get('object', name)`. `objectstack-ai#6723` was the pull request that landed as this
commit (its subject carries the number); `objectstack-ai#6505`, the issue beside it on
`:637`, answers 200 and stays. The spec lane gave the number this
anchor. |
| `objectstack-ai#17114` | `plugin.ts:8`, `:67`;
`stdio-tenancy-posture-api-key-matrix.test.ts:569` | `4af758d47`: the
last two admission doors, this one included, classify the tenancy
rejection through the shared `classifyAdmissionTenancyPosture`. All
three lines blame to it; PR objectstack-ai#17683 names objectstack-ai#17114, and stage 1 gave the
number this anchor. |
| `objectstack-ai#6216` | `plugin.ts:126`;
`__tests__/plugin-execution-context.test.ts:6` | `f586f1a89`: one
`ExecutionContext` assembler for the dispatcher, REST and share-link
sites. Both lines blame to `502dc6fe7`, which converged this stdio face
afterwards and names that convergence as its precedent. Its file list
touches no `packages/mcp` file, which is what `:7` ("not in that
commit's inventory") says. Stages 1 and 2 and the spec lane gave the
number this anchor. |
| `objectstack-ai#8422` | `stdio-data-bridge.ts:85`, `:394`;
`stdio-data-bridge.not-found.test.ts:4` | `4810dd628`: the stdio
bridge's by-id write seams throw the shared `recordNotFoundError`
envelope instead of a bare `Error`. All three lines blame to it; PR
objectstack-ai#8507 names objectstack-ai#8422. |
| `objectstack-ai#17568` | `mcp-record-id-key-mistake-refusal.test.ts:4` |
`9c9e6d08f`: pins that a missing-`recordId` refusal also names the `id`
the caller sent (test-only). The line blames to it; PR objectstack-ai#17650 names
objectstack-ai#17568. |
| `objectstack-ai#13486` | `mcp-tool-bridge-safety-annotations.test.ts:423` |
`6193e576d`: pins the bridge's two hand-copied safety name sets in the
direction the old pin could not see (the docblock's heading is that
commit's subject). The line blames to it; PR objectstack-ai#13888 names objectstack-ai#13486. |

**Anchor checks.** Every cited sha matches exactly one object (`git
rev-parse --disambiguate`, count 1 for each of the 9), is a commit, has
one parent, and is an ancestor of `main` (`merge-base --is-ancestor`
against `5757463712`, exit 0 for all 9). The checkout is not shallow.
The control leg `979ad9575` (2026-08-08, the parent of the oldest anchor
`8ad609c69` of 2026-08-08) exits 0, and the negative control, this
branch's own `459ff81088`, exits 1. Four anchors reuse the landed
stages' (`f586f1a89`, `4af758d47`, `7a5ef0008`, `8ad609c69`), so each
number carries one anchor across the tree; five are new (`3ec8646f1`,
`4f3d2322e`, `4810dd628`, `9c9e6d08f`, `6193e576d`).

**Numbers.** All 9 dropped numbers answer 404 by REST (re-probed
2026-09-29T19:54Z). The numbers kept on changed lines (`objectstack-ai#6839`, a pull
request; `objectstack-ai#6505`, `objectstack-ai#15348`, `objectstack-ai#16013`, `objectstack-ai#4435`, `objectstack-ai#5138`, `objectstack-ai#7867`) answer
200. Four slash-joined groups stand in `packages/mcp/src`, whose later
halves the citation grammar does not read (`objectstack-ai#4435/objectstack-ai#5138/objectstack-ai#7867` twice,
`objectstack-ai#5138/objectstack-ai#5581`, `objectstack-ai#7728/objectstack-ai#7823`); every half answers 200, so none is dead.

## Mechanical guard: no code token moves

**H2 holds on both readings: the parser leaf-token diff is empty, and
the emitted `dist` is byte-identical.**

**Token guard.** It compares the TypeScript parser's leaf tokens
(TypeScript 6.0.3, JSDoc nodes excluded) of the 9 touched files at base
`e4e5222b7b` and at `459ff81088`. Controls mutate the head text in
memory only.
- Real run: 21,192 base tokens, 0 differing (exit 0).
- Comment-insertion control: 0 differing (exit 0).
- Code-insertion control: all 9 files differ at token 0 (exit 1).
- String control (the first character of the `'vitest'` import specifier
in `plugin-execution-context.test.ts` flipped): exactly 1 differing
`StringLiteral`, at token 15 of that file (exit 1).

**Emitted `dist`.** `pnpm --filter @objectstack/mcp build` at the head,
then at base (the base tree of `packages/mcp/src` restored in place
under a trap-armed restore; an on-disk probe read `objectstack-ai#13318` 1 and `commit
3ec8646` 0 before that build; afterwards every touched blob equals its
HEAD blob and `git diff HEAD` is empty), with the same dependency
builds:
- all six files (`index.cjs`, `index.cjs.map`, `index.d.cts`,
`index.d.ts`, `index.js`, `index.js.map`) are **byte-identical** by
sha256. The built files do carry docblocks (14 in `index.js`, 78 in
`index.d.ts`); none of the rewritten ones is on an emitted declaration.
- Code-mutation control (`scripts/ablation-replace.mjs`, anchor: the
sync leg's typed `ctx.getService` call on `'tenancy'` in `plugin.ts`,
hit 1 to 0, its argument renamed to a marker; blob restored to HEAD
`0a1aaa7955`, `git diff HEAD` empty):
`scripts/ablation-dist-preflight.mjs` found the marker in `index.cjs`
and `index.js`, and `index.cjs`, `index.js` and both `.map` files differ
from the head build. `dist` was then rebuilt, its six sha256 values
equal the first head build, and the preflight in `--absent` mode reads
the marker absent from all 6 files with a clean tree.

A raw scan of the 9 changed files for control bytes finds none (a
positive probe on a scratch file matched).

## Changeset

**None, and `skip-changeset`.** `@objectstack/mcp`'s `files[]` is
`dist`, `README.md` and `CHANGELOG.md`, and the build above emits
byte-identical `dist` at base and head, so this diff publishes nothing
from any released package. Stage 5 (PR objectstack-ai#20689) measured the same and
shipped the same; stage 6 (PR objectstack-ai#20703) measured the opposite and carried
a `patch`.

## Gates (head `7a0f15de62`)

This host has no `flock`, so `os-verify-lock.sh` ran in its declared
unlocked mode. Its disclosure, verbatim, from each run at this head and
from the four `dist` builds (at `459ff81088`, `packages/mcp/src`
byte-identical to 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 25s · declare it in the PR body · pnpm --workspace-concurrency=2 --filter '@objectstack/mcp...' 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 116s (1m56s) · declare it in the PR body · pnpm turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=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 10s · declare it in the PR body · pnpm --filter @objectstack/mcp exec vitest run --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 4s · declare it in the PR body · pnpm --filter @objectstack/mcp typecheck
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 3s · declare it in the PR body · pnpm --filter @objectstack/mcp 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 3s · declare it in the PR body · pnpm --filter @objectstack/mcp 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 3s · declare it in the PR body · pnpm --filter @objectstack/mcp 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 2s · declare it in the PR body · pnpm --filter @objectstack/mcp build
```

- **Build:** `@objectstack/mcp` with its closure (9 of 81 workspace
projects), then the whole workspace, `turbo run build
--filter='./packages/*' --filter='./packages/*/*'`, 71 of 71 tasks,
after the merge. The tree was clean after both.
- **Tests:** `vitest run`: 32 files, 344 tests passed (every `*.test.ts`
under `src/`), at the head and before the merge.
- **Typecheck:** `pnpm --filter @objectstack/mcp typecheck` exits 0.
`tsc --listFiles`: `tsconfig.json` compiles the 11 non-test `src` files,
`tsconfig.test.json` all 43 including the 32 test files.
`check:test-typecheck`: 6 files, 53 errors, 8 pinned signatures, held.
- **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`)
exits 0 at this head (2026-09-29T20:18:54Z to 20:19:23Z), and at
`459ff81088` before the merge.
- **Citation judging:** after merging `origin/main` (`9b384f63ae`),
`node scripts/check-issue-citations.mjs --base 9b384f6` judges 5
citations on the changed lines of 3 files (the kept numbers `objectstack-ai#15348`,
`objectstack-ai#16013`, `objectstack-ai#4435`, `objectstack-ai#6505`, and `objectstack-ai#6839` as a pull request) and exits 0:
every one resolves. Against `origin/main` after it moved to
`5757463712`, the same 5 citations, exit 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 53 families. All 53 exit
0, and `--ran` with the exit-coded record reads "53 derived, 53 run, 0
NOT-MEASURED, 0 UNRUN" (a derived zero). Among them:
`check:issue-citations`, `check:doc-authoring` (808 pinned sites, no
growth), `check:nul-bytes` (9,331 files, no raw control bytes),
`check:published-files`, `check:type-check-debt`.
- **Artifact rosters:** 36 of the 39 non-self-test roster rows exit 0,
including the three the derivation marks as keeping their roster under
one of this diff's paths (`check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`). The other three
need a pull request's context; they are run against this PR once it
exists and reported on the card. The 18 self-test-only rows grade their
checkers' fixtures and cannot judge this diff.

## Hypotheses (measured first)

- **H0 holds.** At base `e4e5222b7b` the filtered census answers 10
sites on 10 lines, 7 numbers, in 3 files, as on the seat's `0be898499f`.
The whole-repo count is 1,222.
- **H1 holds.** After the rewrite, the filtered census answers 0 for
`packages/mcp`. No site was left for an open PR (the file lists of all 8
open PRs were read at 20:08:05Z: only the Version Packages PR objectstack-ai#20639
touches `packages/mcp`, in `CHANGELOG.md` and `package.json`) or for an
unfound anchor.
- **H2 holds, on both readings.** The comment-stripped (parser-token)
diff of all 9 touched files is empty with its controls firing, and the
emitted `dist` is byte-identical at base and head with a code control
that changes it.

## Acceptance notes

- **Test titles, the form-D stage.** 4 dead numbers remain in test
string literals in `packages/mcp/src` (`describe` titles, no assertion
text): `objectstack-ai#17568` twice in `mcp-record-id-key-mistake-refusal.test.ts`
(`:151`, `:315`), `objectstack-ai#8422` in `stdio-data-bridge.not-found.test.ts:99`,
`objectstack-ai#17114` in `stdio-tenancy-posture-api-key-matrix.test.ts:592`. They
stay on the card for its form-D stage; no string moved here.
- **Outside `src/**`, a later stage of the card:**
`packages/mcp/vitest.config.ts:18` cites `objectstack-ai#8651` (404).
`packages/mcp/test-typecheck-debt.json:2` cites `objectstack-ai#13470` (404) inside
its `_comment` field, which the file itself says is generated by
`scripts/check-test-typecheck.mts`, so a fix there is at that producer,
in the `scripts/**` lane, not a hand edit. The other citations in
`packages/mcp` outside `src/**` (`CHANGELOG.md` excluded) answer 200.
- **Card-word residue, cited nowhere.** A few docblocks still say "this
card" or "the card" a paragraph away from the rewritten line (for
example `stdio-data-bridge.not-found.test.ts:19`,
`mcp-record-id-key-mistake-refusal.test.ts:19`,
`stdio-tenancy-posture-api-key-matrix.test.ts:580`, `:584`). They cite
no number, so they were left, as the landed stages left theirs; only the
one same-sentence companion (`plugin-execution-context.test.ts:7`) was
changed.
- **The moving `origin/main`.** The branch merged `origin/main` once
(`7a0f15de62`, merging `9b384f63ae`: `service-storage`,
`platform-objects` and `plugin-audit`, nothing in `packages/mcp`). A
later fetch advanced the shared ref to `5757463712`, one commit in
`platform-objects` translations. There was no second merge; CI judges
the merge ref.

## Deviations

- **One companion line (`plugin-execution-context.test.ts:7`)** beyond
the 17 sites, the second half of the `:6` sentence.
- **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 harness's attribution reminder asked for a model-named
trailer and a different PR footer, and AGENTS.md overrides it. 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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ommits that decided them (objectstack-ai#20717)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the seventh stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/plugins/plugin-approvals/src/**` and nothing else. By the
seat's census at the claim (`5897866351`), it is the largest package in
the lane that no in-flight work holds. 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 to 6 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`). That is **41 sites on 38 lines in 13 files, covering 14
numbers**:

- 24 census sites (every census site this package has);
- 15 sites in test comments, which the census defers;
- 2 sites the gate's citation grammar cannot see, found by a raw scan
(see Acceptance notes): the second number of `objectstack-ai#8287/objectstack-ai#8778`
(`approval-node.test.ts:462`) and 「the option objectstack-ai#8710 rejected」
(`approval-service.ts:2329`), which the gate reads as an option ordinal.

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **13 distinct shas**. No number in this package has an ADR or
ruling record of its own in the repository (a grep of `docs/adr/` for
all 14 finds none, and a grep of the rest of `docs/` finds only an audit
that names `objectstack-ai#11311` as evidence), so every anchor is a commit, per
ruling C's order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(41 lines out, 41 in, over 13 files), so no line citation into these
files moves. 3 of those 41 lines hold no dead citation: 1 reflow line
and 2 lost-referent lines, 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: `objectstack-ai#8613`
(`approval-service.ts:2295`, `:2363`, `approval-service.test.ts:751`),
`objectstack-ai#8287` (`sys-approval-request.object.ts:138`,
`approval-node.test.ts:462`), `objectstack-ai#10101`
(`backfill-platform-row-organizations.ts:9`) and `objectstack-ai#12069`
(`translations/index.ts:26`). Each answers 200. Over the whole diff,
added minus removed is 0 or negative for every number, and no number is
new to the diff. No PR number stands on an added line; the one `PR #N`
spelling in scope (`backfill-platform-row-organizations.ts:9`) became
its squash commit.

Five dead sites are left on purpose, all of them test strings (see the
list below).

One more file: a `patch` changeset for `@objectstack/plugin-approvals`,
because the rewritten docblocks and inline comments ship (see Changeset
below).

## Census: `plugin-approvals`, 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-approvals/`. 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-approvals sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `575746371`, run 2026-09-29T20:15:05Z to 20:18:28Z |
enumerated, 186 pages, frontier objectstack-ai#20709 (newest objectstack-ai#20709 before and after),
18,536 numbers | 1,195 | **24** | 22 | 6 | 10 |
| after | head `e698d2393`, run 20:28:39Z to 20:31:58Z | enumerated, 186
pages, frontier objectstack-ai#20716 (newest objectstack-ai#20714 before, objectstack-ai#20716 after), 18,543
numbers | 1,171 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim and A1 (24
sites). The whole-repo drop is 24, exactly this diff's census sites. The
`resolves` tally is 32,971 in both runs, and `resolves-as-pull-request`
(1,984) and `cross-repo-unjudged` (995) did not move either. The after
run was taken on `e698d2393`; the head `708244c2b` adds only the
changeset. No run was truncated or discarded: both enumerations read 186
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 `namesThisRepository`
over every `.ts` file under `plugin-approvals/src` (76 files). It takes
its verdicts from the before census's own board reading rather than from
a second enumeration: a number is dead when that census reported it
`allocated-but-absent`, and alive when that census judged it on this
board anywhere (its `--list` extraction) and did not report it. The 18
numbers the census never saw, because they stand only in test files or
strings here, were read one by one on the issues endpoint: 14 answer
200, and `objectstack-ai#8863`, `objectstack-ai#11081`, `objectstack-ai#11286` and `objectstack-ai#11308` answer 404.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `575746371` | 981 | **44** | 24 | 15 | 0 | 5 |
| after, `e698d2393` | 942 | **5** | 0 | 0 | 0 | 5 |

Its src-comment column equals the census's 24, which is the control on
the second instrument. The 902 live citations and the 32 cross-repo
citations are the same in both readings, and the drop of 39 citations is
exactly the rewritten sites the gate grammar sees. Three extracted
tokens are not citations and stay unjudged in both readings: `&objectstack-ai#39;` (an
HTML entity) and two CSS colours, all in `action-link-pages.ts` string
literals. A third, raw reading (every `#` followed by 2 to 6 digits,
whatever surrounds it) finds 46 dead occurrences before and 5 after; the
2 it sees beyond the gate are the two gate-invisible sites above, and
its residue equals the gate's residue site for site.

## Per-number table

Sites and files count every dead occurrence 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, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (`merge-base --is-ancestor` exit 0 for each pair).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#16709` | 10/2 | 8/2 | `8c7cca1ce`: the three residues of the
stranded-inspection contract review. Item 2 (the PM ruling of
2026-09-08) keeps a row whose third read threw in the report as the
undifferentiated `failed`; item 3 moves `refineFailedRunState` inside
the `try`, so a malformed host verdict costs only its own row. Its
message numbers the items, which is why the lines keep 「item 2」 and
「item 3」. New to the sweep |
| `objectstack-ai#8710` | 6/2 | 6/0 | `04d03c3a0`: a deactivated `sys_position`
confers no sharing-rule shares, filtered at the sharing call site and
never inside the addressing primitive. Its message quotes the 2026-08-15
ruling verbatim, the same sentence the quoted blocks here carry, and its
diff writes the 「a name with no row is untouched」 fallback that
`approval-service.ts:2318` quotes. Stage 2's anchor, and
`plugin-sharing/src/position-graph.ts:42` already reads 「objectstack-ai#8613 / commit
04d03c3」 |
| `objectstack-ai#6523` | 4/3 | 4/0 | `aa4b90d9a`: the 36 enforcement signatures,
`IApprovalService` among them, converged onto the full
`ExecutionContext`. Its subject names it. Stages 2 and 6 and the spec
stage's anchor |
| `objectstack-ai#6206` | 3/3 | 3/0 | `aa4b90d9a`: the same commit, whose body applies
「the objectstack-ai#6206 ruling default (converge on the full envelope, keep no
per-site subset contracts)」. Written as the full-envelope ruling, the
form stages 2 and 6 used |
| `objectstack-ai#8778` | 4/4 | 4/0 | `7901b2dd2`: the stamp-only
`tenancy.organizationField`, Option A of the maintainer's ruling,
declared on `sys_api_key` as `active_organization_id`. The spec,
`plugin-security` and `service-storage` stages' anchor |
| `objectstack-ai#11081` | 5/1 | 5/0 | `c28e4cfae`: the two SqlDriver-backed fixtures
of `objectstack-ai#11081` stop muting their kernel and pin the expected read-refusal
noise with the runtime's shared capture. Its diff writes all five
`[objectstack-ai#11081]` tags. New to the sweep |
| `objectstack-ai#11286` | 5/1 | 2/3 | `b019891cd`: the contract test that pins the
two `managerIsProvablyOutsideOrg` screens to equal verdicts. Its subject
names it. New to the sweep |
| `objectstack-ai#11674` | 2/1 | 2/0 | `1cba33f16`: the seed loader warns at load time
when a seed defers a required column, and the ordering constraint is
documented at the four pointer-pair sites, this object among them. Stage
2's anchor for the same paragraph |
| `objectstack-ai#12493` | 2/2 | 2/0 | `aa5994e17`: the Operation Message Catalog
gains `approval_recall_not_submitter` (and `record_write_denied`) ahead
of their emitters. Its diff names `objectstack-ai#12493` throughout. Stage 2's anchor
|
| `objectstack-ai#8707` | 1/1 | 1/0 | `1408fe385`: audit rows are stamped from the
record's own organization, which its message says the maintainer's
ruling on `objectstack-ai#8287` requires; the line keeps 「honouring objectstack-ai#8287's ruling」.
New to the sweep |
| `objectstack-ai#8863` | 1/1 | 1/0 | `d200b016b`: the two negative pins that assert
the unfiltered position expansion on the approvals side. Its body names
`objectstack-ai#8863`. New to the sweep |
| `objectstack-ai#11308` | 1/1 | 1/0 | `5a916c4d4`: the one-off platform-row
organization backfill, dry run and write, which its body calls the
`objectstack-ai#11308` sweep. New to the sweep |
| `objectstack-ai#11311` | 1/1 | 1/0 | `1272f0a6b`: the squash commit of the pull
request that was `objectstack-ai#11311` (its subject carries the number), which moved
the resolver to `metadata-core` and made the approval and automation-run
writers stamp the subject's organization. New to the sweep |
| `objectstack-ai#11671` | 1/1 | 1/0 | `09b4f4e4e`: the source-hashes provenance
companion. The identical `translations/index.ts` line in
`service-messaging`, `plugin-sharing` and `plugin-security` already
cites it |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 13), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 13; the
history is complete, `--is-shallow-repository` false, 15,129 commits).
Each of the 14 numbers answers 404 on the issues endpoint, read one by
one; `objectstack-ai#11311` answers 404 on the pulls endpoint too.

## Wordings to check

- **The full-envelope ruling, `approval-node.ts:29`,
`approval-service.ts:52-53` and `exec-context-annotation.pin.ts:7-8`.**
「since objectstack-ai#6523 (the objectstack-ai#6206 ruling …)」 became 「since commit aa4b90d (the
full-envelope ruling …)」, word for word the form `plugin-sharing`'s
landed `sharing-service.ts:20` and `exec-context-annotation.pin.ts:7`
use. `approval-service.ts:53` is 1 reflow line.
- **The ruling's record, `approval-service.ts:2295` and
`approval-service.test.ts:751`.** 「Maintainer ruling, 2026-08-15 (objectstack-ai#8710,
inheriting objectstack-ai#8613), verbatim:」 became 「… (commit 04d03c3, inheriting
objectstack-ai#8613), verbatim:」. The quotation under it is the ruling itself and is
untouched; `04d03c3a0`'s message carries the same sentence.
- **`approval-service.ts:2329`.** 「that is the option objectstack-ai#8710 rejected」
became 「that is the option the ruling (commit 04d03c3) rejected」.
- **The test heading, `approval-service.test.ts:749`.** 「the objectstack-ai#8710
carve-out, asserted on THIS side (objectstack-ai#8863)」 became 「the commit 04d03c3
carve-out, asserted on THIS side (commit d200b01)」: the carve-out's
record, and the commit that asserted it here.
- **A PR number, `backfill-platform-row-organizations.ts:9`.** 「objectstack-ai#10101
(landed as PR objectstack-ai#11311)」 became 「objectstack-ai#10101 (landed as commit 1272f0a)」, the
pull request's squash commit.
- **Item numbers, `approval-service.ts:4893`, `:4906` and
`stranded-request-inspection.test.ts:123`.** 「[objectstack-ai#16709 item 3]」 became
「[commit 8c7cca1, item 3]」, and likewise for item 2, beside its 「PM
ruling, 2026-09-08」, which `8c7cca1ce`'s message records under 「Item 2」.
- **Lost referents, 2 lines with no dead site** (every file keeps its
line count): `backfill-platform-row-organizations.test.ts:17` 「the one
thing this card must not do」 became 「the one thing this sweep must not
do」, and `manager-org-screen-parity.contract.test.ts:61` 「the very
decision this card is fenced out of」 became 「the very decision this pin
is fenced out of」. Each 「this card」 pointed at the number the same
comment block opened with, which is now a commit; `b019891cd`'s message
says the pin 「PINS the duplication, it does not remove it」.

## The 5 sites left

- **Test strings, 5 sites**, left as stages 1 to 6 left theirs:
- `describe` / `it` titles:
`manager-org-screen-parity.contract.test.ts:232` (`objectstack-ai#11286`),
`stranded-request-inspection.test.ts:519` and `:642` (`objectstack-ai#16709`);
- a test double's thrown message and an assertion message:
`manager-org-screen-parity.contract.test.ts:107` and `:294` (`objectstack-ai#11286`).
- There is no operator string, generated header or quoted ruling
carrying a dead number in this package. The generated
`*.source-hashes.generated.ts` headers already cite `09b4f4e4e` and are
untouched. The two verbatim quotations of the 2026-08-15 ruling carry no
number and are untouched.

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as
trivia and JSDoc nodes never visited, base `575746371` against head.
Template literals are therefore read in context. It ran over all 13
touched `.ts` files.

- Real run: 36,204 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `sys-approval-request.object.ts` (「who a row is
ABOUT」 to 「whom a row is ABOUT」): 0 files changed, as expected (exit 0).
- Positive control, a code token added in
`sys-approval-request.object.ts` (`referenceVia: 'object_name',` given a
trailing `as const`): DIFFER (exit 1).
- Positive control, one digit changed inside a kept test title
(`stranded-request-inspection.test.ts:642`): 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 (`6cb56301a334`, `757ad45900ac`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/plugin-approvals`
(`.changeset/20596-plugin-approvals-provenance-anchors.md`) is included.
Its body is stage 6's, word for word, with the package name changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build (a cache miss for this package, so
`dist` is this head's source), the rewritten comments reach `dist`:
`8c7cca1ce` 4 times and `04d03c3a0` 4 times in each of `dist/index.d.ts`
and `index.d.mts`; `04d03c3a0` 4 times, `1cba33f16` twice, and
`8c7cca1ce`, `7901b2dd2` and `1408fe385` once each in each of `index.js`
and `index.mjs`. Positive controls: the unchanged line 「A step routing
to nobody is」, in the same docblock as the shipped rewrite at
`approval-service.ts:2295`, is found once in each of the four files, and
the unchanged line 「itself stays unwalled (`tenancy.enabled: false`)」
beside the shipped rewrite at `sys-approval-request.object.ts:144` once
in each JS file. A never-written negative phrase appears nowhere in
`dist`. None of the 14 dead numbers is left anywhere in `dist`.

## Gates (head `708244c2b`)

- **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 6 files, and all 5 resolve
(`objectstack-ai#8613` twice, `objectstack-ai#8287`, `objectstack-ai#10101`, `objectstack-ai#12069`), each already on the line
it replaces.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `708244c2b` derived 64 commands:
all 57 derived at dispatch, plus `check:dispatcher-error-vocabulary`,
`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`. Each ran with its
exit code captured before any pipe, and all 64 exit 0. `--ran`, fed each
command with its exit code, reports 64 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.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/plugin-approvals test`: 51 files pass and
791 tests pass. That is every test file in the package, the 7 touched
ones included.
- `pnpm --filter @objectstack/plugin-approvals typecheck` exits 0 (`tsc`
on `tsconfig.json`, the scripts program, and the test layer on
`tsconfig.test.json`, held at its ledger of 8 files, 324 errors and 27
pinned signatures). `--listFiles`: the `tsconfig.json` program holds the
25 non-test files under `src/`, the 6 touched ones included; the
`tsconfig.test.json` program holds all 76 files under `src/`, the 51
test files and all 13 touched files included.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 13 touched `.ts` files gives 13 files, 0 errors and 0
warnings. All 13 are in eslint's own population (`isPathIgnored` is
false for each; a `dist` file, as the control, is ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), 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 14 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636). In this package there is one `#N-word` spelling, 「objectstack-ai#3266-era」
(`record-reader-visibility.test.ts:342`), and two `#A/#B` spellings,
`objectstack-ai#8287/objectstack-ai#8778` (`approval-node.test.ts:462`) and `objectstack-ai#8543/objectstack-ai#8580`
(`approval-vocabularies.test.ts:66`): the claim's 3, 1 and 2. `objectstack-ai#3266`,
`objectstack-ai#8287`, `objectstack-ai#8543` and `objectstack-ai#8580` answer 200; the second number `objectstack-ai#8778` is
dead, so that one line is rewritten here.
- **A third spelling the gate cannot see, found by the raw scan.**
`NON_CITATION_HEADS` excuses any `#N` after the word 「option」 as an
option ordinal, so 「the option objectstack-ai#8710 rejected」
(`approval-service.ts:2329`) was never extracted: a dead number there
would pass the diff gate at exit 0 and never enter a census count. It is
rewritten here. Across the gate's declared surfaces at the base, the
only other `option #N` with three or more digits is
`packages/objectql/src/validation/rule-validator.ts:2202` (`option
objectstack-ai#14088`), which answers 200. Same family as objectstack-ai#20636; noted for its
closeout, not a card of its own.
- **A retired key name in this package's prose, not changed here.**
`tenancy.organizationField` left the authorable surface in `502f179cc`,
and limb 0 of the shared resolver now reads
`PLATFORM_STAMP_ORGANIZATION_COLUMNS` in `metadata-core`, keyed by
object name. Comments in this package still name the retired key as what
limb 0 reads (`sys-approval-request.object.ts:143`, the line above a
rewrite; `backfill-platform-row-organizations.ts:35`,
`approval-node.test.ts:463`, `approval-service.ts:2707`,
`backfill-platform-row-organizations.test.ts:50`), and two test fixtures
still declare it on a stub `sys_api_key` (`approval-node.test.ts:467`,
`backfill-platform-row-organizations.test.ts:54`), where it is inert
because the resolver keys by name. The anchor `7901b2dd2` is right for
the key those lines name, and nothing is wrong at runtime. Correcting
the prose would reach past the dead citations, and the fixtures are code
tokens, so none of it is changed here.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#16709` →
`8c7cca1ce`; `objectstack-ai#11081` → `c28e4cfae`; `objectstack-ai#11286` → `b019891cd`; `objectstack-ai#11308` →
`5a916c4d4`; `objectstack-ai#11311` → `1272f0a6b`; `objectstack-ai#8707` → `1408fe385`; `objectstack-ai#8863` →
`d200b016b`.
- **Base.** The branch is on `main` at `575746371`, which is still
`main` at 20:56Z (read into a private ref), so there was no merge.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…nstead of a deleted tracker number (stage 9) (objectstack-ai#20718)

Part of objectstack-ai#20234

Clause-②: no

Stage 9 of the dead-citation sweep. Four conversion `summary` literals
in `packages/spec/src/conversions/registry.ts` cited tracker numbers
that no longer exist on the board. A `summary` is author-shown: `os
migrate meta` prints it, and `gen:spec-changes` / `gen:upgrade-guide`
project it into `packages/spec/spec-changes.json` and
`docs/protocol-upgrade-guide.md`. So each now takes ruling C+D form D:
the decision in words, no number to look up. One stale comment clause at
`:2100` is rewritten to be true (form C). No conversion id, surface,
retirement state, transform or code token moves.

## What changes

| conversion | citation removed | decision now stated in words | decided
by |
|---|---|---|---|
| `datasource-driver-mongo-to-mongodb` | `(objectstack-ai#6345)` | appended: "so the
id that selects a driver and the id that selects its config contract are
one string with no mapping between them" | commit e2798fa (the entry's
own docblock states this rationale) |
| `translation-component-submit-label-removed` | `objectstack-ai#10926 — ` | "retired
rather than re-anchored", and appended: "and re-anchoring the key there
would only have added a second place to translate one word" | commit
d173125 (the maintainer chose retire over re-anchor) |
| `mapping-lookup-params-removed` | `objectstack-ai#10329, ` | appended: "Implementing
them instead would have added a second reference-resolution dialect to
the import path" | commit 15d58db |
| `connector-error-mapping-removed` | `objectstack-ai#14676, ` | appended: "deleting
the block resolves that collision without a rename" | commit 13c48c2 |

The other number in the submitLabel literal, objectstack-ai#9249, is live (200) and
stays. `ADR-0049` stays in the two literals that carried it.

The comment at `:2100` said the flag "does not mean what its name and
every docblock around it say it means". That has been false since commit
29dd1a6, which rewrote the flag's own docblock to state its
authoring-only reach. It now reads: "does not mean what its name says:
its own docblock (`RetiredConversionState` in `types.ts`) has stated the
authoring-only reach since commit 29dd1a6." Evidence: `git log -S"its
reach is the authoring surface" --
packages/spec/src/conversions/types.ts` answers 29dd1a6 only;
`rev-parse --disambiguate` count 1; `merge-base --is-ancestor` exit 0
against the base and against HEAD; `GET
/repos/objectstack-ai/objectstack/commits/29dd1a6dd` answers 200 with
the full sha, and the 4-character control `29dd` answers 422.

## Verification

Base `e4e5222b7b`; head `7ec0ef4c84` (the base plus three commits, then
`origin/main` `5757463712` merged through
`scripts/pm/os-regen-merge.sh`; that merge touched no path under
`packages/spec` or `scripts`, and the post-merge regeneration produced
no diff).

**Deadness.** REST `GET issues/N`, no redirects, with lit control objectstack-ai#20234
and dead control objectstack-ai#8710 at the start and end of each pass. At base,
registry.ts has 109 string sites holding 79 distinct unqualified numbers
of 100 or more: 75 answer 200 and 4 answer 404, exactly objectstack-ai#6345, objectstack-ai#10926,
objectstack-ai#10329, objectstack-ai#14676. At head: 105 string sites, 75 numbers, all 200. Controls
4/4 and 4/4 each pass.

**The gate's census** (`node scripts/check-issue-citations.mjs --census
--json`, board enumerated in full):

| | base e4e5222 (19:45Z, frontier objectstack-ai#20708) | head 7ec0ef4 (20:54Z,
frontier objectstack-ai#20717) |
|---|---|---|
| registry.ts findings | 0 | 0 |
| packages/spec/src findings | 235 (22 numbers: migrations/ 233,
data/api-derivation.ts 1, identity/identity.zod.ts 1) | 235, the
identical site set |
| sites of the four numbers elsewhere | objectstack-ai#6345 1, objectstack-ai#10926 1, objectstack-ai#10329 1,
objectstack-ai#14676 11 (release pages and migrations/) | the same |

Repo-wide allocated-but-absent goes 1222 to 1195. That moved with
`main`'s merged commits, not with this diff.

**Hypothesis falsified: neither count moves, by construction.** The
citation gate reads source through the comment-prose projection, which
blanks string literals, so these four sites were never in its census.
`scripts/doc-authoring-prose-id.baseline.json` excludes `packages/spec`
(`PACKAGES_PROSE_EXCLUDED`), and `check:doc-authoring`'s spec leg sweeps
message, strictObject, tombstone, describe and function-built positions,
not a conversion `summary`: it was green at base with all four numbers
present. At head it prints "16827 customer-facing string(s) across 1186
spec sources clean" and "808 pinned site(s) across 229 file(s) ... no
growth, no burn-down unrecorded".

**Residue.** A scratch instrument over TypeScript 6.0.3 builds (a) the
file with every comment token cut and everything else kept byte for byte
and (b) the leaf-token stream, with the four `summary` initializers
masked by conversion id. Base vs head: IDENTICAL (residue sha256 prefix
`3352f29d13665120` on both sides; 47,810 leaf tokens and 842 comment
tokens cut on both). Controls mutate the head text in memory. A comment
insertion stays IDENTICAL. Each of these DIFFERS: another summary
literal edited, a template literal, a regex literal, an appended
declaration, a masked conversion's `id`, and its `surface` (7/7).
Unmasking the mongo summary alone DIFFERS (exit 1). The instrument
evaluates each literal's value at base and at head: only the citation
and the added clause differ. Line balance for registry.ts: +16/-11
(12,678 to 12,683 lines).

**Regeneration.** `gen:spec-changes` and `gen:upgrade-guide`, never by
hand. Exactly 3 lines move, all objectstack-ai#6345's copy: `spec-changes.json` :229
and :1121, `docs/protocol-upgrade-guide.md` :188. Word diff on each:
`used (objectstack-ai#6345)` becomes `used, so the id that selects a driver and the id
that selects its config contract are one string with no mapping between
them`. `check:spec-changes` and `check:upgrade-guide` exit 1 before
regeneration and 0 after.

**Tests.** Spec sources at `31a296909f` are identical to head.
- Build under `os-verify-lock`: `turbo run build --concurrency=2` over
`./packages/*` and `./packages/*/*`, 71 of 71 (pre-merge and again at
head).
- `check:generated`: all 15 generated artifacts are up to date.
- Targeted run (`src/conversions src/migrations src/integration
src/data`, the translation and cron retirement files, the two step-18
merge scripts): 132 files, 4,732 passed.
- The whole `local` project: 575 files, 16,960 passed and 1 todo.
- `repo` project: 40 of 44 files, 645 passed. The other four
(`build-schemas-check-mode`, `def-key-collisions`,
`publish-smoke-boot-failure`, `publish-smoke-port-collision`) hit
`timeout 560` (exit 124). NOT MEASURED, left to CI. None reads summary
text.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.

**Gates at 7ec0ef4.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derives 78, the same list as
before the merge. All 78 exit 0, `check:issue-citations`,
`check:doc-authoring`, `check:generated` and `check:upgrade-guide` among
them. `--ran` reports 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN. Also
run: `check:spec-changes` and `check:authorable-surface`, both exit 0.

**Lint, a proven narrowing.** `eslint --no-inline-config --format json`
on registry.ts: 1 file, 0 errors, 0 warnings. `isPathIgnored` is false.
`eslint.config.mjs:327-328` states that type-aware linting is never
enabled, so a text edit cannot move an untouched file's verdict. The
repo-wide `pnpm lint` is CI's run.

**Changeset.** `patch`, with a standalone `Clause-②: no` line. `files[]`
ships `dist` and `spec-changes.json`. Each new phrase is in 6 dist
files, and the four old citation spellings are in 0. Control: the
unchanged `rateLimitConfig` summary is in 6.

**Merge probes.** From a bare `--shared` clone with no merge driver,
deleted afterwards: `merge-tree` of this head with objectstack-ai#20637's branch head
`a2abb8c78a` exits 0, and with objectstack-ai#20662's head `3fcedfb564` exits 0.

## Acceptance notes

- `docs/protocol-upgrade-guide.md:68` still carries objectstack-ai#6345. It is the
protocol-17 step rationale, generated from
`packages/spec/src/migrations/registry.ts:343`, so it moves with the
`migrations/` stage (233 comment sites there per the census). Stage 8's
record calls it hand-written, but it is generated.
- Not touched: the `service-datasource` and `metadata-core` sites of the
same numbers (objectstack-ai#20594 / objectstack-ai#20595's lanes), the test-file sites, and the
liveness JSON notes stage 8 listed.
- Neither the citation census nor `check:doc-authoring` reaches a
conversion `summary`, so a number that dies later in one goes unflagged.
Noted, not filed: it is a gate's coverage boundary, not a defect class,
and no new gate is proposed.
- The other summary literals in registry.ts still carry live tracker
numbers: 105 string sites, 75 distinct numbers, all 200 at 20:58Z (for
example `objectstack-ai#4911` in `connector-rate-limit-config-removed`, and objectstack-ai#9249 kept
here). Form D says anything an author is shown carries no number, which
would reach them too. This stage removes dead citations only, as its
claim requires. Whether live numbers in summaries become a later stage
is the seat's call.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…commits that decided them (objectstack-ai#20729)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the eighth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-analytics/src/**` and nothing else. By the
seat's census at the claim (`5899485578`), it is the largest package in
the lane that no in-flight work holds. 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 to 7 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`). That is **76 sites on 76 lines
in 22 files, covering 14 numbers**:

- 42 census sites (every census site this package has);
- 34 sites in test comments, which the census defers.

The raw scan found no dead site the gate's grammar cannot see (see
Acceptance notes), so there is no third class this time.

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **13 distinct shas**. No number in this package has an ADR or
ruling record of its own in the repository (a grep of `docs/adr/` for
all 14 finds none, and a grep of the rest of `docs/` finds none either),
so every anchor is a commit, per ruling C's order. No number was
dropped.

Only comments changed. Every touched source file keeps its line count
(78 lines out, 78 in, over 22 files), so no line citation into these
files moves. 2 of those 78 lines hold no dead citation: they are reflow
lines, 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: `objectstack-ai#10861` (5 lines), `objectstack-ai#12776` (3),
`objectstack-ai#10413` (2), `objectstack-ai#16750` (2), and `objectstack-ai#10759`, `objectstack-ai#11152`, `objectstack-ai#5716` and the
decision-batch ordinal `objectstack-ai#59` once each. Each tracker number among them
resolves. Over the whole diff, added minus removed is 0 or negative for
every number, and no number is new to the diff. No PR number is the
citation on an added line: the two `PR #N` spellings in scope became
their pull request's squash commit, and `objectstack-ai#16750` stays only as the
convenience link beside `ed7243d52`, on the line it already stood on.

Eight dead sites are left on purpose, all of them test strings (see the
list below).

One more file: a `patch` changeset for `@objectstack/service-analytics`,
because the rewritten docblocks and inline comments ship (see Changeset
below).

The `AnalyticsResultWithDrill` type and its four sidecar members are not
touched: its docblocks carry no dead number (`objectstack-ai#20644`, `objectstack-ai#3214` and
`objectstack-ai#1752` all resolve).

## Census: `service-analytics`, 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/services/service-analytics/`. 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` |
service-analytics sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `cbaf04c1f`, run 2026-09-29T21:41:53Z to 21:45:05Z |
enumerated, 186 pages, frontier objectstack-ai#20721 (newest objectstack-ai#20721 before and after),
18,548 numbers | 1,161 | **42** | 42 | 10 | 10 |
| after | head `967d73531`, run 21:55:23Z to 21:58:36Z | enumerated, 186
pages, frontier objectstack-ai#20723 (newest objectstack-ai#20723 before and after), 18,550 numbers
| 1,119 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim and A1 (42
sites): the two comments PR objectstack-ai#20712 rewrote in `analytics-service.ts` did
not move it. The whole-repo drop is 42, exactly this diff's census
sites. The `resolves` tally is 32,991 in both runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did
not move either. The after run was taken on `967d73531`; the head
`82d2b40b2` adds only the changeset. No run was truncated or discarded:
both enumerations read 186 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 `namesThisRepository`
over every `.ts` file under `service-analytics/src` (162 files). It
takes its verdicts from the before census's own board reading rather
than from a second enumeration: a number is dead when that census
reported it `allocated-but-absent`, and alive when that census judged it
on this board anywhere (its `--list` extraction) and did not report it.
The 21 numbers the census never saw, because they stand only in test
files or strings here, were read one by one on the issues endpoint: 17
answer 200, and `objectstack-ai#16778`, `objectstack-ai#16860`, `objectstack-ai#16918` and `objectstack-ai#17125` answer 404.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `cbaf04c1f` | 3,514 | **84** | 42 | 34 | 0 | 8 |
| after, `967d73531` | 3,438 | **8** | 0 | 0 | 0 | 8 |

Its src-comment column equals the census's 42, which is the control on
the second instrument. The 3,410 live citations and the 20 cross-repo
citations are the same in both readings, and the drop of 76 citations is
exactly the rewritten sites. A third, raw reading (every `#` followed by
2 to 6 digits, whatever surrounds it) finds 3,598 occurrences and 84
dead before, 3,522 and 8 after; its residue equals the gate's residue
site for site, and it sees no dead site beyond the gate.

## Per-number table

Sites and files count every dead occurrence 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, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (`merge-base --is-ancestor` exit 0 for each pair).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#11461` | 20/2 | 19/1 | `399ecad58`: a cross-object leaf in one
measure's own `filter` (the third producer, lowered onto
`aggregations[].filter`) is refused on both ObjectQL doors with
`INVALID_FIELD` / 400 naming the measure, folded into the one member
view, with insertion order keeping every earlier refusal's message. The
last line of its message names `objectstack-ai#11461` as the card it settles. New to
the sweep |
| `objectstack-ai#17130` | 17/5 | 13/4 | `54b3d1d4a` (PR objectstack-ai#17336): the row-scope
resolution refusals carry `READ_SCOPE_COMPILE_FAILED` / 500 through one
constructor, so `queryDataset`'s catch re-throws them instead of reading
their words, every message byte-unchanged; plus the source-derived
wording-collision guard. Named in its diff only (18 added lines carry
the tag). New to the sweep |
| `objectstack-ai#17124` | 12/8 | 10/2 | `86c505286` (PR objectstack-ai#17593):
`explicitDateRangeWindow` is the one reading of `dateRange`'s array arm
on all four faces, and an array that is not two string bounds is refused
with `ANALYTICS_DATE_RANGE_UNRECOGNIZED` / 400. Named in its diff only
(its changeset file is `17124-daterange-array-arm-arity.md`). New to the
sweep |
| `objectstack-ai#12209` | 10/5 | 10/0 | `017130a09` (PR objectstack-ai#12318): a custom-SQL measure
is refused on the ObjectQL aggregate path with `INVALID_FIELD` / 400,
keyed on the `EXPRESSION_METRIC_TYPES` partition shared with
`NativeSQLStrategy`. Its message records the two failure modes the lines
describe (`driver-sql` blaming a `function` key, the in-memory evaluator
answering `null` per bucket). Named in its diff only. New to the sweep |
| `objectstack-ai#16778` | 5/1 | 4/1 | `357f4992b`: the compile-leg refusal of an
aggregate a datetime measure's field type cannot carry, scoped to
temporal source fields. The squash commit of the pull request that was
`objectstack-ai#16778`; its subject carries the number. New to the sweep |
| `objectstack-ai#12940` | 4/2 | 4/0 | `aa16721b6` (PR objectstack-ai#13361): this package's
consumer-local `executeAggregate` config mirrors (the plugin options and
`AnalyticsServiceConfig`) narrow `aggregations[].method` to
`AggregationFunction`, after `objectstack-ai#12776` narrowed the contract. Named in
its diff only. New to the sweep |
| `objectstack-ai#17015` | 4/2 | 4/0 | `0da638cd9`: the closed `dateRange` preset
vocabulary is lowered once and the rest refused, the `[range, range]`
fallback is removed from the faces it reached, and the shared
conformance kit holds them. The squash commit of the pull request that
was `objectstack-ai#17015`. New to the sweep |
| `objectstack-ai#16860` | 3/1 | 3/0 | `041d9fdc6`: the object-level read grant is
asked at the analytics door, and its bridge to the `security` service
resolves an explicit three-way (absent admits; throwing or method-less
denies at `error`, finding F3 in its message). The squash commit of the
pull request that was `objectstack-ai#16860`. New to the sweep |
| `objectstack-ai#12248` | 2/1 | 2/0 | `8425c17cc`: the five ruled engine members,
`getDriverForObject?` and `resolveEffectiveDatasource` among them,
adopted onto `IDataEngine`, and `getObject` typed. Its subject names it.
Stage 5's and the spec stage's anchor |
| `objectstack-ai#16685` | 2/2 | 2/0 | `ed7243d52` (PR objectstack-ai#16750): `boolean` / `toggle`
accepted for `sum` / `avg` / `min` / `max` in the aggregate × field-type
table, holding maintainer ruling `objectstack-ai#11152`. Its subject names it. The
spec stage's anchor |
| `objectstack-ai#17125` | 2/2 | 2/0 | `5d12b16e7`: the row-scope bridge tells an
absent security service from a broken one, so a broken one refuses the
query. The squash commit of the pull request that was `objectstack-ai#17125` (404 on
the pulls endpoint too). New to the sweep |
| `objectstack-ai#16918` | 1/1 | 1/0 | `5d12b16e7`: the same commit. Its changeset's
headline names `objectstack-ai#16918` as the card it answers, and its diff writes the
line (`admission-bridge-resolution.test.ts:120`) |
| `objectstack-ai#6123` | 1/1 | 1/0 | `59d1933f9`: `err.code` lands at `error.code`,
not `error.details.code`; the commit that wrote this very line. The
`runtime` stage's anchor |
| `objectstack-ai#13279` | 1/1 | 1/0 | `6a180e42d`: permission-store read failures
fail loud, and the same commit renames
`metadata/src/utils/schema-sync-errors.ts` to
`packages/types/src/driver-error-classification.ts`, the move the line
describes. The anchor of stages 2, 5 and 6, and of the `types`, `rest`
and `runtime` stages |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 13), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 13;
control leg: stage 1's landing `422db788a` exit 0; the history is
complete, `--is-shallow-repository` false, 15,135 commits). Each of the
14 numbers answers 404 on the issues endpoint, read one by one;
`objectstack-ai#16778`, `objectstack-ai#16860`, `objectstack-ai#17015` and `objectstack-ai#17125` answer 404 on the pulls
endpoint too.

## Wordings to check

- **Bracket tags.** `[#N]` became `[commit SHA]`, as in stage 7;
`[objectstack-ai#10861 / objectstack-ai#11461]` and `[objectstack-ai#10861, objectstack-ai#11461]` keep the live `objectstack-ai#10861` beside
the new sha.
- **The boolean rows, `measure-result-type.ts:115-116` and
`aggregate-datetime-measure-refusal.test.ts:65-66`.** 「objectstack-ai#16685 ruled A,
landed as objectstack-ai#16750」 and 「objectstack-ai#16685 was ruled A and objectstack-ai#16750 added」 became
「commit ed7243d (objectstack-ai#16750) added those rows」 and 「commit ed7243d
(objectstack-ai#16750) added」. 「ruled A」 named an option on the dead card;
`ed7243d52`'s message records the decision itself. Line 116 of the first
file and line 66 of the second are the 2 reflow lines: each keeps the
`objectstack-ai#16750` it already carried.
- **PR numbers, `read-scope-resolution-envelope.test.ts:25` and
`refusal-wording-collision.test.ts:21`.** 「PR objectstack-ai#17125's refusal」 became
「Commit 5d12b16's refusal」, the pull request's squash commit.
- **`read-scope-refusal.ts:29`.** 「objectstack-ai#17130 exists to remove it」 became
「commit 54b3d1d was made to remove it」, the form stage 6 used.
- **`refusal-wording-collision.test.ts:49`.** 「the exact move objectstack-ai#17130
forbids」 became 「the exact move commit 54b3d1d ruled out」; its message
says the fix is the declaration, not a luckier string.
- **`read-scope-resolution-envelope.test.ts:161`.** The verb after the
number moved from present to past tense with the sha.
- **`measure-expression-both-strategies.test.ts:45` and `:166`.**
「deleting the objectstack-ai#12209 arm in」 became 「deleting the arm commit 017130a
added in」, and 「every objectstack-ai#12209 refusal」 became 「every custom-SQL refusal
(commit 017130a)」.
- **`dataset-executor.ts:609`.** 「objectstack-ai#17015's kit」 became 「commit
0da638c's kit」, the conformance kit that commit built.
- **`plugin.ts:116`.** 「and in objectstack-ai#12209:」 became 「and in commit
017130a:」, whose message records the two ways the engine failed.
- **`analytics-service.ts:238`.** 「objectstack-ai#13279 moved it there」 became 「commit
6a180e4 moved it there」; that commit's diff is the rename.

## The 8 sites left

- **Test strings, 8 sites**, left as stages 1 to 7 left theirs, all
`describe` / `it` titles:
  - `crossobject-conjunct-refusal.test.ts:589` (`objectstack-ai#11461`);
  - `aggregate-nontemporal-measure-refusal.test.ts:243` (`objectstack-ai#16778`);
  - `date-range-array-arm-arity.test.ts:213` and `:294` (`objectstack-ai#17124`);
- `read-scope-resolution-envelope.test.ts:155`, `:199` and `:226`, and
`refusal-wording-collision.test.ts:336` (`objectstack-ai#17130`).
- There is no operator string, generated file or quoted ruling carrying
a dead number in this package. It has no generated file at all.

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as
trivia and JSDoc nodes never visited, base `cbaf04c1f` against head.
Template literals are therefore read in context. It ran over all 22
touched `.ts` files.

- Real run: 26,705 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `plugin.ts` (「refusal buys is in」 to 「refusal earns
is in」): 0 files changed, as expected (exit 0).
- Positive control, a code token added in `plugin.ts` (`field: a.field,`
given `as string`): DIFFER (exit 1).
- Positive control, one digit changed inside a kept test title
(`date-range-array-arm-arity.test.ts:213`): 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 (`ad3dc9fff4d3`, `a606ffbb6ead`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-analytics`
(`.changeset/20596-service-analytics-provenance-anchors.md`) is
included. Its body is stage 7's, word for word, with the package name
changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build (a cache miss for this package, so
`dist` is this head's source), the rewritten comments reach `dist`:
`399ecad58` 6 times in each of `dist/index.js`, `index.cjs`,
`index.d.ts` and `index.d.cts`; `86c505286` twice in each JS file and
once in each declaration file; `54b3d1d4a` once in all four; `aa16721b6`
once in each JS file and twice in each declaration file; `017130a09`
once in each JS file. Positive controls: the unchanged line 「none of the
coverage: a compiled measure's own」, in the same docblock as the shipped
rewrite at `objectql-strategy.ts:744`, is found once in each of the four
files, and the unchanged line 「back into line. Widening it here again
would not be a local matter」 beside the shipped rewrite at
`analytics-service.ts:559` once in each declaration file. A
never-written negative phrase appears nowhere in `dist`. None of the 14
dead numbers is left anywhere in `dist`.

## Gates (head `82d2b40b2`)

- **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 11 citations across 10 files; 10 resolve and
1 resolves as a pull request (`objectstack-ai#16750`, the convenience link that
already stood on its line).
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `82d2b40b2` derived 62 commands:
all 56 derived at dispatch, plus `check:engine-double-contract`,
`check:objectql-double-limit`, `check:query-options-erasure`,
`check:type-check-coverage`, `check:type-check-debt` and
`check:where-matcher`. Each ran with its exit code captured before any
pipe, and all 62 exit 0. `--ran`, fed each command with its exit code,
reports 62 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.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/service-analytics test`: 137 files pass
and 3,216 tests pass. That is every test file in the package, the 12
touched ones included.
- `pnpm --filter @objectstack/service-analytics typecheck` exits 0 (`tsc
--noEmit` on `tsconfig.json`). `--listFiles`: the program holds all 162
files under `src/`, the 137 test files and all 22 touched files
included.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 22 touched `.ts` files gives 22 files, 0 errors and 0
warnings. All 22 are in eslint's own population (`isPathIgnored` is
false for each; a `dist` file, as the control, is ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), 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 23 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word
「option」. In this package:
- `#N-word`: 8 lines by a plain grep, and 7 once a hyphen before the `#`
is excluded too, which is the claim's 7. The eighth is
「pre-objectstack-ai#10413-phase-2」 (`execution-context-bridge.test.ts:223`). The
numbers, `objectstack-ai#10413`, `objectstack-ai#5298`, `objectstack-ai#13570` and `objectstack-ai#13640`, all resolve.
- `#A/#B`: 29 lines, the claim's 29, over 28 distinct numbers. All
resolve; `objectstack-ai#2149`, which the census never judged, was read on its own.
  - `option #N`: none.
So nothing here needed a rewrite beyond the gate, and the raw scan
agrees.
- **「This card」 phrases are left.** 113 lines in 39 files of this
package speak of 「this card」, 「that card」 or 「the card」. They carry no
number, neither instrument sees them, and most sit in blocks whose
numbers still resolve. Stage 7 rewrote two such lines as lost referents;
here none is changed, because the phrase runs through the whole package
and rewriting a subset would be arbitrary.
- **Prose that names `queryDataset`'s catch, not changed.** Nine comment
lines say `queryDataset`'s catch. Since `10c36cc43` that catch sits in
the private `answerDataset`, whose docblock calls it the body of
`queryDataset`, so the lines still hold at the level of the public
method. This is not a dead citation, so it is outside this stage.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#11461` →
`399ecad58`; `objectstack-ai#17130` → `54b3d1d4a`; `objectstack-ai#17124` → `86c505286`; `objectstack-ai#12209` →
`017130a09`; `objectstack-ai#16778` → `357f4992b`; `objectstack-ai#12940` → `aa16721b6`; `objectstack-ai#17015` →
`0da638cd9`; `objectstack-ai#16860` → `041d9fdc6`; `objectstack-ai#17125` and `objectstack-ai#16918` →
`5d12b16e7`.
- **Base.** The branch is on `main` at `cbaf04c1f`. `main` has since
moved four commits (`3711e0b76`, `61455de27`, `6afccda5a`, `671d4c164`).
They touch `packages/spec`, `packages/metadata/package.json`,
`pnpm-lock.yaml`, docs and changesets, and no file under
`service-analytics` or in this diff, so no merge was taken; the merge
queue rebuilds on the merged generation. One of them, `671d4c164`,
declares the four drill-through sidecars on `AnalyticsResult` in the
spec. This diff leaves the local `AnalyticsResultWithDrill` untouched,
as the claim requires.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ts that decided them (objectstack-ai#20737)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the ninth stage of the `domain:services` lane of the
dead-citation sweep. It covers `packages/plugins/plugin-audit/src/**`
and nothing else. By the seat's census at the claim (`5900808881`), it
is the largest package in the lane that no in-flight work holds. 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 to 8 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`, PR objectstack-ai#20729 as `d2820876f`). That
is **56 sites on 55 lines in 16 files, covering 16 numbers**:

- 23 census sites (every census site this package has);
- 32 sites in test comments, which the census defers;
- 1 site the gate's grammar cannot see: the slash-joined second number
in `objectstack-ai#9719/objectstack-ai#9798` (`comment-access-hooks.ts:35`).

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **15 distinct shas**. No number in this package has an ADR or
ruling record of its own in the repository (a grep of `docs/adr/` for
all 16 finds none; the rest of `docs/` cites `objectstack-ai#11507` and `objectstack-ai#11374` only
as evidence, in an audit table and a QA checklist), so every anchor is a
commit, per ruling C's order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(56 lines out, 56 in, over 16 files), so no line citation into these
files moves. 1 of those 56 lines holds no dead citation: it is a reflow
line, 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: `objectstack-ai#10101` (3 lines), `objectstack-ai#8287` (3),
`objectstack-ai#5928` (2), `objectstack-ai#9974` (2), `objectstack-ai#4630` (2), and `objectstack-ai#8144`, `objectstack-ai#9719`, `objectstack-ai#12069`
and `objectstack-ai#19054` once each. Each resolves. Over the whole diff, added minus
removed is 0 for every number, and no number is new to the diff. No PR
number is the citation on an added line: the two `PR #N` spellings in
scope became their pull request's squash commit.

23 dead sites are left on purpose, all of them string literals (see the
list below).

One more file: a `patch` changeset for `@objectstack/plugin-audit`,
because some of the rewritten docblocks and inline comments ship (see
Changeset below).

## Census: `plugin-audit`, 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-audit/`. 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-audit sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `d2820876f`, run 2026-09-29T23:11:55Z to 23:15:11Z |
enumerated, 186 pages, frontier objectstack-ai#20735 (newest objectstack-ai#20735 before and after),
18,562 numbers | 1,110 | **23** | 22 | 6 | 12 |
| after | head `a9a4ea478`, run 23:26:11Z to 23:29:20Z | enumerated, 186
pages, frontier objectstack-ai#20735 (newest objectstack-ai#20735 before and after), 18,562 numbers
| 1,087 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim and A1 (23
sites). The whole-repo drop is 23, exactly this diff's census sites. The
`resolves` tally is 32,995 in both runs, and `resolves-as-pull-request`
(1,984) and `cross-repo-unjudged` (995) did not move either. The after
run was taken on `a9a4ea478`; the head `d6e67afa5` adds only the
changeset. No run was truncated or discarded: both enumerations read 186
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 `namesThisRepository`
over every `.ts` file under `plugin-audit/src` (45 files). It takes its
verdicts from the before census's own board reading rather than from a
second enumeration: a number is dead when that census reported it
`allocated-but-absent`, and alive when that census judged it on this
board anywhere (its `--list` extraction, 37,084 citations over 2,613
files) and did not report it. The 10 numbers the census never saw,
because they stand only in test files or strings here, were read one by
one on the issues endpoint: 7 answer 200 (`objectstack-ai#602`, `objectstack-ai#1532`, `objectstack-ai#4186`,
`objectstack-ai#7291`, `objectstack-ai#7333`, `objectstack-ai#16312`, `objectstack-ai#20494`), and `objectstack-ai#8852`, `objectstack-ai#12143` and
`objectstack-ai#12147` answer 404, on the pulls endpoint too.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `d2820876f` | 655 | **77** | 23 | 32 | 5 | 17 |
| after, `a9a4ea478` | 600 | **22** | 0 | 0 | 5 | 17 |

Its src-comment column equals the census's 23, which is the control on
the second instrument. The 572 live citations and the 6 cross-repo
citations are the same in both readings, and the drop of 55 citations is
exactly the rewritten sites the gate's grammar sees. A third, raw
reading (every `#` followed by 2 to 6 digits, whatever surrounds it)
finds 672 occurrences and 79 dead before, 616 and 23 after. Beyond the
gate's grammar it sees 2 dead sites before (the `objectstack-ai#9719/objectstack-ai#9798` comment,
rewritten, and the `[objectstack-ai#8203/objectstack-ai#11507]` test title, left) and 1 after (that
title). Its only unjudged tokens are `objectui#10520`, `cloud#340`,
`cloud#1395` and the decision-batch ordinal `objectstack-ai#153`.

## Per-number table

Sites and files count every dead occurrence 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, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (`merge-base --is-ancestor` exit 0 for all 56 line and anchor
pairs).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#11507` | 26/8 | 10/16 | `88b9d749a`: `sys_activity.type` is declared
an open, author-extensible vocabulary whose options are the built-in
set, per the maintainer ruling of 2026-08-24, direction 4. Its body
names `objectstack-ai#11507` twice. The spec stages' anchor |
| `objectstack-ai#8707` | 12/2 | 9/3 | `1408fe385`: an audit row is stamped from the
record's own organization, not the actor's, applying the maintainer's
ruling on `objectstack-ai#8287`; the precedence flips to `recordOrgId ??
sess.tenantId`, and the organization column is resolved from the schema
(`resolveRecordOrganizationField`, first written in this file). Its
subject names it. Stage 7's anchor |
| `objectstack-ai#9798` | 8/2 | 7/1 | `c7655d472` (PR objectstack-ai#9993): the `sys_comment`
access-hook registration declares the whole-operation dispatch `objectstack-ai#9719`
built, so the `objectstack-ai#4630` unscoped multi-delete refusal reaches the handler
through the wired engine; the update half is split out. Its body ends
with the closing line for `objectstack-ai#9798`. The `lint` stage's anchor |
| `objectstack-ai#16829` | 7/3 | 6/1 | `8d4690b8f`: the read-audit ledger write
declares `preserveAudit`, so a record-view row keeps the VIEW instant;
`isSystem` is kept for the readonly strip, and the new integration pin
runs the real stamp hook. Its body ends with the closing line for
`objectstack-ai#16829`. New to the sweep |
| `objectstack-ai#6575` | 4/2 | 4/0 | `69787f07b`: the hook registration surface gains
`excludeObjects` ("global except these objects"), refusing `'*'` and
blank members on it. The squash commit of the pull request that was
`objectstack-ai#6575` (404 on the pulls endpoint too); `objectstack-ai#5928`, the card it answers,
stays beside it. New to the sweep |
| `objectstack-ai#11374` | 4/3 | 3/1 | `f64668d3c`, the squash commit of `objectstack-ai#12143`, for
the two object comments: sourced bounds on the keyed text columns
`sys_activity.record_id` and `sys_audit_log.record_id` (255, the
physical `id` column), route A. `3954fb7df`, for the test's statement of
the rule: the route A ruling that keyed identity columns declare a
sourced `maxLength`; its subject names `objectstack-ai#11374 route A`. Both are stage
4's anchors for the sibling lines in `plugin-security` |
| `objectstack-ai#10091` | 3/3 | 3/0 | `da891e0ef` (PR objectstack-ai#10169): `sys_attachment`'s
`beforeUpdate` gate, uploader or parent editor, with the attach rule on
the NEW parent when a row is re-pointed. Its body names `objectstack-ai#10091`. Stage
6's anchor |
| `objectstack-ai#14927` | 3/2 | 3/0 | `ab489388b` (PR objectstack-ai#17450): a lost audit row is
reported once per cause, keyed on the error `code`, and the datasource
remedy prints only for the missing-table cause; its message records that
the measured `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` refusal had sent
its operator to a working datasource. It names `objectstack-ai#14927` in its diff only
(the 3 lines it wrote). New to the sweep |
| `objectstack-ai#8778` | 3/1 | 2/1 | `7901b2dd2` (PR objectstack-ai#8905): the stamp-only
`tenancy.organizationField`, option A per the maintainer ruling on
`objectstack-ai#8778`. Its subject names it. The anchor of stages 4, 6 and 7 |
| `objectstack-ai#6523` | 2/2 | 2/0 | `aa4b90d9a` (PR objectstack-ai#7068): enforcement contracts
take the full `ExecutionContext`. Its subject names `objectstack-ai#6523` |
| `objectstack-ai#6206` | 2/2 | 2/0 | `aa4b90d9a`: the same commit, whose body applies
"the objectstack-ai#6206 ruling default (converge on the full envelope, keep no
per-site subset contracts)", written as the full-envelope ruling, the
form of stages 2, 6 and 7 |
| `objectstack-ai#8852` | 1/1 | 1/0 | `51bb277ef`: the `sys_activity.type` writer
census; its message records the objectui mirror as unguarded in both
directions, filed as `objectstack-ai#8852`, and not asserted here because this package
cannot import objectui. The commit that wrote the line. New to the sweep
|
| `objectstack-ai#11674` | 1/1 | 1/0 | `1cba33f16` (PR objectstack-ai#11961): the load-time warning
and the ordering constraint documented at the four pointer-pair sites.
Its subject names it; blame puts the line in it. Stage 7's anchor |
| `objectstack-ai#12147` | 1/1 | 1/0 | `945e91a13`: the class-level keyed-text-bounds
gate over every `*.object.ts`, retiring the per-package rule this file
carried. It names `objectstack-ai#12147` in its diff only. Stage 4's anchor for the
sibling file |
| `objectstack-ai#12143` | 1/1 | 1/0 | `f64668d3c`: the squash commit of the pull
request that was `objectstack-ai#12143` (404 on both endpoints), where the
dependency-graph measurement was made. Stage 4's anchor |
| `objectstack-ai#11671` | 1/1 | 1/0 | `09b4f4e4e` (PR objectstack-ai#12557): records which source
revision a generated translation leaf was filled from. The anchor the
identical `translations/index.ts` line already carries in five packages
on `main` |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 15), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 15;
control leg: stage 1's landing `422db788a` exit 0; the history is
complete, `--is-shallow-repository` false, 15,143 commits). Each of the
16 numbers answers 404 on the issues endpoint.

## Wordings to check

- **Bracket tags.** `[#N]` became `[commit SHA]`; `[#N route A]` became
`[commit f64668d, route A]`, the form stage 4 gave the sibling lines;
`[objectstack-ai#8707 / objectstack-ai#10101]` and `[objectstack-ai#8144 / objectstack-ai#8707 / objectstack-ai#10101]` keep the live numbers
beside the sha.
- **`sys-activity.object.ts:59-60`.** 「Maintainer ruling 2026-08-24 on /
objectstack-ai#11507 (direction 4 of the four that card framed)」 became 「Maintainer
ruling 2026-08-24, / executed by commit 88b9d74 (direction 4 of the
four weighed)」. Line 59 is the one reflow line: only its last word
changed, and it carries no number. 「that card」 would have lost its
referent.
- **「re-open objectstack-ai#11507」**, `sys-activity.object.ts:92` and
`activity-type-vocabulary-enforcement.test.ts:332`, became 「re-open the
ruling (commit 88b9d74)」: a card that answers 404 cannot be re-opened,
and the instruction is about the decision.
- **`sys-activity-type-open-vocabulary.test.ts:14`**, the attribution
above a verbatim maintainer ruling: 「on objectstack-ai#11507 (direction 4 of the four
the card framed)」 became 「on the card behind commit 88b9d74 (direction
4 of the four it framed)」, so line 15's 「Recorded on the card as:」 keeps
its referent. Line 15, which carries the ruling 「四维分析一致的,接手你的建议。」, and
the quoted block under it are untouched.
- **Headings.** 「what the ruling on objectstack-ai#11507 changed」 and 「the objectstack-ai#11507
ruling」 became 「the open-vocabulary ruling (commit 88b9d74)」; 「objectstack-ai#11507
— the declaration」 became 「Commit 88b9d74 — the declaration」.
- **PR numbers.** 「objectstack-ai#5928 / PR objectstack-ai#6575」 (`audit-writers.ts:193`) and
「(objectstack-ai#5928, PR objectstack-ai#6575)」 (`audit-hook-object-scope.test.ts:19`) became
`commit 69787f0` beside the kept `objectstack-ai#5928`; 「measured on PR objectstack-ai#12143」
(`plugin-keyed-text-bounds.test.ts:21`) became 「measured on commit
f64668d」.
- **The `objectstack-ai#8707` ruling phrases**, `audit-writers.test.ts:1882` and
`:1922`. 「the ORDER the objectstack-ai#8707 ruling set」 became 「the ORDER commit
1408fe3 set」, and 「objectstack-ai#8707's ruling reasons about」 became 「commit
1408fe3 reasons about」: the ruling was the maintainer's on `objectstack-ai#8287`,
which stays on those lines, and `1408fe385`'s message carries the
reasoning. `audit-writers.ts:1402` 「because objectstack-ai#8707 reordered」 became
「because commit 1408fe3 reordered」, the flip its message states.
- **`objectstack-ai#14927`, three lines.** 「the cause measured on objectstack-ai#14927」 became 「the
cause commit ab48938 records」 (`audit-writers.ts:789`,
`audit-writers.test.ts:1211`), and 「The measured objectstack-ai#14927 misdirection」
became 「The misdirection commit ab48938 records」
(`audit-writers.test.ts:1370`).
- **`sys-activity-type-vocabulary.test.ts:91`.** 「Filed as objectstack-ai#8852;」
became 「Commit 51bb277 recorded it;」.
- **`comment-access-hooks.test.ts:404-405`.** 「the objectstack-ai#6523 contract's unit
is the envelope / and objectstack-ai#6206 forbids」 became 「the unit of commit
aa4b90d's contract is the envelope / and the full-envelope ruling
forbids」; `comment-access-hooks.ts:222` 「(objectstack-ai#6523 / the objectstack-ai#6206 ruling)」
became 「(commit aa4b90d / the full-envelope ruling)」.
- **`audit-writers.test.ts:1648`**, a section rule: the trailing rule
was shortened from 10 characters to 2 so the line stays near its old
width. `:1653` 「That day is objectstack-ai#8778」 became 「That day came with commit
7901b2d」.
- **`read-audit.test.ts:43`.** 「precisely how / objectstack-ai#16829 shipped」 became
「precisely how / the defect fixed by commit 8d4690b shipped」.
- **`comment-access-hooks.ts:35`**, the gate-invisible site:
「(objectstack-ai#9719/objectstack-ai#9798 built」 became 「(objectstack-ai#9719/commit c7655d4 built」.

## The 23 sites left

- **Source strings, 5 sites**, all `objectstack-ai#11507`: the `sys_activity.type`
field's `description` (`objects/sys-activity.object.ts:121`) and its
four generated copies
(`translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts:125`). They
are runtime strings, all five are held by the shrink-only
`doc-authoring-prose-id` baseline, and the generated files are left as
A5 says. They ship in `dist` (see Changeset).
- **Test strings, 18 sites**, left as stages 1 to 8 left theirs:
- `describe` / `it` titles:
`activity-type-vocabulary-enforcement.test.ts:315` (the gate-invisible
`[objectstack-ai#8203/objectstack-ai#11507]`), `sys-activity-type-open-vocabulary.test.ts:70`
(`objectstack-ai#11507`), `audit-writers.test.ts:1420`, `:1659` (two sites, `objectstack-ai#8707`
and `objectstack-ai#8778`) and `:1873` (`objectstack-ai#8707`), `comment-access-hooks.test.ts:690`
(`objectstack-ai#9798`), `plugin-keyed-text-bounds.test.ts:90` (`objectstack-ai#11374`),
`read-audit-view-instant-preservation.integration.test.ts:121`
(`objectstack-ai#16829`);
- assertion and hint messages, all `objectstack-ai#11507`:
`activity-type-vocabulary-enforcement.test.ts:249`, `:352`, `:355`,
`:378`, `:380`, and `sys-activity-type-open-vocabulary.test.ts:83`,
`:90`, `:110`, `:152`.
- No quoted maintainer ruling in this package carries a dead number. The
package's generated `*.source-hashes.generated.ts` headers carry none
either (PR objectstack-ai#20656 fixed their producer).

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as
trivia and JSDoc nodes never visited, base `d2820876f` against head.
Template literals are therefore read in context. It ran over all 16
touched `.ts` files.

- Real run: 19,445 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `audit-writers.ts` (「the cause commit ab48938
records」 to 「… recorded」): 0 files changed, as expected (exit 0).
- Positive control, a code token added in `audit-writers.ts`
(`createRecordOrganizationResolver(engine)` given `as any`): DIFFER
(exit 1).
- Positive control, one digit changed inside a kept test title
(`audit-writers.test.ts:1873`, `objectstack-ai#8707` to `objectstack-ai#8708`): 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 (`2dbd2059e8f5`, `8ec28790da22`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/plugin-audit`
(`.changeset/20596-plugin-audit-provenance-anchors.md`) is included. Its
body is stage 8's, word for word, with the package name changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build, part of the rewritten prose reaches
`dist`: `c7655d472` twice in each of `dist/index.js` and `index.mjs` and
once in each of `index.d.ts` and `index.d.mts` (the
`CommentAccessEngine` option docblock is on an exported interface);
`88b9d749a` and `f64668d3c` twice, and `1cba33f16` and `8d4690b8f` once,
in each JS file (the object-definition comments and a `read-audit.ts`
comment). The comments in `audit-writers.ts` and `translations/index.ts`
do not reach `dist` (0 for each of their anchors). Positive controls:
the unchanged line 「below carries into the contract; this comment
carries the reasoning.」, in the same docblock as the shipped rewrite at
`sys-activity.object.ts:60`, is found once in each JS file, and the
unchanged line beside the shipped rewrite at
`comment-access-hooks.ts:77` once in each declaration file. A
never-written negative phrase appears nowhere in `dist`. Of the 16 dead
numbers, only `objectstack-ai#11507` is left in `dist`, 5 times in each JS file: the
kept `description` string and its four generated copies.

## Gates (head `d6e67afa5`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test, 114 cases, 8 batteries) exits 0. `node
scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged
11 citations across 6 files, and all 11 resolve (they are the live
numbers that already stood on the rewritten lines).
- **Doc authoring:** `pnpm check:doc-authoring` exits 0; the
sibling-package prose-id baseline holds (808 pinned sites, no growth),
which includes the five kept `objectstack-ai#11507` strings.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `d6e67afa5` derived 64 commands:
all 57 derived at dispatch, plus `check:dispatcher-error-vocabulary`,
`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`. Each ran with its
exit code captured before any pipe, and all 64 exit 0. `--ran`, fed each
command with its exit code, reports 64 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.
- The derivation warns that its tree is 3 commits behind `origin/main`
and that one input, `scripts/engine-double-contract.pinned.json`,
changed there: `main` added one pinned row for
`packages/objectql/src/protocol-packaged-dashboard-base.test.ts`, a file
outside this diff. The family is in the 64 either way and exits 0 on
this tree.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/plugin-audit test`: 26 files pass and 366
tests pass. `vitest list --filesOnly` names 26 files, all the tracked
test files, the 10 touched ones included.
- `pnpm --filter @objectstack/plugin-audit typecheck` exits 0. `tsc
--listFiles`: `tsconfig.json` holds the 6 touched source files (19 `src`
files; it excludes tests), and `tsconfig.test.json`, which the script's
`check:test-typecheck` step compiles, holds all 45 files under `src/`,
all 16 touched files included.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 16 touched `.ts` files gives 16 files, 0 errors and 0
warnings. All 16 are in eslint's own population (`isPathIgnored` is
false for each; a `dist` file, as the control, is ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), 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 17 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word
「option」. In this package:
  - `#N-word`: none.
- `#A/#B`: 11 lines, the claim's 11, over 14 distinct numbers. Two
second numbers are dead: `objectstack-ai#9798` in `comment-access-hooks.ts:35`,
rewritten, and `objectstack-ai#11507` in the test title
`activity-type-vocabulary-enforcement.test.ts:315`, left as a string.
The other 12 numbers resolve.
  - `option #N`: none.
The raw scan agrees: nothing dead beyond the gate is left outside a kept
string.
- **The kept `description` string is a runtime string with a dead
number.** `sys_activity.type`'s `description` ships to the metadata API,
the i18n bundles and `dist`, and ends 「(maintainer ruling 2026-08-24,
objectstack-ai#11507)」. It and its four generated copies are held by the
`doc-authoring-prose-id` baseline, so they belong to the runtime-string
lane (form D), not to this stage, as stages 1, 2 and 4 left theirs.
- **「This card」 phrases are left.** 46 lines in 21 files of this package
speak of 「this card」, 「that card」 or 「the card」. They carry no number
and neither instrument sees them. Two were rewritten here because the
rewrite on their own line removed their referent
(`sys-activity.object.ts:60`,
`sys-activity-type-open-vocabulary.test.ts:14`); the rest are unchanged,
as in stage 8.
- **Dead `objectstack-ai#11507` and `objectstack-ai#11374` outside the census surface.**
`docs/qa/platform-checklist/areas/records-forms.json` (4 lines) and
`docs/audits/gate-census-2026-09.md` (1 line) cite them as evidence.
`docs/` is outside this stage's surface; noted for objectstack-ai#20556, the carrier
of dead citations outside `packages/spec/src`.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#16829` →
`8d4690b8f`; `objectstack-ai#6575` → `69787f07b`; `objectstack-ai#14927` → `ab489388b`; `objectstack-ai#8852` →
`51bb277ef`; `objectstack-ai#9798` → `c7655d472`; `objectstack-ai#11507` → `88b9d749a`.
- **Base.** The branch is on `main` at `d2820876f`. `main` has since
moved three commits (`f05919b82`, `99786f930`, `1940afdaf`). They touch
`packages/spec`, `packages/metadata-protocol`, one new
`packages/objectql` test file, a design doc, three changesets and
`scripts/engine-double-contract.pinned.json` (one added row for that
test file), and no file under `plugin-audit`,
`scripts/check-issue-citations.mjs` or `.changeset/config.json`, so no
merge was taken; the merge queue rebuilds on the merged generation.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…mmits that decided them (objectstack-ai#20742)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the tenth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-package/src/**` and nothing else. By the
seat's census at the claim (`5901757839`), it is the largest package in
the lane that no in-flight work holds. 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 to 9 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`, PR objectstack-ai#20729 as `d2820876f`, PR
objectstack-ai#20737 as `4dfff176b`). That is **18 sites on 17 lines in 5 files,
covering 5 numbers**:

- 13 census sites (every census site this package has);
- 5 sites in test comments, which the census defers;
- no site the gate's grammar cannot see (the package has none, see
Acceptance notes).

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **4 distinct shas**. No number in this package has an ADR or
ruling record of its own in the repository (a grep of `docs/adr/` and
`scripts/adr-anchors/` for all 5 finds none, and nothing else under
`docs/` names them), so every anchor is a commit, per ruling C's order.
No number was dropped.

Only comments changed. Every touched source file keeps its line count
(17 lines out, 17 in, over 5 files), so no line citation into these
files moves. Every one of the 17 changed lines carried a dead citation;
there is no reflow line. No code token moves (see the guard below).

**No citation number is added.** The one tracker number on an added
line, `objectstack-ai#10677`, was already on the line it replaces (`index.ts:234`) and
resolves. Over the whole diff, added minus removed is 0 for `objectstack-ai#10677` and
negative for the five dead numbers, and no number is new to the diff. No
PR number is the citation on an added line: the three `PR #N` spellings
in scope became their pull request's squash commit.

4 dead sites are left on purpose, all of them `describe` titles (see the
list below).

One more file: a `patch` changeset for `@objectstack/service-package`,
because one rewritten docblock ships (see Changeset below).

## Census: `service-package`, 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/services/service-package/`. 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` |
service-package sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `4dfff176b`, run 2026-09-30T00:41:15Z to 00:44:24Z |
enumerated, 186 pages, frontier objectstack-ai#20741 (newest objectstack-ai#20741 before and after),
18,568 numbers | 1,082 | **13** | 12 | 1 | 4 |
| after | head `34ba921e6`, run 00:49:33Z to 00:52:49Z | enumerated, 186
pages, frontier objectstack-ai#20741 (newest objectstack-ai#20741 before and after), 18,568 numbers
| 1,069 | **0** | 0 | 0 | 0 |

The before count matches the seat's census and A1 (13 sites). The
whole-repo drop is 13, exactly this diff's census sites. The `resolves`
tally is 33,003 in both runs, and `resolves-as-pull-request` (1,984) and
`cross-repo-unjudged` (995) did not move either. The after run was taken
on `34ba921e6`; the head `ffd2f1ed2` adds only the changeset. No run was
truncated or discarded: both enumerations read 186 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 `namesThisRepository`
over every `.ts` file under `service-package/src` (6 files). It takes
its verdicts from the before census's own board reading rather than from
a second enumeration: a number is dead when that census reported it
`allocated-but-absent`, and alive when that census judged it on this
board anywhere (its `--list` extraction, 37,065 rows) and did not report
it. The one number the census never saw, because it stands only in test
files here, was read on its own: `objectstack-ai#16650` answers 404 on the issues
endpoint and on the pulls endpoint.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `4dfff176b` | 82 | **22** | 13 | 5 | 0 | 4 |
| after, `34ba921e6` | 64 | **4** | 0 | 0 | 0 | 4 |

Its src-comment column equals the census's 13, which is the control on
the second instrument. The 60 live citations are the same in both
readings (no cross-repo citation stands in this package), and the drop
of 18 citations is exactly the rewritten sites. A third, raw reading
(every `#` followed by 2 to 6 digits, whatever surrounds it) finds 82
occurrences and 22 dead before, 64 and 4 after: the same as the gate's
grammar, so nothing here sits beyond it, and it has no unjudged token.

## Per-number table

Sites and files count every dead occurrence 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, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (`merge-base --is-ancestor` exit 0 for all 17 line and anchor
pairs).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#10965` | 17/3 | 13/4 | `ab47f6974` (PR objectstack-ai#11064): `get()` and `list()`
refuse a storage seam that accepted the query and returned no result
set, with a declared ADR-0112 envelope (`SERVICE_UNAVAILABLE` / 503),
and the skipped boot rehydration is logged at warn; a seam that answers
with zero rows is unchanged. Its body says `Part of objectstack-ai#10965` three times,
and it is the only commit that wrote the seam guard (`git log -S
packageSeamUnreadableError`). The `runtime` stage's anchor for the same
number |
| `objectstack-ai#10788` | 1/1 | 1/0 | `3a7ec2d3b`: `os migrate duplicates` holds a
raw-SQL seam that cannot answer to be absent, not empty. The squash
commit of the pull request that was `objectstack-ai#10788` (404 on the pulls endpoint
too); `objectstack-ai#10677`, the card it answers, stays beside it. New to the sweep |
| `objectstack-ai#10789` | 1/1 | 1/0 | `38bc74ed1`: `backfillSeedTenancy`'s read
probes hold a seam that cannot answer to be absent, not empty. Its
subject names `objectstack-ai#10789`. The `runtime` stage's anchor for the same number
|
| `objectstack-ai#10964` | 1/1 | 1/0 | `38bc74ed1`: the same commit, the squash commit
of the pull request that was `objectstack-ai#10964` (404 on the pulls endpoint too),
so the pair `objectstack-ai#10789 / PR objectstack-ai#10964` became one sha |
| `objectstack-ai#16650` | 2/2 | 2/0 | `001a83b04`: `SqlDriver.execute()` declares a
backend refusal as `DATABASE_ERROR` / 500. The squash commit of the pull
request that was `objectstack-ai#16650`; its review round (「pin the package-door code
flip」) wrote the two `[objectstack-ai#16019]` blocks whose closing sentence these
lines are. The `rest` stage's anchor for the same sentence in
`package-door-16019-raw-statement-fault-code.test.ts` |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 4), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 4;
control leg: stage 1's landing `422db788a` exit 0; the history is
complete, `--is-shallow-repository` false, 15,149 commits). Each of the
5 numbers answers 404 on the issues endpoint and on the pulls endpoint.

## Wordings to check

- **Bracket tags.** `[objectstack-ai#10965]` became `[commit ab47f69]` on 9 lines of
`index.ts` (`:208`, `:286`, `:304`, `:327`, `:451`, `:476`, `:502`,
`:517`, `:626`).
- **`index.ts:223`**, a section heading: 「(objectstack-ai#10965)」 became 「(commit
ab47f69)」, and its trailing rule was shortened from 11 characters to 2
so the line stays near its old width.
- **`index.ts:234-235`**, the two siblings of the seam guard: 「(objectstack-ai#10677 /
PR objectstack-ai#10788 for / `os migrate duplicates`, objectstack-ai#10789 / PR objectstack-ai#10964 for
`backfillSeedTenancy`)」 became 「(objectstack-ai#10677 / commit 3a7ec2d for / `os
migrate duplicates`, commit 38bc74e for `backfillSeedTenancy`)」. The
live `objectstack-ai#10677` stays beside its fix; the dead issue and its dead pull
request became their one squash commit.
- **`mysql2-tuple.test.ts:26` and `:196`.** 「objectstack-ai#10965's guard」 and
「(objectstack-ai#10965's leg」 became 「commit ab47f69's guard」 and 「(commit
ab47f69's leg」.
- **`null-seam.test.ts:4`**, the file's title line: 「objectstack-ai#10965 — `get()` /
`list()` answered over a driver they never queried.」 became 「The card
behind commit ab47f69 — …」, so line 8's 「The card established the
conflation by READING」 keeps its referent.
- **`delete-driver-fault.test.ts:319` and
`publish-driver-fault.test.ts:357`.** 「The reviewer of PR objectstack-ai#16650
required the flip」 became 「The reviewer of commit 001a83b required the
flip」, the `rest` stage's form for the same sentence.

## The 4 sites left

- **Test strings, 4 sites**, all `objectstack-ai#10965`, all `describe` titles in
`null-seam.test.ts` (`:140`, `:184`, `:229`, `:284`), left as stages 1
to 9 left theirs.
- No source string, operator log string, assertion message, quoted
maintainer ruling or generated file in this package carries a dead
number.

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes (a `forEachChild`
walk, so comments are trivia and JSDoc nodes are never visited), base
`4dfff176b` against head. String and template literals are therefore
read in full. It ran over all 5 touched `.ts` files.

- Real run: 3,323 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `index.ts` (「Is this the seam refusal above?」 to 「…
named above?」): 0 files changed, as expected (exit 0).
- Positive control, a code token added in `index.ts`
(`isResultSet(result)` given `as any` in `get()`): DIFFER (exit 1).
- Positive control, one digit changed inside a kept test title
(`null-seam.test.ts:140`, `objectstack-ai#10965` to `objectstack-ai#10966`): 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 (`2555410dd0a7`, `0c5bf5e7e190`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-package`
(`.changeset/20596-service-package-provenance-anchors.md`) is included.
Its body is stage 9's, word for word, with the package name changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build, `ab47f6974` appears once in each of
`dist/index.d.ts` and `dist/index.d.cts`: the rewritten docblock sits on
the exported `PACKAGE_SEAM_UNREADABLE_MESSAGE`. The other rewritten
comments do not reach `dist` (0 for `3a7ec2d3b`, `38bc74ed1` and
`001a83b04`, and 0 for `ab47f6974` in `index.js` and `index.cjs`).
Positive control: the unchanged line 「Like {@link
PACKAGE_PUBLISH_DRIVER_FAULT_MESSAGE}, a CONSTANT that」, in the same
docblock, is found once in each declaration file. A never-written
negative phrase appears nowhere in `dist`. None of the 5 dead numbers is
left in `dist`.

## Gates (head `ffd2f1ed2`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test, 114 cases, 8 batteries) exits 0. `node
scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged 1
citation in 1 file and found it on the board: `objectstack-ai#10677`, which already
stood on its line.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0; the
sibling-package prose-id baseline holds (808 pinned sites, no growth).
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `ffd2f1ed2` derived 62 commands:
all 56 derived at dispatch, plus `check:engine-double-contract`,
`check:objectql-double-limit`, `check:query-options-erasure`,
`check:type-check-coverage`, `check:type-check-debt` and
`check:where-matcher`. Each ran with its exit code captured before any
pipe, and all 62 exit 0. `--ran`, fed each command with its exit code,
reports 62 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.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/service-package test`: 5 files pass and 79
tests pass. `vitest list --filesOnly` names 5 files, all the tracked
test files, the 4 touched ones included.
- `pnpm --filter @objectstack/service-package typecheck` exits 0. `tsc
--listFiles` holds all 6 files under `src/`, all 5 touched files
included.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 5 touched `.ts` files gives 5 files, 0 errors and 0
warnings. All 5 are in eslint's own population (`isPathIgnored` is false
for each; a `dist` file, as the control, is ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), 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 6 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word
「option」. In this package: `#N-word` none, `#A/#B` none, `option #N`
none, at the base and at the head. The raw scan agrees: nothing sits
beyond the gate's grammar here.
- **「This card」 phrases are left.** 14 comment lines in 5 files of this
package speak of 「this card」, 「the card」 or 「The card」. They carry no
number and neither instrument sees them. One title line was worded so
that its neighbour keeps a referent (`null-seam.test.ts:4`, above); the
rest are unchanged, as in stages 8 and 9.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#10788` →
`3a7ec2d3b`; `objectstack-ai#10964` → `38bc74ed1`. The other three reuse sibling
stages' anchors: `objectstack-ai#10965` → `ab47f6974` and `objectstack-ai#10789` → `38bc74ed1` (the
`runtime` stage), `objectstack-ai#16650` → `001a83b04` (the `rest` stage).
- **Base.** The branch is on `main` at `4dfff176b`. `main` has since
moved four commits (`03cdb9a5c`, `b785c3b11`, `5a23096ca`, `01e78dcee`).
Their 40 files touch nothing under `service-package`, nor
`scripts/check-issue-citations.mjs` or `.changeset/config.json`; the
`doc-authoring-prose-id` baseline they shrink has no `service-package`
row. So no merge was taken; the merge queue rebuilds on the merged
generation.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ts that decided them (objectstack-ai#20757)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the eleventh stage of the `domain:services` lane of the
dead-citation sweep. It covers `packages/plugins/plugin-email/src/**`
and nothing else. By the seat's census at the claim (`5902547086`), it
is the largest package in the lane that no in-flight work holds. 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 to 10 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`, PR objectstack-ai#20729 as `d2820876f`, PR
objectstack-ai#20737 as `4dfff176b`, PR objectstack-ai#20742 as `697845d19`). That is **16 sites on
16 lines in 8 files, covering 4 numbers**:

- 7 census sites (every census site this package has);
- 9 sites in test comments, which the census defers. Three of them carry
`objectstack-ai#13190`, a dead number that stands only in test files here, so the
census never judged it; it was read on its own (404);
- no site the gate's grammar cannot see (the package has none that is
dead, see Acceptance notes).

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **4 distinct shas**. No number in this package has an ADR or
ruling record of its own (a grep of `docs/adr/` and
`scripts/adr-anchors/` finds only ADR-0131 naming `objectstack-ai#11741`, as evidence
in its D7, not as the record of that decision; nothing else under
`docs/` names the four), so every anchor is a commit, per ruling C's
order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(16 lines out, 16 in, over 8 files), so no line citation into these
files moves. Every one of the 16 changed lines carried a dead citation;
there is no reflow line. No code token moves (see the guard below).

**No citation number is added.** The added lines carry no tracker number
at all. Over the whole diff, added minus removed is negative for the
four dead numbers and zero for every other number, and no number is new
to the diff. No PR number is the citation on an added line: the two `PR
objectstack-ai#8675` spellings became that pull request's squash commit.

10 dead sites are left on purpose, all of them `describe` / `it` titles
(see the list below).

One more file: a `patch` changeset for `@objectstack/plugin-email`,
because the rewritten prose ships (see Changeset below).

## Census: `plugin-email`, 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-email/`. Each run counts as a reading only
because its board frontier equals the newest issue or pull-request
number, read by a separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
plugin-email sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `97005aed0`, run 2026-09-30T02:00:45Z to 02:04:02Z |
enumerated, 186 pages, frontier objectstack-ai#20748 (newest objectstack-ai#20747 before, objectstack-ai#20748
after: a pull request opened at 02:03:20Z, inside the run) | 1,064 |
**7** | 7 | 4 | 3 |
| after | head `15a7d69a7`, run 02:11:19Z to 02:14:30Z | enumerated, 186
pages, frontier objectstack-ai#20753 (newest objectstack-ai#20753 before and after) | 1,057 | **0**
| 0 | 0 | 0 |

The before count matches the seat's census and A1 (7 sites: `objectstack-ai#13189` ×4,
`objectstack-ai#11741` ×2, `objectstack-ai#8675` ×1). The before run's board moved during the run;
its frontier equals the newest number at the run's end, which is A1's
criterion (stage 7's precedent). The whole-repo drop is 7, exactly this
diff's census sites. The `resolves` tally is 33,029 in both runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did
not move either. The after run was taken on `15a7d69a7`; the head
`23283d394` adds only the changeset. No run was truncated or discarded:
both enumerations read 186 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 `namesThisRepository`
over every `.ts` file under `plugin-email/src` (50 files). It takes its
verdicts from the before census's own board reading rather than from a
second enumeration: a number is dead when that census reported it
`allocated-but-absent`, and alive when that census judged it on this
board anywhere (its `--list` extraction, 37,072 rows) and did not report
it. The eleven numbers the census never saw, because they stand only in
test files or as the second half of a slash pair here, were read one by
one on the issues endpoint: `objectstack-ai#13190` answers 404; `objectstack-ai#5169`, `objectstack-ai#5286`,
`objectstack-ai#10619`, `objectstack-ai#16506`, `objectstack-ai#20374`, `objectstack-ai#5197` answer 200 as issues, and `objectstack-ai#8348`,
`objectstack-ai#5191`, `objectstack-ai#5211`, `objectstack-ai#5232` as pull requests.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `97005aed0` | 360 | **26** | 7 | 9 | 0 | 10 |
| after, `15a7d69a7` | 344 | **10** | 0 | 0 | 0 | 10 |

Its src-comment column equals the census's 7, which is the control on
the second instrument. The 323 live citations are the same in both
readings, and the drop of 16 citations is exactly the rewritten sites.
11 extracted tokens are not tracker references at all and are not
judged: the HTML entity `&objectstack-ai#39;` (6 sites in the template engine and its
tests) and the fixture subjects `Invoice objectstack-ai#42` to `Invoice objectstack-ai#45` (5
sites). A third, raw reading (every `#` followed by 2 to 6 digits,
whatever surrounds it) finds 371 occurrences and 26 dead before, 355 and
10 after. Beyond the gate's grammar it sees 11 tokens, none dead: the
nine second numbers of the `#A/#B` lines (all live), the excused `Prime
Directive objectstack-ai#12`, and the CSS colour `#2563eb`.

## Per-number table

Sites and files count every dead occurrence 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, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (`merge-base --is-ancestor` exit 0 for all 16 line and anchor
pairs).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#13189` | 13/4 | 8/5 | `33fbd3566` (PR objectstack-ai#13375): the SMTP port guard
tests integrality (`Number.isInteger`), so a fractional port such as
`587.5` is refused at construction, and the generated refusal sentence
reads `(expected an integer 1-65535)`, the range still rendered from the
constants. Its changeset headline names `objectstack-ai#13189`; its diff writes the
integrality docblocks the rewritten lines sit in. New to the sweep |
| `objectstack-ai#13190` | 5/1 | 3/2 | `56c5b1dbe` (PR objectstack-ai#13316):
`smtpOptionsFromMailSettings` passes a present-but-unreadable
`smtp_port` through to the guard instead of omitting it (which had
silently fallen back to 587); absent and `''` still mean "not set", and
no second refusal was added. Its changeset headline names `objectstack-ai#13190`; its
diff writes the `objectstack-ai#13190` comment block itself. New to the sweep |
| `objectstack-ai#11741` | 6/3 | 3/3 | `b706af987` (PR objectstack-ai#11839): `SendEmailInput` /
`SendTemplateInput` gain an optional `organizationId`, which
`plugin-email`'s writer stamps verbatim onto `sys_email.organization_id`
(pass-through only, no resolution or fabrication), and `sendTemplate`
forwards it as a producer of `send()`. Its message names `objectstack-ai#11741` as the
card that commit closed; `git blame` puts all three rewritten lines in
it. The `plugin-auth` stage's anchor for the same number |
| `objectstack-ai#8675` | 2/2 | 2/0 | `c9f595083`: the squash commit of the pull
request that was `objectstack-ai#8675` (its subject ends `(objectstack-ai#7987) (objectstack-ai#8675)`):
`sys_account`'s OAuth token columns are declared `internal: true`. Its
diff records the trap both lines describe: those columns are `required:
false`, so inferring "key missing, therefore the strip ran" broke
ordinary sign-in (16 red tests), which is why the readback carries the
`absenceProvesStrip` discriminator. New to the sweep |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 4), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 4;
control leg: stage 1's landing `422db788a` exit 0; the history is
complete, `--is-shallow-repository` false, 15,155 commits). Each of the
4 numbers answers 404 on the issues endpoint, which serves pull requests
too. Independently, the package's own shipped `CHANGELOG.md` pairs
`b706af9`, `33fbd35` and `56c5b1d` with the same three decisions.

## Wordings to check

- **Tag swaps in parentheses.** 「(objectstack-ai#13189)」 became 「(commit 33fbd35)」
at `transports/smtp-port-contract.ts:87` (a section heading), `:134` and
`transports/smtp.ts:68`.
- **Line openers.** 「objectstack-ai#11741 —」 became 「Commit b706af9 —」 at
`email-service.ts:742` and `:1439`; 「objectstack-ai#13190 —」 became 「Commit 56c5b1d
—」 at `transports/smtp.test.ts:221`; 「## objectstack-ai#13189 —」 became 「## Commit
33fbd35 —」 at `transports/smtp-port-contract.test.ts:34`.
- **`email-service.test.ts:342`**, a section rule: 「── objectstack-ai#11741 —」 became
「── Commit b706af9 —」, and its trailing rule was shortened by 10
characters so the line keeps its width exactly.
- **`internal-header-readback.ts:37`.** 「(PR objectstack-ai#8675 hit exactly this on
`sys_account`'s optional」 became 「(Commit c9f5950 records exactly this
on `sys_account`'s optional」: a commit does not "hit" a trap, it records
one, and that commit's own diff is where the 16 red tests are recorded.
- **`email-headers-internal.integration.test.ts:251`.** 「The regression
PR objectstack-ai#8675 measured on a sibling card」 became 「The regression commit
c9f5950 records from a sibling card」, the same reading.
- **`transports/smtp-port-contract.test.ts:228`.** 「objectstack-ai#13189 is the card
that SPENDS that」 became 「Commit 33fbd35 is the change that SPENDS
that」, so the noun matches the anchor.
- **`transports/smtp.ts:127`, `transports/smtp.test.ts:272`, `:276`,
`:281`, `:283`.** The number became 「commit SHA」 in place (「until commit
33fbd35:」, 「The bucket commit 56c5b1d never had to name」, 「Commit
33fbd35 made the guard test」, 「Commit 56c5b1d's rule is that」,
「commit 33fbd35 changed which numbers」).

## The 10 sites left

- **Test strings, 10 sites on 9 lines**, all `describe` / `it` titles,
left as stages 1 to 10 left theirs: `email-service.test.ts:349` and
`send-template.test.ts:63`, `:88` (`objectstack-ai#11741`);
`transports/smtp-port-contract.test.ts:225`, `:309`, `:340` (`objectstack-ai#13189`);
`transports/smtp.test.ts:230` (`objectstack-ai#13190`), `:271` (`objectstack-ai#13189`), `:293`
(`objectstack-ai#13190` and `objectstack-ai#13189`).
- No source string, operator log string, assertion message, quoted
maintainer ruling or generated file in this package carries a dead
number.
- Outside `src`, the package's `CHANGELOG.md` names three of these
numbers on 5 lines. It is release-owned and deliberately not edited here
(see Acceptance notes).

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes (a `forEachChild`
walk, so comments are trivia and JSDoc nodes are never visited), base
`97005aed0` against head. String and template literals are therefore
read in full. It ran over all 8 touched `.ts` files.

- Real run: 7,035 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `email-service.ts` (「no resolution, no default, no
fabrication」 to 「… no default and no fabrication」): 0 files changed, as
expected (exit 0).
- Positive control, a code token added in `transports/smtp.ts`
(`isValidSmtpPort(port)` given `as number`): DIFFER, 587 to 588 leaf
tokens (exit 1).
- Positive control, one digit changed inside a kept test title
(`transports/smtp.test.ts:293`, `objectstack-ai#13189` to `objectstack-ai#13188`): DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs` (wrap mode)
under a shell trap that restores by absolute path, and each landed
(anchor 1 to 0, blob changed). Each restore was proven byte-identical to
the HEAD blob (`1e99bd5e2bcb`, `46c13267611b`, `da5314910bc4`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/plugin-email`
(`.changeset/20596-plugin-email-provenance-anchors.md`) is included. Its
body is stage 10's, word for word, with the package name changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`, and the package is not private. After the build,
`b706af987` appears twice in each of `dist/index.js` and
`dist/index.mjs` (the two inline comments in `email-service.ts`, which
the bundle keeps). `c9f595083` appears once in each of `dist/index.d.ts`
and `dist/index.d.mts` (the `internal-header-readback.ts` docblock), and
so does `33fbd3566` (the docblock on `SmtpTransportOptions.port`).
`56c5b1dbe` reaches nothing (test files only). Positive controls, one
unchanged line beside each shipped rewrite, land exactly where their
neighbours do: 「context, so the input's organization is the one fact it
may stamp:」 and 「caller's organization so the sys_email row it persists
is stamped.」 once in each JS file; 「token columns: inheriting」 and the
unchanged line just above the rewritten one in the `port` docblock once
in each declaration file. A never-written negative phrase appears
nowhere in `dist`. None of the 4 dead numbers is left in `dist`.

## Gates (head `23283d394`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
exits 0. `node scripts/check-issue-citations.mjs` exits 0: the
diff-scoped run found no citation added against `97005aed0` (4 files
read; test files are a deferred surface).
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `23283d394` derived 61 commands:
all 55 derived at dispatch, plus `check:engine-double-contract`,
`check:objectql-double-limit`, `check:query-options-erasure`,
`check:type-check-coverage`, `check:type-check-debt` and
`check:where-matcher`. Each ran with its exit code captured before any
pipe, and all 61 exit 0. `--ran`, fed each command with its exit code,
reports 61 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.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/plugin-email test`: 31 files pass and 510
tests pass. `vitest list --filesOnly` names 31 files, all the tracked
test files, the 4 touched ones included.
- `pnpm --filter @objectstack/plugin-email typecheck` exits 0 (`tsc` on
`tsconfig.json`, then `check:test-typecheck` on `tsconfig.test.json`: 0
files and 0 errors in its debt ledger). `tsc --listFiles` holds all 8
touched files in both programs, and the test program holds all 50 files
under `src/`.
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 8 touched `.ts` files, gives 8 files, 0 errors and 0 warnings.
All 8 are in eslint's own population (`isPathIgnored` is false for each;
a `dist` file, as the control, is ignored). `eslint.config.mjs` never
enables type-aware linting (no `parserOptions.project`, as its own lines
327-328 state), 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 9 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word
「option」. In this package: `#N-word` none, `#A/#B` 9 lines, `option #N`
none, at the base and at the head, which is the claim's 0 / 9 / 0. Every
second number on the 9 slash lines answers 200 (`objectstack-ai#5197` ×2, `objectstack-ai#5191`,
`objectstack-ai#5211`, `objectstack-ai#5232` ×2, `objectstack-ai#5177`, `objectstack-ai#4251`, `objectstack-ai#5094`), so nothing there needed
rewriting.
- **ADR-0131 names `objectstack-ai#11741`.** Its D7 cites `objectstack-ai#11741` as the writer fact
that keeps `sys_email` tenant data. That is evidence inside a later
record, not the record of what `objectstack-ai#11741` decided, so it is not this
stage's anchor, and `docs/adr/**` is a governed Tier H surface outside
this card's stages. It joins the ADR-tree residue the seat already
carries (ADR-0131's `objectstack-ai#14484`, stage 2).
- **`CHANGELOG.md` is left.**
`packages/plugins/plugin-email/CHANGELOG.md` names `objectstack-ai#11741`, `objectstack-ai#13189`,
`objectstack-ai#13190` and `objectstack-ai#8675` on 5 lines. It is release-owned (AGENTS.md,
Documentation Guardrails), a deferred surface of the citation gate, and
⛔ not part of this stage.
- **「This card」 phrases are left.** 20 comment lines in 8 files of this
package speak of 「this card」, 「the card」 or 「the two cards」. They carry
no number and neither instrument sees them. Inside the `objectstack-ai#13189` test
block, they still have the kept `(objectstack-ai#13189)` title as their referent; the
one rewritten line that said 「the card」 now says 「the change」 (above).
The rest are unchanged, as in stages 8 to 10.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#13189` →
`33fbd3566`; `objectstack-ai#13190` → `56c5b1dbe`; `objectstack-ai#8675` → `c9f595083`. `objectstack-ai#11741` →
`b706af987` reuses the `plugin-auth` stage's anchor.
- **Base.** The branch is on `main` at `97005aed0`. `main` has since
moved two commits (`9c8f113c6`, `a6866da0c`). Their 14 files touch
nothing under `plugin-email`, nor `scripts/check-issue-citations.mjs`,
`.changeset/config.json` or the `doc-authoring-prose-id` baseline, and
the three console-injection scripts they change are not among this
diff's 61 derived families. So no merge was taken; the merge queue
rebuilds on the merged generation.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ommits that decided them (objectstack-ai#20775)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the twelfth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/triggers/trigger-schedule/src/**` and nothing else. By the
seat's claim (`5903462246`), it is the largest package in the lane that
no in-flight work holds, now that objectstack-ai#20599's PR objectstack-ai#20746 (which edited
`time-relative-trigger.ts`) has landed. 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 to 11 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`, PR objectstack-ai#20729 as `d2820876f`, PR
objectstack-ai#20737 as `4dfff176b`, PR objectstack-ai#20742 as `697845d19`, PR objectstack-ai#20757 as
`cba417a8f`). That is **32 sites on 32 lines in 6 files, covering 2
numbers**:

- 18 census sites (every census site this package has);
- 14 sites in test comments, which the census defers;
- no site the gate's grammar cannot see (the package has none, see
Acceptance notes).

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **2 distinct shas**. Neither number has an ADR or ruling record
of its own (a grep of `docs/adr/`, `scripts/adr-anchors/` and the rest
of `docs/` for both numbers finds nothing, and no ADR records the
acting-organization decision or the driver-memory per-call refusal), so
both anchors are commits, per ruling C's order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(32 lines out, 32 in, over 6 files), so no line citation into these
files moves. Every one of the 32 changed lines carried a dead citation;
there is no reflow line. No code token moves (see the guard below).

**No citation number is added.** The only tracker number on an added
line is the live `objectstack-ai#8844`, on the line it already stood on. Added minus
removed is negative for the two dead numbers and zero for every other
number, and no number is new to the diff. No PR number is the citation
on an added line.

4 dead sites are left on purpose: three `describe` titles, and one
comment that quotes one of those titles verbatim (see the list below).

One more file: a `patch` changeset for `@objectstack/trigger-schedule`,
because the rewritten prose ships (see Changeset below).

## Census: `trigger-schedule`, 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/triggers/trigger-schedule/`. Each run counts as a reading only
because its board frontier equals the newest issue or pull-request
number, read by a separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
trigger-schedule sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `cba417a8f`, run 2026-09-30T03:30:14Z to 03:33:24Z |
enumerated, 186 pages, frontier objectstack-ai#20769 (newest objectstack-ai#20769 before and after)
| 823 | **18** | 18 | 2 | 2 |
| after | head `226be8050`, run 03:37:59Z to 03:41:09Z | enumerated, 186
pages, frontier objectstack-ai#20769 (newest objectstack-ai#20769 before and after) | 805 | **0** |
0 | 0 | 0 |

The before count matches the seat's census and A1 (18 sites: `objectstack-ai#16659`
×17 and `objectstack-ai#16589` ×1, in `schedule-trigger.ts` ×5 and
`time-relative-trigger.ts` ×13). A1 noted that PR objectstack-ai#20746 edited
`time-relative-trigger.ts` today; the before count above is taken on the
base that already holds that edit. The whole-repo drop is 18, exactly
this diff's census sites. The `resolves` tally is 33,038 in both runs,
and `resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995)
did not move either. The after run was taken on `226be8050`; the head
`14314f49c` adds only the changeset. No run was truncated or discarded:
both enumerations read 186 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 `namesThisRepository`
over every `.ts` file under `trigger-schedule/src` (14 files). It takes
its verdicts from the before census's own board reading rather than from
a second enumeration: a number is dead when that census reported it
`allocated-but-absent`, and alive when that census judged it on this
board anywhere (its `--list` extraction, 36,840 rows) and did not report
it. Every number this package cites is covered by one or the other, so
no number needed a separate read to be judged; the two dead numbers were
also read one by one on the issues endpoint, and each answers 404.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `cba417a8f` | 159 | **36** | 18 | 15 | 0 | 3 |
| after, `226be8050` | 127 | **4** | 0 | 1 | 0 | 3 |

Its src-comment column equals the census's 18, which is the control on
the second instrument. The 123 live citations are the same in both
readings, and the drop of 32 citations is exactly the rewritten sites. A
third, raw reading (every `#` followed by 2 to 6 digits, whatever
surrounds it) finds 162 occurrences and 36 dead before, 130 and 4 after.
Beyond the gate's grammar it sees 3 tokens, none a tracker reference:
the maintainer decision-batch ordinals `batch objectstack-ai#13`, `objectstack-ai#116` and `objectstack-ai#118`,
which the gate's `NON_CITATION_HEADS` excuses by design.

## Per-number table

Sites and files count every dead occurrence 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, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (21 lines blame to the anchor itself; for the other 11,
`merge-base --is-ancestor` of anchor and blamed commit exits 0).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#16659` | 34/6 | 30/4 | `ecdfc9411` (PR objectstack-ai#17334): a time-triggered
flow (`schedule` or `time_relative`) declares its acting organization on
its start node as `config.organization`; the engine lifts it onto the
binding; both time triggers refuse to bind a flow that declares none,
naming it at `error` and THROWING so the engine records the refusal
instead of reporting the flow bound; the run carries the declared
organization as `tenantId`; and the time-relative sweep's own query
carries it too, so the sweep SELECTS inside that organization (the
review finding F2 its diff names), with a store that cannot honour the
scope reported at `error` and an object the engine exempts from scoping
disclosed at bind. Its body names `objectstack-ai#16659` twice (the three consequences
pinned on both drivers, and the proof registered), and its diff names it
on 65 added lines. The anchor the spec stage (`0f6dcac5e`) and the lint
stage (`f29c83db1`) already give the same number |
| `objectstack-ai#16589` | 2/2 | 2/0 | `555a89cbd` (PR objectstack-ai#17005): `driver-memory` gains
a third seam, `assertCallNotTenantScoped`, called first in every driver
door that accepts `DriverOptions`, which REFUSES a call the engine
tenant-scoped instead of discarding the scope and answering every
organization's rows; row-level isolation is deliberately not
implemented. Its message does not carry the number, but its own diff
writes the mechanism the two lines describe and names `objectstack-ai#16589` 30 times
(the `[objectstack-ai#16589] Seam 3` markers and the guard's docblock), so it is the
commit that decided it. New to the sweep |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 2), and both are ancestors of
the base (`merge-base --is-ancestor`, exit 0 for both; control leg:
stage 1's landing `422db788a` exit 0; reverse leg, base against
`ecdfc9411`, exit 1; the history is complete, `--is-shallow-repository`
false, 15,160 commits; each anchor lies deeper than the control, 1,616
and 1,814 commits behind the base). Each of the 2 numbers answers 404 on
the issues endpoint, which serves pull requests too. Independently, the
package's own shipped `CHANGELOG.md` pairs `ecdfc94` with `objectstack-ai#16659` (line
149) and `assertCallNotTenantScoped` with `objectstack-ai#16589` (line 238).

## Wordings to check

- **Tag swaps in brackets or parentheses.** 「[objectstack-ai#16659]」 became 「[commit
ecdfc94]」 on 18 lines, 「(objectstack-ai#16659)」 became 「(commit ecdfc94)」 at
`schedule-trigger.ts:251`, `:372` and `time-relative-trigger.ts:50`, and
「(objectstack-ai#16589)」 became 「(commit 555a89c)」 at `time-relative-trigger.ts:615`
and `time-relative-trigger.test.ts:988`.
- **Section rules.** `schedule-trigger.test.ts:327`,
`time-relative-trigger.test.ts:794` and `:862`: the 16-character phrase
replaces the 6-character number and the trailing rule loses 10
characters, so each line keeps its width exactly. The `:862` heading
keeps 「F2」 beside the sha; F2 is the selection finding `ecdfc9411`'s own
diff names.
- **「before objectstack-ai#16659」** at `time-relative-trigger.ts:365` and `:543`
became 「before commit ecdfc94」: before that commit the sweep queried
with `isSystem` alone, which is the unscoped selection both sentences
describe.
- **「the objectstack-ai#16659 defect」** at `time-relative-trigger.ts:561` and
`time-relative-trigger.test.ts:1371` became 「the defect commit ecdfc94
fixed」: a commit fixes a defect, it is not one, and the widening both
sentences name is the selection half that commit closed.
- **`schedule-trigger.test.ts:512`.** 「the exact defect objectstack-ai#16659's own
refusal was shaped to avoid」 became 「the exact defect commit ecdfc94's
own refusal was shaped to avoid」: the defect is a refusal that logs and
arms anyway, and that commit is where the refusal became a throw so the
engine records it.
- **`schedule-trigger.test.ts:588`.** 「(the objectstack-ai#16659 suite above)」 became
「(commit ecdfc94's refusal suite above)」, so the pointer still lands
on the refusal suite at `:337`.

## The 4 sites left

- **Test strings, 3 sites on 3 lines**, all `describe` titles, left as
stages 1 to 11 left theirs: `schedule-trigger.test.ts:337` and `:462`,
`time-relative-trigger.test.ts:805` (all `objectstack-ai#16659`).
- **One comment that quotes a kept title verbatim:**
`schedule-trigger.test.ts:71` points the reader at 「`ScheduleTrigger —
the acting-organization refusal (objectstack-ai#16659)` below」, the exact text of the
`describe` title at `:337`. The number there belongs to the quotation,
so it stays with the title it quotes: rewriting it would point at a
title that does not exist. It moves when the title does.
- No source string, operator log string, assertion message, quoted
maintainer ruling or generated file in this package carries a dead
number.
- Outside `src`, the package's `CHANGELOG.md` names `objectstack-ai#16659` on 6 lines
and `objectstack-ai#16589` on 2 (lines 149, 153, 238, 273, 297, 300, 302, 304). It is
release-owned and deliberately not edited here (see Acceptance notes).
The package `README.md`, which also ships, names neither number.

## Mechanical guard: no code token moves

The guard compares, base `cba417a8f` against head, over all 6 touched
`.ts` files:

- **Reading 1**, the TypeScript parser's leaf nodes (a `forEachChild`
walk, so comments are trivia and JSDoc nodes are never visited). String
and template literals are therefore read in full.
- **Reading 2**, the full token stream in parser context (a
`getChildren` walk, so punctuation and keywords are included; JSDoc
nodes skipped).

Results:

- Real run: 10,192 base leaf tokens, **0 files with a token change** on
either reading (exit 0).
- Comment control in `schedule-trigger.ts` (「the same way `schedule`
is.」 to 「the same way as `schedule`.」): 0 files changed, as expected
(exit 0).
- Positive control, a code token added in `time-relative-trigger.ts`
(`resolveBindingOrganization(binding)` given `as FlowTriggerBinding`):
DIFFER, 1,215 to 1,216 leaf tokens and 2,760 to 2,762 full tokens (exit
1).
- Positive control, one digit changed inside a kept test title
(`schedule-trigger.test.ts:462`, `objectstack-ai#16659` to `objectstack-ai#16658`): DIFFER on the
string literal (exit 1).

Every mutation went through `scripts/ablation-replace.mjs` (wrap mode)
under a shell trap that restores by absolute path, and each landed
(anchor 1 to 0, blob changed). Each restore was proven byte-identical to
the HEAD blob (`651170483856`, `c85aadbd168d`, `78a5dea4a463`), with
`git diff HEAD` empty and a clean tree afterwards.

A first version of reading 2 used TypeScript's context-free scanner and
was discarded before any control ran: it opened template tokens on
backticks it could not place and swallowed comment text into them, so it
reported comment edits as token changes (4 files) while reading 1 read
0. The parser-context stream replaced it, and every figure above is from
the replacement.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/trigger-schedule`
(`.changeset/20596-trigger-schedule-provenance-anchors.md`) is included.
Its body is stage 11's, word for word, with the package name changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`, and the package is not private. After the build:

- `ecdfc9411` appears 3 times in each of `dist/index.js` and
`dist/index.mjs`: the inline comments at `schedule-trigger.ts:777` and
`time-relative-trigger.ts:585` and `:702`, which the bundle keeps.
- It appears twice in each of `dist/index.d.ts` and `dist/index.d.mts`:
the `FlowTriggerBinding.organization` docblock
(`schedule-trigger.ts:32`) and the sweep-context docblock
(`time-relative-trigger.ts:50`).
- `555a89cbd` appears once in each JS entry
(`time-relative-trigger.ts:615`).
- Positive controls, one unchanged line beside each shipped rewrite,
land exactly where their neighbours do: four neighbours once in each JS
file and 0 in the declaration files, and two once in each declaration
file and 0 in the JS files.
- A never-written negative phrase appears nowhere in `dist`.
- Neither dead number is left in `dist`.

## Gates (head `14314f49c`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
exits 0. `node scripts/check-issue-citations.mjs` exits 0: the
diff-scoped run judged 1 added citation across 2 files, the live
`objectstack-ai#8844`, and it resolves.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0 (the
sibling-package prose-id baseline holds, no growth).
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `14314f49c` (after a fresh fetch)
derived 59 commands. They are all 53 derived at dispatch, plus
`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`.
- Each ran with its exit code captured before any pipe, and all 59 exit
0.
- `--ran`, fed each command with its exit code, reports 59 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.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/trigger-schedule test`: 8 files pass and
170 tests pass. `vitest list --filesOnly` names 8 files, all the tracked
test files, the 4 touched ones included.
- `pnpm --filter @objectstack/trigger-schedule typecheck` exits 0, and
`tsc --listFiles` holds all 14 files under `src/`, the 6 touched ones
included.
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 6 touched `.ts` files, gives 6 files, 0 errors and 0 warnings
(its `--format json` output). All 6 are in eslint's own population
(`isPathIgnored` is false for each; a `dist` file, as the control, is
ignored). `eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), 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 7 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word
「option」. In this package: `#N-word` none, `#A/#B` none, `option #N`
none, at the base and at the head, which is the claim's 0 / 0 / 0. The
two `pre-objectstack-ai#10220` spellings in `time-relative-trigger.test.ts` are
extracted by the gate as this repository's `objectstack-ai#10220`, which the census
judges live.
- **`CHANGELOG.md` is left.**
`packages/triggers/trigger-schedule/CHANGELOG.md` names `objectstack-ai#16659` and
`objectstack-ai#16589` on 8 lines. It is release-owned (AGENTS.md, Documentation
Guardrails), a deferred surface of the citation gate, and ⛔ not part of
this stage.
- **「The card」 phrases are left.** 8 comment lines in 5 files of this
package speak of 「this card」, 「that card」 or 「the card」. They carry no
number and neither instrument sees them. The one beside a rewritten
line, `schedule-trigger.test.ts:329` (「the card's consequence (3)」),
sits under the heading `:327` that now names `ecdfc9411`, whose own
message pins those three consequences, so it keeps a referent. The rest
are unchanged, as in stages 8 to 11.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#16589` →
`555a89cbd` is new to the sweep; `driver-memory`'s own `src` still names
`objectstack-ai#16589` on 29 lines in 4 files (26 of them comments; corrected by the
seat from the dev report, which measured it), all outside this lane's
stage surface. `objectstack-ai#16659` → `ecdfc9411` reuses the spec and lint stages'
anchor.
- **Base.** The branch is on `main` at `cba417a8f`. `main` has since
moved three commits (`0d9349fea`, `7053333e1`, `f284ab26d`). Their 9
files are one changeset, ADR-0053, and sources and tests under
`service-analytics` and `service-automation`. They touch nothing under
`trigger-schedule`, nor `scripts/check-issue-citations.mjs`,
`.changeset/config.json` or the `doc-authoring-prose-id` baseline.
`service-automation` is a dev dependency of this package, but this diff
moves no code token, so nothing here can interact with it. No merge was
taken; the merge queue rebuilds on the merged generation.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…the commits that decided them (objectstack-ai#20789)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the thirteenth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/triggers/trigger-record-change/src/**` and nothing else. By
the seat's claim (`5904332626`), it is the largest package in the lane
that no in-flight work holds, while `service-automation` stays held
behind objectstack-ai#20726. 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 to 12 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`, PR objectstack-ai#20729 as `d2820876f`, PR
objectstack-ai#20737 as `4dfff176b`, PR objectstack-ai#20742 as `697845d19`, PR objectstack-ai#20757 as
`cba417a8f`, PR objectstack-ai#20775 as `91e8fa194`). That is **29 sites on 29 lines
in 5 files, covering 3 numbers**:

- 6 census sites (every census site this package has, all `objectstack-ai#14744`);
- 23 sites in test comments, which the census defers: 17 more of
`objectstack-ai#14744`, 1 of `objectstack-ai#13657`, and 5 of `objectstack-ai#11081`. `objectstack-ai#11081` stands only in a
test file here, so the census never judged it; it was read on its own
and answers 404.

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **4 distinct shas**. None of the three numbers has an ADR or
ruling record of its own, so every anchor is a commit, per ruling C's
order (see the per-number table). No number was dropped.

Only comments changed. Every touched source file keeps its line count
(30 lines out, 30 in, over 5 files), so no line citation into these
files moves. 29 of the 30 changed lines carried a dead citation; the
thirtieth keeps a referent the rewrite would otherwise have removed (see
Wordings). No code token moves (see the guard below).

**No citation number is added.** The only tracker numbers on added lines
are the live `objectstack-ai#15356` (3 times) and `objectstack-ai#8738` (once), each on the line it
already stood on. Added minus removed is negative for the three dead
numbers and zero for every other number, and no number is new to the
diff. No PR number is the citation on an added line.

4 dead sites are left on purpose, all test titles (see the list below).

One more file: a `patch` changeset for
`@objectstack/trigger-record-change`, because the rewritten prose ships
(see Changeset below).

## Census: `trigger-record-change`, 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/triggers/trigger-record-change/`. Each run counts as a reading
only because its board frontier equals the newest issue or pull-request
number, read by a separate request just before and just after the run.
In all three runs a new number was opened while the run was enumerating;
each frontier equals the newest number at the run's end, which is the
criterion (stages 7 and 11 met the same shape).

| reading | tree | board | whole-repo `allocated-but-absent` |
trigger-record-change sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `91e8fa194`, run 2026-09-30T04:58:08Z to 05:01:28Z |
enumerated, 187 pages, frontier objectstack-ai#20779 (newest objectstack-ai#20778 before, objectstack-ai#20779
after) | 802 | **6** | 6 | 2 | 1 |
| after | `bb9d39a87` (the comments commit), run 05:07:54Z to 05:11:48Z
| enumerated, 187 pages, frontier objectstack-ai#20780 (newest objectstack-ai#20779 before, objectstack-ai#20780
after) | 796 | **0** | 0 | 0 | 0 |
| after, final head | head `bbfe7cb24`, run 05:39:10Z to 05:42:26Z |
enumerated, 187 pages, frontier objectstack-ai#20784 (newest objectstack-ai#20783 before, objectstack-ai#20784
after) | 796 | **0** | 0 | 0 | 0 |

The before count matches the seat's census and A1 (6 sites, all
`objectstack-ai#14744`: `decouple-flow-record.ts` ×1 and `record-change-trigger.ts`
×5). The whole-repo drop is 6, exactly this diff's census sites. The
`resolves` tally is 33,055 in all three 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 read 187 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 `namesThisRepository`
over every `.ts` file under `trigger-record-change/src` (14 files). It
takes its verdicts from the before census's own board reading rather
than from a second enumeration: a number is dead when that census
reported it `allocated-but-absent`, and alive when the gate's own
census-scope extraction (36,836 citations over 2,617 files) judged it
and the census did not report it. Five numbers are covered by neither,
because they stand only in test files: each was read on its own.
`objectstack-ai#11081` answers 404; `objectstack-ai#5715` and `objectstack-ai#17982` answer 200 as pull requests;
`objectstack-ai#5785` and `objectstack-ai#17985` answer 200 as issues. The three dead numbers were
also read one by one, and each answers 404.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `91e8fa194` | 186 | **32** | 6 | 23 | 0 | 3 |
| after, `bbfe7cb24` | 157 | **3** | 0 | 0 | 0 | 3 |

Its src-comment column equals the census's 6, which is the control on
the second instrument. The 154 live citations are the same in both
readings, and the drop of 29 citations is exactly the rewritten sites. A
third, raw reading (every `#` followed by 2 to 6 digits, whatever
surrounds it) finds 195 occurrences before and 166 after. Beyond the
gate's grammar it sees 9 tokens, the same at base and head: the second
number of five `#A/#B` pairs (only one is dead, the kept title at
`before-update-flow-payload-reach.test.ts:872`), two `/objectstack-ai#3457/` regex
literals in assertions (live), and two `PD objectstack-ai#12` ordinals.

## Per-number table

Sites and files count every dead occurrence 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#14744` | 27/4 | 22/4 | `4f85e4d11` (PR objectstack-ai#15475): the flow-facing
`record` (and its `params` alias) and `previous` are decoupled from the
engine's own objects before a flow runs (`decoupleFromEngineState`:
arrays, plain objects, `Date`, `RegExp`, `Map` and `Set` are copied,
primitives, functions and other class instances shared), so a flow
mutating a nested value in place no longer writes the batch payload that
ADR-0058 Addendum II D3 shares across every row of a `multi: true`
update. A COPY rather than a FREEZE, because `expandDeclaredLookups`
writes into the record it is handed. The engine's write shape is
unchanged, and the same-key per-row-value residue is deliberately left
unguarded. Its changeset records the maintainer's option-A ruling on
`objectstack-ai#14744` in its own words, its diff names `objectstack-ai#14744` on 29 added lines,
and it created `decouple-flow-record.ts` and both of this package's pin
files. `git blame` at the base puts every one of the 22 lines in this
commit. New to the sweep |
| `objectstack-ai#14744` (the census line) | (in the row above) | 1/0 | `03c1b0f6f`
(PR objectstack-ai#15301): the census of same-key / per-row-VALUE `beforeUpdate`
rewrites, which found ZERO across 23 production registration sites and
recorded the `buildContext` overlay conclusion as a source reading, not
a measurement. Its message names `objectstack-ai#14744` four times and states that
result word for word. `before-update-flow-payload-reach.test.ts:29`
describes this census, not the fix, so it cites the census commit, by
the per-arm precedent of stages 5 and 9. The line was written by
`4f85e4d11`, which descends from `03c1b0f6f` (`merge-base --is-ancestor`
exit 0). New to the sweep |
| `objectstack-ai#13657` | 1/1 | 1/0 | `b003cf2e8` (PR objectstack-ai#13864): the post-hook half of
the declared-field door, which refuses an undeclared field a before-hook
writes, with one envelope on every driver. Its message names `objectstack-ai#13657`
seven times. The runtime and lint stages' anchor for the same number.
The line was written by `4f85e4d11`, which descends from it (exit 0) |
| `objectstack-ai#11081` | 5/1 | 5/0 | `c28e4cfae` (PR objectstack-ai#11570): the two
SqlDriver-backed fixtures stop blanket-silencing their kernel and carry
`@objectstack/runtime`'s shared expected-noise capture, which withholds
only a declared table's own `no such table` line, forwards every other
driver fault, and lets `afterAll` assert each channel fired. Its message
names `objectstack-ai#11081`, and its diff writes the five `[objectstack-ai#11081]` tags in this
very file; `git blame` at the base puts all five lines in it. Stage 7's
anchor for the same number |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 4), and all 4 are ancestors of
the base (`merge-base --is-ancestor`, exit 0 for each; reverse leg, base
against each anchor, exit 1 for each; control legs exit 0: stage 1's
landing `422db788a`, and the repository's root commit, which lies deeper
than every anchor; the history is complete, `--is-shallow-repository`
false, 15,167 commits; the anchors lie 2,516, 2,585, 3,082 and 4,207
commits behind the base). Each of the 3 numbers answers 404 on the
issues endpoint, which serves pull requests too.

No ADR, `scripts/adr-anchors/` file or other `docs/` page records any of
the three as its decision.
`docs/audits/2026-09-multi-update-per-row-value-census.md` names
`objectstack-ai#14744`, but it states that it is "measurement only — ships nothing …
implements no guard", the input to a decision rather than its record, so
the census line cites the commit that landed it.

## Wordings to check

- **Tag swaps in brackets or parentheses.** 「[objectstack-ai#14744]」 became 「[commit
4f85e4d]」 at `decouple-flow-record.test.ts:4` and
`before-update-flow-payload-reach.test.ts:805`. 「[objectstack-ai#11081]」 became
「[commit c28e4cf]」 on 5 lines. 「(objectstack-ai#14744, measured by objectstack-ai#15356)」 became
「(commit 4f85e4d, measured by objectstack-ai#15356)」 at `decouple-flow-record.ts:5`.
「(objectstack-ai#14744)」 became 「(commit 4f85e4d)」 at
`record-change-trigger.ts:340`. 「(objectstack-ai#8738 pre-hook / objectstack-ai#13657 post-hook)」
became 「(objectstack-ai#8738 pre-hook / commit b003cf2 post-hook)」.
- **Headings `:4` and `:859`.** 「[objectstack-ai#15356 measured, objectstack-ai#14744 closed]」 and
「[objectstack-ai#15356 measured it, objectstack-ai#14744 closed it]」 keep the live `objectstack-ai#15356` and put
the sha where the dead number stood.
- **`before-update-flow-payload-reach.test.ts:10`.** 「objectstack-ai#14744 then ruled
the door closed」 became 「The option-A ruling (commit 4f85e4d) then
closed the door」: the ruling is named in words beside the commit that
carried it, whose changeset records it, the form stages 2, 6 and 7 used
for a ruling.
- **`:22` and `:87`.** 「the objectstack-ai#14744 residue shape」 and 「the objectstack-ai#14744 pinned
residue shape」 became 「the residue shape commit 4f85e4d pins」: the
positive control that pins it is in that commit's diff.
- **`:23`.** 「because objectstack-ai#14744's fix is about aliasing」 became 「because
commit 4f85e4d fixes aliasing」: a commit fixes something, it does not
have a fix.
- **`:29` and `:34`, the census paragraph.** 「objectstack-ai#14744's census found」
became 「The census in commit 03c1b0f found」. That removed the referent
of 「The conclusion recorded on that card」 five lines down, so `:34`
became 「The conclusion recorded in that census」. This is the one changed
line that carried no dead number. It is true as written: the census
record `03c1b0f6f` landed carries that very conclusion, "On a source
reading, `buildContext` materialises a *new* record object by overlay …
a reading, not a measurement"
(`docs/audits/2026-09-multi-update-per-row-value-census.md:308-311`).
- **`:455`.** 「that is precisely the blind spot objectstack-ai#14744 is weighing」
became 「… the blind spot commit 4f85e4d left unguarded」. The present
tense described a card still being weighed; that commit's changeset says
the key-set refusal "is untouched and is not widened — a hook that
assigns the same key with per-row values still passes it".
- **「Before objectstack-ai#14744」 / 「before objectstack-ai#14744」** at `:686`, `:705`, `:738`,
`:924` (the word 「Before」 sits at the end of the line above at `:685`
and `:704`) became 「before commit 4f85e4d」: before that commit the
flow-facing record shared its nested values with the payload, which is
the reading each sentence quotes.
- **「objectstack-ai#14744 made」, 「objectstack-ai#14744 carries the fix」, 「objectstack-ai#14744 closed the door」**
at `:47`, `:95`, `:642`, 「Until objectstack-ai#14744」 at
`record-change-trigger.ts:341`, 「and objectstack-ai#14744.」 at `:124`, 「objectstack-ai#14744 —
DECOUPLE」 at `:453`, 「(unchanged by objectstack-ai#14744 —」 at `:496`: the number
became the commit, and each sentence already states what the commit did.

## The 4 sites left

- **Test strings, 4 sites on 4 lines**, all `describe` / `it` titles
carrying `objectstack-ai#14744`, left as stages 1 to 12 left theirs:
`before-update-flow-payload-reach.test.ts:825` and `:872` (the second
number of `[objectstack-ai#15356/objectstack-ai#14744]`, a spelling the gate's grammar cannot see),
`decouple-flow-record.test.ts:78` and `:136`.
- No source string, operator log string, assertion message, quoted
maintainer ruling or generated file in this package carries a dead
number.
- Outside `src`, the package's `CHANGELOG.md` names `objectstack-ai#14744` on 2 lines
(467, 478). It is release-owned and deliberately not edited here (see
Acceptance notes). The package `README.md`, which also ships, names none
of the three.

## Mechanical guard: no code token moves

The guard compares, base `91e8fa194` against head, over all 5 touched
`.ts` files:

- **Reading 1**, the TypeScript parser's leaf nodes (a `forEachChild`
walk, so comments are trivia and JSDoc nodes are never visited). String
and template literals are therefore read in full.
- **Reading 2**, the full token stream in parser context (a
`getChildren` walk, so punctuation and keywords are included; JSDoc
nodes skipped).

Results:

- Real run at the final head `bbfe7cb24`: 6,110 base leaf tokens, **0
files with a token change** on either reading (exit 0).
- Comment control in `record-change-trigger.ts` (「reach nothing outside
its own run.」 to 「reach nothing beyond its own run.」): 0 files changed,
as expected (exit 0).
- Positive control, a code token added in `record-change-trigger.ts`
(`params: isolatedRecord,` given `as typeof isolatedRecord`): DIFFER,
953 to 954 leaf tokens and 2,130 to 2,133 full tokens (exit 1).
- Positive control, one digit changed inside a kept test title
(`decouple-flow-record.test.ts:78`, `objectstack-ai#14744` to `objectstack-ai#14745`): DIFFER on the
string literal (exit 1).

Every mutation went through `scripts/ablation-replace.mjs` (wrap mode)
under a shell trap that restores by absolute path, and each landed
(anchor 1 to 0, blob changed). Each restore was proven byte-identical to
the HEAD blob (`f3235a962fc5`, `9a8bf70abbcc`), with `git diff HEAD`
empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/trigger-record-change`
(`.changeset/20596-trigger-record-change-provenance-anchors.md`) is
included. Its body is stage 12's, word for word, with the package name
changed.

Measured on the built package (A3), after a full workspace build in
which this package was a cache miss: `files[]` is `dist`, `README.md`
and `CHANGELOG.md`, and the package is not private.

- `4f85e4d11` appears 3 times in each of `dist/index.js` and
`dist/index.mjs`: the `buildContext` docblock
(`record-change-trigger.ts:340` and `:341`) and the inline comment at
`:496`, which the bundle keeps.
- It appears twice in each of `dist/index.d.ts` and `dist/index.d.mts`:
the same `buildContext` docblock.
- The other three anchors appear nowhere in `dist`: their lines are in
test files. The rewrites at `record-change-trigger.ts:124` and `:453`
and `decouple-flow-record.ts:5` are stripped by the bundle.
- Positive controls, one unchanged line beside each rewrite, land
exactly where their neighbours do: the line after `:341` once in all
four files, the line before `:496` once in each JS file and 0 in the
declaration files, and the neighbours of the three stripped rewrites 0
everywhere.
- A never-written negative phrase appears nowhere in `dist`.
- None of the three dead numbers is left in `dist`.

## Gates (final head `bbfe7cb24`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
exits 0 (self-test, 114 cases, 8 batteries). `node
scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged 1
added citation across 2 files, the live `objectstack-ai#15356` at
`decouple-flow-record.ts:5`, and it resolves.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0 (the
sibling-package prose-id baseline holds, no growth).
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `bbfe7cb24` (after a fresh fetch)
derived 59 commands. They are all 53 derived at dispatch, plus
`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`.
- Each ran with its exit code captured before any pipe, and all 59 exit
0; none exited 3.
- `--ran`, fed each command with its exit code, reports 59 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.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock, at `bbfe7cb24`:**
- `pnpm --filter @objectstack/trigger-record-change test`: 10 files pass
and 101 tests pass. `vitest list --filesOnly` names 10 files, all the
tracked test files, the 3 touched ones included.
- `pnpm --filter @objectstack/trigger-record-change typecheck` exits 0.
`tsc --listFiles` on `tsconfig.test.json` holds all 14 files under
`src/`, and on `tsconfig.json` the 4 non-test files, so all 5 touched
files are compiled.
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 5 touched `.ts` files, gives 5 files, 0 errors and 0 warnings
(its `--format json` output). All 5 are in eslint's own population
(`isPathIgnored` is false for each; a `dist` file, as the control, is
ignored). `eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), 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 6 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the
`#`, `NON_CITATION_HEADS` excuses a number after the word 「option」, and
a URL-spelled link carries no `#` at all (objectstack-ai#20636). In this package, at
the base and at the head: `#N-word` none, `#A/#B` 5 lines, `option #N`
none, URL-spelled none, which is the claim's 0 / 5 / 0 / 0. Of the five
`#A/#B` second numbers (`objectstack-ai#4251` twice, `objectstack-ai#5038`, `objectstack-ai#4649`, `objectstack-ai#14744`), only
`objectstack-ai#14744` is dead, and it stands in a kept test title.
- **`CHANGELOG.md` is left.**
`packages/triggers/trigger-record-change/CHANGELOG.md` names `objectstack-ai#14744` on
2 lines. It is release-owned (AGENTS.md, Documentation Guardrails), a
deferred surface of the citation gate, and ⛔ not part of this stage.
- **A live number in a runtime string, left for its lane.**
`record-change-trigger.ts:239`'s operator `warn` for an array-form
trigger event ends with the live `objectstack-ai#3457`, and two tests assert the
message carries it. That is form D, not this card's comment-only form C,
and the shrink-only `doc-authoring-prose-id` baseline already holds it
(`record-change-trigger.ts`: `objectstack-ai#3457: 1`), so `check:doc-authoring` sees
no growth.
- **「The card」 phrases are left.** 3 other comment lines in 2 files of
this package speak of 「the card」. They carry no number, neither
instrument sees them, and none of them lost a referent in this diff.
They are unchanged, as in stages 8 to 12.
- **The census instrument did not truncate in this stage.** All three
enumerations read 187 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#14744` →
`4f85e4d11` (the decoupling) or `03c1b0f6f` (its census), both new to
the sweep; `objectstack-ai#13657` → `b003cf2e8` and `objectstack-ai#11081` → `c28e4cfae` reuse the
runtime and lint stages' anchor and stage 7's.
- **Base.** The branch is on `main` at `91e8fa194`. `main` has since
moved six commits (`cd6d8a5ff`, `1bcba27d2`, `a3d7588b5`, `9ad654487`,
`274e16271`, `085ca6bc1`). Their 50 files touch nothing under
`trigger-record-change`, nor `scripts/check-issue-citations.mjs`,
`.changeset/config.json` or the `doc-authoring-prose-id` baseline, and
none is a path in this diff. Three of them are gate inputs
(`scripts/engine-double-contract.pinned.json`,
`scripts/objectql-double-limit.baseline.json`,
`scripts/sdui-manifest.record.json`), so those families ran here against
the base's copies; this diff moves no code token, so nothing here can
interact with them. No merge was taken; the merge queue rebuilds on the
merged generation.

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

---------

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/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants