Skip to content

docs(plugin-email): re-anchor the dead tracker citations to the commits that decided them - #20757

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20596-plugin-email-citations
Sep 30, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #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 #19123), by the method of stages 1 to 10 (PR #20609 as 422db788a, PR #20626 as b80ab579d, PR #20634 as 4d04b6be3, PR #20658 as 9a4b2bb38, PR #20693 as 0e9ad74fb, PR #20708 as 9b384f63a, PR #20717 as cbaf04c1f, PR #20729 as d2820876f, PR #20737 as 4dfff176b, PR #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 #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 #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 #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 #20748 (newest #20747 before, #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 #20753 (newest #20753 before and after) 1,057 0 0 0 0

The before count matches the seat's census and A1 (7 sites: #13189 ×4, #11741 ×2, #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: #13190 answers 404; #5169, #5286, #10619, #16506, #20374, #5197 answer 200 as issues, and #8348, #5191, #5211, #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 ' (6 sites in the template engine and its tests) and the fixture subjects Invoice #42 to Invoice #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 #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
#13189 13/4 8/5 33fbd3566 (PR #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 #13189; its diff writes the integrality docblocks the rewritten lines sit in. New to the sweep
#13190 5/1 3/2 56c5b1dbe (PR #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 #13190; its diff writes the #13190 comment block itself. New to the sweep
#11741 6/3 3/3 b706af987 (PR #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 #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
#8675 2/2 2/0 c9f595083: the squash commit of the pull request that was #8675 (its subject ends (#7987) (#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

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 (#11741); transports/smtp-port-contract.test.ts:225, :309, :340 (#13189); transports/smtp.test.ts:230 (#13190), :271 (#13189), :293 (#13190 and #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, #13189 to #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 # (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), 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 (#5197 ×2, #5191, #5211, #5232 ×2, #5177, #4251, #5094), so nothing there needed rewriting.
  • ADR-0131 names #11741. Its D7 cites #11741 as the writer fact that keeps sys_email tenant data. That is evidence inside a later record, not the record of what #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 #14484, stage 2).
  • CHANGELOG.md is left. packages/plugins/plugin-email/CHANGELOG.md names #11741, #13189, #13190 and #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 #13189 test block, they still have the kept (#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: #13189 → 33fbd3566; #13190 → 56c5b1dbe; #8675 → c9f595083. #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

…ts that decided them

Sixteen comment and docblock sites under packages/plugins/plugin-email/src
cited tracker numbers that no longer resolve. Each now cites the commit in
this repository's history that decided what the line describes:

- #8675  -> c9f5950 (sys_account OAuth tokens internal; the key-absence
  trap on optional columns and the absenceProvesStrip discriminator)
- #11741 -> b706af9 (SendEmailInput.organizationId, stamped pass-through
  onto sys_email by its producers)
- #13189 -> 33fbd35 (the SMTP port guard tests integrality, and the
  generated refusal sentence says so)
- #13190 -> 56c5b1d (a present-but-unreadable smtp_port reaches the
  guard instead of silently falling back to 587)

Comments only: 16 lines out, 16 in, every file keeps its line count, and
no code token moves. Test titles carrying these numbers are string tokens
and are left.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
The rewritten docblocks and inline comments ship: two anchors reach the
package's JavaScript entries and two its declaration files, so the change
publishes bytes and takes a patch changeset.

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 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-email, touching 3 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/plugins/plugin-email/src/internal-header-readback.ts, packages/plugins/plugin-email/src/transports/smtp-port-contract.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/email-templates.mdx (via sendTemplate (symbol, a method of class EmailService))
  • content/docs/kernel/index.mdx (via sendTemplate (symbol, a method of class EmailService))
  • content/docs/kernel/runtime-services/email-service.mdx (via sendTemplate (symbol, a method of class EmailService))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/plugins/plugin-email/src/internal-header-readback.ts, packages/plugins/plugin-email/src/transports/smtp-port-contract.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 — 5 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 33e4a5609c4d6cc012279f08e24e111c3370d14f → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 33e4a5609c4d6cc012279f08e24e111c3370d14f

⚠️ 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 33e4a5609c4d6cc012279f08e24e111c3370d14f → 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: 23283d394b2182a846066d4bd6fd1b7aa2d170ac
Local-runs: none

① Derived judgments

Read against main at the merge-base 97005aed0 (stage 10's landing 697845d19 plus two). The branch was fetched into an owned ref, refs/review/pr-20757, which resolves to the head above; nothing was read through FETCH_HEAD. origin/main at 33e4a5609 (the PR's recorded base) is three commits past that merge-base (9c8f113c6, a6866da0c, 33e4a5609); of the 24 files they touch, none is under packages/plugins/plugin-email, none is this PR's changeset, .changeset/config.json or scripts/check-issue-citations.mjs (the one .changeset/ file among them is #20599's own), and the merge-base diff and the three-dot diff against origin/main are byte-identical: 9 files, +26/−16 — 8 source files under packages/plugins/plugin-email/src/** (4 modules, 4 test files) and one changeset. The head 23283d394 adds only the changeset on top of 15a7d69a7, 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. 16 source lines out, 16 in; every one of the 32 opens with a comment marker after whitespace (// or *), and a -U0 diff filtered on those markers leaves nothing. Each of the 8 touched files keeps its line count (350, 383, 1445, 152, 396, 145, 324, 329), 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; no packages/spec file touched, so no generated artifact is owed.
  • Published bytes: changed — right, and it decides ②. @objectstack/plugin-email (17.5.0, not private, files = dist, README.md, CHANGELOG.md; in the changeset fixed group) ships the rewritten lines: the two inline comments inside EmailService.send and sendTemplate (email-service.ts:742, :1439), the SmtpTransportOptions.port docblock (transports/smtp.ts:68), the isValidSmtpPort and refusal-sentence docblocks (transports/smtp-port-contract.ts:87, :134) and the module docblock of internal-header-readback.ts (whose exports reach src/index.ts:131). The dev's A3 build reading (each sha located in dist with positive and negative controls, 56c5b1dbe correctly absent because its lines are test-only) says the same; this record does not repeat the build.
  • The 4 numbers are dead — right. Each of #13189 #13190 #11741 #8675 answers 404 on the issues endpoint (which serves pull requests too), read for this record. No ADR, scripts/adr-anchors/ file or other docs/ page records any of the four as its decision: a grep over docs/ and scripts/adr-anchors/ at the head finds only ADR-0131 line 474 naming #11741 as evidence inside its D7. Ruling C's first rung is empty, so a commit is the right anchor for every one.
  • The 4 anchors — each right. Each abbreviated sha resolves to exactly one commit (rev-parse --disambiguate, count 1 for all 4) and is an ancestor of the base (merge-base --is-ancestor, exit 0 for all 4). Each names the number it replaces, and the decision the rewritten lines state is the commit's. #13189 → 33fbd3566 (PR fix(plugin-email): refuse a fractional SMTP port at construction, in the sentence that promised to #13375): its changeset headline names #13189, its diff writes both smtp-port-contract.ts docblocks the rewritten lines sit in, and its message decides what the eight lines say — the guard tests Number.isInteger, a fractional port such as 587.5 is refused at construction, the generated sentence reads (expected an integer 1-65535) with the range still rendered from the constants. #13190 → 56c5b1dbe (PR fix(plugin-email): refuse a present-but-unreadable smtp_port instead of silently sending on 587 #13316): its changeset headline names #13190, its diff wrote the #13190 comment block and describe title in smtp.test.ts, and its message decides the three lines — a present-but-unreadable smtp_port is passed through to the constructor's existing refusal, while absent and '' still fall back to 587. #11741 → b706af987 (PR feat(spec,plugin-email,service-messaging,plugin-auth): widen SendEmailInput with optional organizationId, threaded from org-holding producers #11839): "Fixes Widen SendEmailInput with organizationId so sys_email can be stamped at its producers (Decision 2 of #11303) #11741" in its message; SendEmailInput / SendTemplateInput gain an optional organizationId, stamped verbatim onto sys_email.organization_id (pass-through only), sendTemplate forwarding it — what email-service.ts:742, :1439 and email-service.test.ts:342 say. #8675 → c9f595083: the squash commit of the pull request that was #8675 (subject ends (#7987) (#8675)); its diff declares sys_account's three OAuth token columns internal: true, records "measured: 16 red tests" on those required: false columns, and adds the absenceProvesStrip discriminator — exactly what internal-header-readback.ts:37 and email-headers-internal.integration.test.ts:251 now say, and the two PR #N spellings in scope became this sha.
  • Line origin — right. git blame at the base puts 12 of the 16 rewritten lines in their anchor commit and the other 4 in a descendant of it: 61581462b for the two #8675 lines, and 33fbd3566 for smtp.test.ts:272 and :281, which cite 56c5b1dbe — right, because those two sentences state #13190's bucket rule, not #13189's narrowing, and form C anchors what the sentence describes. The dev's "anchor or a descendant" reading holds for all 16 pairs.
  • The wordings — each right. smtp-port-contract.test.ts:228 「the card that SPENDS」 → 「the change that SPENDS」, so the noun matches a commit. internal-header-readback.ts:37 「PR fix(security): sys_account OAuth access/refresh/id tokens stop serializing on the data API (#7987) #8675 hit exactly this」 → 「Commit c9f5950 records exactly this」 — the commit's own diff is where the 16 red tests are recorded, so "records" is the truer verb. email-headers-internal.integration.test.ts:251 「The regression PR fix(security): sys_account OAuth access/refresh/id tokens stop serializing on the data API (#7987) #8675 measured on a sibling card:」 → 「The regression commit c9f5950 records from a sibling card:」 — faithful, if a little stiff; a wording nit, not a finding. email-service.test.ts:342, a section rule: the 16-character phrase replaces a 6-character number and the trailing rule loses 10 characters, so the line keeps its width; checked. The five in-place swaps in smtp.ts:127 and smtp.test.ts:272, :276, :281, :283 keep each sentence's subject and tense.
  • Citation accounting — right. Over the diff: the 16 removed lines carry 16 dead occurrences (#13189 ×8, #11741 ×3, #13190 ×3, #8675 ×2) and no other number; the added lines carry no tracker number at all (a grep for # plus digits over the + lines is empty); 4 distinct shas stand on added lines; no PR #N stands on an added line. A grep of the four numbers over plugin-email/src returns 25 lines at the base and 9 at the head: the 16 rewritten lines, exactly.
  • The 10 sites left — right, and the list is exact. The 9 lines at the head carry 10 occurrences, every one a describe or it title: email-service.test.ts:349, send-template.test.ts:63, :88 (#11741); transports/smtp-port-contract.test.ts:225, :309, :340 (#13189); transports/smtp.test.ts:230 (#13190), :271 (#13189), :293 (#13190 and #13189). Titles are string tokens, left as stages 1 to 10 left theirs. No source string, log string, assertion message, quoted ruling or generated file in this package carries a dead number. Because that grep also matches a dead number standing as the second half of an #A/#B pair, none of the package's 9 slash lines carries one of the four; the liveness of their second numbers rests on the dev's single-number reads, not repeated here. packages/plugins/plugin-email/CHANGELOG.md names all four numbers on 5 lines (808, 917, 946, 962, 1696): release-owned, untouched, right. (The PR body says "three of these numbers" in one place and four in another; the file carries four. A body nit, not a finding.)
  • Form — consistent with the landed stages 1 to 10 (422db788a through 697845d19): the word commit plus the abbreviated sha in the position where the number stood, the decision carried in the sentence. The one reuse this thread can check, #11741 → b706af987, is the plugin-auth stage's anchor for the same number.
  • Check-runs on the head, the gate verdicts, read 2026-09-30T03:01Z: 34 check-runs, each name once, so latest-per-name is the list itself — 30 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in): paths-filtered or opt-in, not verdicts against), 1 in_progress, 0 failure. Of the seven required contexts, six are success — TypeScript Type Check, Test Core (the aggregate and all six shards), Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard — and one was still in_progress at that read: Lint & Repo Gates, which carries check:issue-citations and check:doc-authoring, the two gates this diff answers to. Check Changeset, Check PR Size, Part-of PR must not also close its card, The card this PR closes must claim this branch, No other open PR may claim the same issue and No other open PR may claim the same single-writer path are success. Not awaited: the ① judgments above rest on the diff, and the landing separately requires every check green, so the owning seat reads that one before it queues. Nothing was built, run or re-run locally.

② Semver level

  • .changeset/20596-plugin-email-provenance-anchors.md declares '@objectstack/plugin-email': patch — matches what the diff publishes. The package is released and its dist carries the rewritten comments and 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 10's landed form, and the filename carries the card number. plugin-email sits in the fixed group beside the ten 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 (5902547086) 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/**, docs/NORTH-STAR.md, .claude/**, skills/**, AGENTS.md, CLAUDE.md); 42 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 on line 1 and no closing keyword anywhere in the body, so the card stays open for the remaining stages. Both commits end with the model-free trailer pair and no model identifier appears in the diff, the commit messages, the PR body or the changeset.

③ Boundary flags

The dev report (5903104616) has open_questions: []. Its eleven deviations and two out-of-scope findings, and the ACCEPT's (5903127828) accepted list, each answered:

  1. 9 test-comment sites beyond the census's 7 — answered, in scope. The claim's surface is comment and docblock prose under plugin-email/src/**; test comments are that, and stages 1 to 10 rewrote theirs. The head grep above confirms the residue is titles only. Three of the nine carry #13190, which stands only in test files here, so the census never judged it; it was read on its own and answers 404 (confirmed for this record).
  2. Wordings beyond the tag swap — answered, right (① above). Every one sits on a line that already carried a dead number; no reflow line; every file keeps its line count.
  3. The read-channel refusal — answered, right, and nothing for this PR. A first card read by curl carrying the session token was refused by the local permission layer; the dev did not re-route it through another credential-bearing channel, took every later read through the read-only MCP tools, and sent every write (three relay strokes: the draft PR, the assignee, the report comment) through the relay. That is the rule applied as written, and the ACCEPT records it as it recorded stage 6's.
  4. Earlier dev reports for stages 1 to 5, 7 and 8 read only through their deviations and out-of-scope fields — answered, immaterial. The method is the landed form of stages 1 to 10, read off the tree; this record read all 47 comments on the card in full and found nothing there that binds this stage differently.
  5. Supplementary and raw instruments judged against the before census's own board reading plus single-number reads, no board-dump script — answered, immaterial here. Stage 6's method; the census instrument itself ran unchanged, and the residue at the head is verified above by grep.
  6. One docs/ grep loop discarded because its exit codes were a pipe's, then re-run without the pipe — answered, right. Process hygiene, and the re-run's reading (no ADR hit for the four numbers) equals this record's own grep.
  7. 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-URL trailer and Co-authored-by: Claude), no model identifier appears in either message, and the PR body's footer is the session-URL form that surface keeps.
  8. No pre-PR merge of main — answered, right. Verified above: the three commits past the base touch no path in this diff and no input the citation gate derives from, so the queue's rebuild has nothing to reconcile by hand.
  9. PR-body byte-level comparison NOT MEASURED — answered, immaterial. The body read over the API for this record carries every section named in the report and exactly one footer.
  10. Labels — answered. documentation, size/s, tests, tooling are the labeler's; no skip-changeset, which is right.
  11. Cleanup after the report (worktree removed, branch kept on the remote) — answered, immaterial to the head.
  12. Out-of-scope 1, ADR-0131 line 474 (D7) cites the dead #11741 — verified at the head; escalated to its carrier, not to this PR. docs/adr/** is Tier H and outside every stage of this card; the ADR names the number as evidence, not as the record of its decision, so the anchor here is rightly the commit. It joins the ADR-tree residue this seat already carries (ADR-0131's #14484 from stage 2; ADR-0055 and ADR-0094 from stage 4). Stage 9's landing note records that [finding] dead tracker citations outside packages/spec/src have no carrier: #20234 sweeps only the spec tree, and PR #20554 makes 26 more visible (pre-#N / Pre-#N) in cli, drivers, metadata, objectql, plugins, runtime and types #20556 has closed, so the seat names a live carrier when it leaves this residue — the same observation stage 4's record made, still open. Not blocking.
  13. Out-of-scope 2, 「this card」 on 20 comment lines in 8 files — answered. Wording only, no number, seen by neither instrument; stages 8 to 10 left theirs; the one rewritten line that said 「the card」 now says 「the change」, and inside the #13189 test block the kept (#13189) title is still the referent. Not blocking.

One advisory read, not a flag: the Docs Drift Check comment on the PR lists three pages via sendTemplate; it fired on the inline comment inside that method, and nothing those pages document changed.

Nothing is escalated against this PR. One reading for the seat, not a flag: Lint & Repo Gates was still in progress when this record was rendered, so the landing waits on its success as it always does.

Implemented-by: claude/issue-20596-plugin-email-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 30, 2026 03:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit cba417a Sep 30, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20596-plugin-email-citations branch September 30, 2026 03:23
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ugins/plugin-dev/src to the commits that decided them (objectstack-ai#20767)

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

## What changed

This is stage 12 of the `domain:cli` lane of the dead-citation sweep:
`packages/plugins/plugin-dev/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 11 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,
PR objectstack-ai#20713, PR objectstack-ai#20723, PR objectstack-ai#20735, PR objectstack-ai#20741, PR objectstack-ai#20748) are the
precedents. The card stays open for the lane's remaining packages, so
this PR says `Part of`.

That is **6 sites on 6 lines in 3 files, covering 2 numbers**, rewritten
to **2 distinct commits**:
- the census's **3 sites**, all in `src/dev-plugin.ts` (`:1063`,
`:1078`, `:1097`);
- **3 test-file comment sites** (the census defers `*.test.ts`; stages 1
to 11 took test comments too): `dev-plugin.test.ts:90` and `:127`,
`dev-plugin-security-enforcement-warning.test.ts:53`.

Only comments changed: **6 lines out, 6 in**, every one of them a site
(no companion line), and every touched file keeps its line count (1159 /
317 / 199), so no line citation into these files moves. **No citation
number is added**: the only tracker number on an added line is `objectstack-ai#3900`
at `dev-plugin.ts:1063`, which the removed line already carried and
which answers 200; no PR number stands on an added line. No ADR or
ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records
either decision (a grep there for the 2 numbers, their PR number objectstack-ai#10092
and the 2 shas reads 0 hits; the control number `7329` reads 1 file in
the same tree), so both anchors are commits. ADR-0115 records the older
decision the warning comes from (an empty security slot gets one loud
boot-log line), not the move these lines describe.

**A `patch` changeset** for `@objectstack/plugin-dev` rides along
(`.changeset/plugin-dev-provenance-anchors.md`, in PR objectstack-ai#20632's form),
because the two rewritten docblock lines reach the published `dist`
(measured below), as stage 6 (PR objectstack-ai#20703) measured for its package.

## Census: `packages/plugins/plugin-dev`, 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/plugins/plugin-dev/`. Both runs enumerated the whole
board.

| reading | tree | board | whole-repo `allocated-but-absent` | package
sites | lines | numbers | files |
|---|---|---|---|---|---|---|---|
| before | base `33e4a5609c`, run 2026-09-30T02:45:30Z to 02:51:51Z |
enumerated, 186 pages, frontier objectstack-ai#20757, 18,584 numbers | 1,061 | **3** |
3 | 2 | 1 |
| after | head `a237b10ee7`, run 03:07:39Z to 03:14:14Z | enumerated,
186 pages, frontier objectstack-ai#20765, 18,592 numbers | 1,058 | **0** | 0 | 0 | 0 |

The whole-repo drop of 3 is exactly these sites: a site-by-site diff of
the two JSON outputs has 3 findings gone (`dev-plugin.ts:1063`, `:1078`,
`:1097`) and none added. The other three tallies (`resolves` 33,038,
`resolves-as-pull-request` 1,984, `cross-repo-unjudged` 995) are equal
in both runs.

**Supplementary scan (test files, strings and files outside `src/`
included).** Every `#N` token (two to six digits) in the package's 19
tracked files, `CHANGELOG.md` excluded, was probed by REST: 39 distinct
numbers at base, of which 2 answer 404 in `src/` (`objectstack-ai#10035`, `objectstack-ai#10036`)
and 1 outside it (`objectstack-ai#13176`, in `tsconfig.test.json`); `objectstack-ai#1020` is
`cloud#1020`, cross-repo. Dead occurrences at base: 6 in `src/` comments
(3 source, 3 test), 1 in a test string, 2 in `tsconfig.test.json`.
After: 0 in comments, the test string and the two `tsconfig.test.json`
lines unchanged (see Acceptance notes). A grep for the two numbers with
no word-boundary operator, beside a control of the same shape (`objectstack-ai#3900`
reads 6 lines of `dev-plugin.ts`), finds only those three lines left.

## Per-number table

`git blame` at the base ties every one of the 6 lines to `7552e0337`,
the commit that wrote them, and each anchor was read in its message and
its diff, not only its subject.

| number | sites (base line) | anchor: what it decided |
|---|---|---|
| `objectstack-ai#10036` | `dev-plugin.ts:1063`, `:1078`; `dev-plugin.test.ts:90`,
`:127`; `dev-plugin-security-enforcement-warning.test.ts:53` |
`7552e0337`: the "RBAC/RLS/masking are NOT enforced" warning stops
probing the three `SecurityPlugin.init()` internals
(`security.permissions`, `security.rls`, `security.fieldMasker`, which
the spec contract names implementation internals) and asks the published
`security` service instead, and asks it from `DevPlugin.start()`, after
the child-start loop and beside the boot banner, since asking from
`init()` would find it absent on every stack; the internal handles keep
one use, telling "never loaded" apart from "loaded, then failed to
start". Both halves of its squash message carry this number. Its own PR
number (objectstack-ai#10092) answers 404 as well. |
| `objectstack-ai#10035` | `dev-plugin.ts:1097` | `c1731d023`: `plugin-hono-server`'s
`/auth/me/permissions` and `/me/apps` delegate permission-set resolution
to the `security` service, and their degraded branches key on the
published `security` service instead of `security.permissions` (its
docblock "What absent now means, precisely"). The site's sentence says
the same presence signal misled that endpoint and was cured "by this
same move"; `objectstack-ai#10035` is that commit's own PR number, carried in its
subject. |

**How the lines read now.** `:1063` keeps `objectstack-ai#3900` and says `commit
7552e03 moved this check here from init()`; the `:1078` heading and
the test-comment brackets name `commit 7552e03` where the number
stood, with the decision spelled out in the surrounding prose they
already carried; `:127` reads `(the two told apart since commit
7552e03)`; `:1097` reads `commit c1731d0 by this same move`.

**Anchor checks.** Both cited shas match exactly one object (`git
rev-parse --disambiguate`, count 1 each), are commits, have one parent,
and are ancestors of `main` (`merge-base --is-ancestor` against
`33e4a5609c`, exit 0 for both). The checkout is not shallow. Control
legs: `44738f7af6` (the parent of `c1731d023`) exits 0 against the base;
the negative control (the base as an ancestor of `7552e0337`) exits 1.

**Numbers.** `objectstack-ai#10035`, `objectstack-ai#10036` and `objectstack-ai#10092` answer 404 by REST (probed
2026-09-30T02:43:04Z and again at 03:14:40Z). `objectstack-ai#3900`, kept on `:1063`,
answers 200.

## Mechanical guard: no code token moves

**H2 holds on the token reading; the emitted `dist` is NOT
byte-identical, and the difference is exactly the two docblock lines.**

**Token guard.** It compares the TypeScript parser's leaf tokens
(TypeScript 6.0.3, `getChildren` walk, JSDoc nodes excluded) of the 3
touched files at base `33e4a5609c` and at `37eaf1647f` (the comment
commit). Controls mutate the head text in memory only.
- Real run: 6,653 base tokens, 0 files differing.
- Comment-insertion control: 0 differing.
- Code-insertion control: all 3 files differ.
- String control (the first character of the first import specifier
flipped in each file): all 3 files differ, first differing kind
`StringLiteral`.
- The script's own verdict: exit 0 (real 0 and every control as
expected).

All 12 changed lines in `src/` (6 out, 6 in) are `//` or `*` comment
lines.

**Emitted `dist`.** `pnpm --filter @objectstack/plugin-dev build` at
base (before any edit, after its dependency closure) and at
`37eaf1647f`. Of the 6 `dist` files, `index.js.map` and `index.mjs.map`
have equal sha256; `index.js`, `index.mjs`, `index.d.ts` and
`index.d.mts` differ, and `diff -r` shows exactly two changed lines in
each: the `:1078` heading and the `:1097` line of the
`warnIfNothingIsEnforcingSecurity` docblock. The `//` comment at `:1063`
does not ship. So the published tarball carried both dead numbers, and
now carries the commits.
- Code-mutation control (`scripts/ablation-replace.mjs`, wrap mode,
anchor `ctx.logger.info(' Discovery: /.well-known/objectstack');` hit 1
to 0, planted marker 0 to 1, blob `708af69f9b2a` to `b0b387f53d6a`;
`scripts/ablation-dist-preflight.mjs` found the marker in
`dist/index.js` and `dist/index.mjs`): `index.js`, `index.mjs` and both
source maps differ from the head build. The blob was restored to HEAD
`708af69f9b2a` with `git diff HEAD` empty, `dist` was rebuilt, the
preflight in `--absent` mode reads the marker absent from all 6 files
with a clean tree, and the 6 sha256 values equal the head build.
- The whole-workspace build (below) left `plugin-dev`'s `dist` equal to
the same 6 values.

A raw scan of the 4 changed files for ASCII control bytes finds none (a
positive probe on a scratch file with one such byte reads 1), and
`check:nul-bytes` exits 0.

## Changeset

**`patch` for `@objectstack/plugin-dev`.** The package publishes
(`files` is `dist`, `README.md`, `CHANGELOG.md`), and the measurement
above shows the rewritten docblock reaching four `dist` files. The
changeset states comments only, with no behaviour change.
`check-empty-changeset`, `check-changeset-no-major`,
`check-adr-0087-registration` (1 non-breaking changeset seen) and
`check-changeset-fixed` all exit 0.

## Gates (head `a237b10ee7`)

This host has no `flock`, so `os-verify-lock.sh` ran in its declared
unlocked mode. Its official wording, verbatim (printed by every run; the
command line differs per run and is listed in the verdicts below):

> **Declared narrowing — verification ran UNLOCKED.**
`scripts/pm/os-verify-lock.sh`
> could not take the shared verify lock on this host: no usable `flock`.
The shared
> verify lock is declared Linux-only (`flock` is util-linux, and a stock
macOS does
> not ship it), so the command below was run directly, without the lock
—
> a declared narrowing, not a silent one. No serialization guarantee
held for this
> run, nor for any sibling agent in this container while it ran.

Its verdict line from each run (the closure build at base `33e4a5609c`;
the head build at `37eaf1647f`; the whole-workspace build, the tests and
the typecheck at this head):

```text
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 65s (1m05s) · declare it in the PR body · pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-dev...' 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/plugin-dev 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 97s (1m37s) · declare it in the PR body · pnpm exec 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 7s · declare it in the PR body · pnpm --filter @objectstack/plugin-dev 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 6s · declare it in the PR body · pnpm --filter @objectstack/plugin-dev typecheck
```

- **Build:** `plugin-dev` with its dependency closure (36 packages, the
filter spelled with the package included), then the package, then the
whole workspace, `turbo run build --filter=./packages/*
--filter=./packages/*/* --concurrency=2`, 71 of 71 tasks. The tree was
clean after each.
- **Tests:** `vitest run --maxWorkers=2`: 9 files, 86 tests, all passed.
- **Typecheck:** `pnpm --filter @objectstack/plugin-dev typecheck` (`tsc
--noEmit`, then `check:test-typecheck` over `tsconfig.test.json`) exits
0. `--listFiles` under both configs reaches all 12 `src/` files,
including the 9 tests and the 3 touched files.
- **Spec artifacts:** not run. `origin/main` did not move while this
branch was open (still `33e4a5609c`; the merge was a no-op), and this
diff does not touch `packages/spec`.
- **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`)
exits 0 at this head (2026-09-30T03:07:02Z to 03:07:32Z).
- **Citation judging:** after merging `origin/main` (already up to date
at `33e4a5609c`), `node scripts/check-issue-citations.mjs --base
origin/main` judges 1 added citation (`objectstack-ai#3900`), which resolves (exit 0).
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 62 families from the 4
changed paths. All 62 exit 0 in one pass at this head, and `--ran` with
the exit-coded record reads "62 derived, 62 run, 0 NOT-MEASURED, 0
UNRUN" (a derived zero). Among them: `check:issue-citations`,
`check:doc-authoring`, `check:nul-bytes`, `check:published-files`,
`check:cross-package-test-inputs`, `check:dts-closure`,
`check:dual-build-cjs-loads`, `check:type-check-debt`,
`check-empty-changeset`, `check-adr-0087-registration`.
- **Artifact rosters:** 36 of the 39 non-self-test roster rows exit 0 at
this head, among them `check-changeset-fixed` and the three others 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 `33e4a5609c` the filtered census answers 3 sites
on 3 lines, 2 numbers, 1 file, as on the seat's `0be898499f`. The
whole-repo count is 1,061.
- **H1 holds.** After the rewrite, the filtered census answers 0 for
`packages/plugins/plugin-dev`. No site was left for an open PR (the file
lists of all 8 open PRs were read at 2026-09-30T02:45:10Z: only the
Version Packages PR objectstack-ai#20639 touches the package, in `CHANGELOG.md` and
`package.json`) or for an unfound anchor.
- **H2 holds, by the token reading, not the `dist` reading.** The parser
leaf-token diff of all 3 touched files is empty with its controls
firing. The emitted `dist` is not byte-identical, and it is not meant to
be: its only difference is the two docblock lines, which is why the
changeset ships.

## Acceptance notes

- **Strings, the form-D stage.** One dead number remains in a string
literal: the `describe` title at
`dev-plugin-security-enforcement-warning.test.ts:121` (`objectstack-ai#10036`). It
stays on the card for its form-D stage; no string moved here. It is not
assertion text. The same title is quoted in three recorded CI-log
fixtures under `scripts/fixtures/merge-queue-triage/`; those are
captured logs read by `check-merge-queue-triage-outcome.mjs`, so a later
rename of the title does not need them edited.
- **Outside `src/**`:** `tsconfig.test.json:3` and `:56` cite `objectstack-ai#13176`,
which answers 404. The same number sits in the `tsconfig.test.json` of
13 `packages/plugins/*` packages (17 `tsconfig*.json` files under
`packages/` in all), outside the census's declared surface; stage 10 (PR
objectstack-ai#20741) recorded its own copy for a later stage of this card. Every
other citation in the package outside `src/` answers 200
(`vitest.config.ts`, `README.md`, `tsconfig.json`, `package.json`);
`CHANGELOG.md` is release-owned and was not read as a site.
- **`origin/main` did not move.** It read `33e4a5609c` at worktree
creation and at every later fetch, so every run above is against the
same base and nothing needed rerunning after the merge.

## Deviations

- **The two builds inside the code-mutation control** (the mutate leg
and the restore leg) ran directly, not through `os-verify-lock.sh`. On
this host that wrapper runs unlocked anyway, so nothing was serialized
either way.
- **The dependency-closure build** used the filter
`'@objectstack/plugin-dev...'` (package plus its dependencies) rather
than the closure-only `^...` spelling; it built the same closure and the
package in one run.
- **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.

---
_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#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