Skip to content

fix(scripts): check-issue-citations reads a qualifier only when it names a known repository, so pre-#N / post-#N are judged and framework#N is this repository - #20554

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20330-citation-qualifier-grammar
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20330-citation-qualifier-grammar

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20330
Clause-②: no

What changed

scripts/check-issue-citations.mjs read ANY word#N as a repository reference, so pre-#12248, post-#6640, Pre-#N, POST-#N and Framework#N were classed cross-repo and never judged. Its qualifier is now a closed set. A token joined to #N names a repository only when it is an owner/repo form or a name in the new KNOWN_REPOSITORIES table, matched case-insensitively. Any other prefix is prose, and the number after it is this repository's and is judged.

One recogniser, repositoryOf, is asked in all three places: by extraction, by the board's probe set (boardWanted) and by the classifier (namesThisRepository). The three can no longer disagree about a token.

  • KNOWN_REPOSITORIES rows: objectstack and framework (both THIS repository), objectui, ui (an objectui alias, joined form only), cloud, hotcrm, hotcrm-heimao, os-tianshun-mtc and os-project-titanwind-ehr. Each row carries the measurement that put it in.
  • Prose form. objectui PR #10264, objectui #2670, cloud #2937 and framework #2679 read as their repository. The form is a known repository name, whitespace, an optional PR or issue, then #N. The alias ui is excluded from it, because UI #N is ordinary English.
  • No carry across a pair. In objectui#6110 + #6111 the second number stays this repository's. The convention is to qualify each number, and the one live site in the claim's surface is respelled: packages/spec/src/data/field.zod.ts:370 now reads objectui#6110 + objectui#6111. This is comment-only, with a patch changeset.
  • Ordinal heads. Closing the set exposes ordinals that were hidden behind a fake qualifier. NON_CITATION_HEADS gains OQ (ADR-0076 OQ#10, 10 sites) and PKCS (PKCS#11, 1 site). A hyphen joining a head to its # is now read as the same head (Prime-Directive-#12, 2 sites). PD#12 (8 sites) was already covered by the existing pd head once its candidate is refused.
  • Refusal text. The REMEDY text now states the grammar that judged the author.

A false red in the same seam, fixed because this change would have widened it

buildBoard probed only UNQUALIFIED numbers, so a diff adding objectstack#N was classified against a board that never asked about N. Reproduced on unmodified 288611e3e5: I appended objectstack#20330 (this live card) to a swept file and ran node scripts/check-issue-citations.mjs. It exited 2, reading board: probed (0 citations) and [allocated-but-absent] ... objectstack#20330. Reading framework#N as this repository would have inherited that false red on every site. The probe set is now boardWanted, meaning every citation judged here. The self-test pins it through probeBoard over a stub. The blocking rule is unchanged: findings still exit 2.

Measurements the design rests on

  • framework names this repository. git ls-remote https://github.com/objectstack-ai/framework answered HEAD 288611e3e5, identical to objectstack-ai/objectstack. The controls diverged: a nonexistent name under the same owner exited 128, and objectstack-ai/objectui answered its own HEAD 0eb9f36aca. The REST and web routes to framework answered 403 from this session's proxy (bound to configured repositories), so git's rename redirect was the readable instrument. framework#N / Framework#N therefore read as THIS repository.
  • Qualifier census on 288611e3e5, over the declared surfaces. There were 27 distinct candidates behind 1,655 sites:
    • This repository: framework 255, objectstack 111, objectstack-ai/objectstack 9, Framework 2.
    • Siblings: objectui 672, cloud 213, hotcrm 24, objectstack-ai/objectui 12, ui 11, objectstack-ai/cloud 9, better-auth/better-auth 3, os-tianshun-mtc 2, and 1 each of hotcrm-heimao, os-project-titanwind-ehr, objectstack-ai/objectos, objectstack-ai/ats and objectstack-ai/hotcrm.
    • Prose: pre- 284, post- 12, Pre- 4, Post- 4, PRE- 1 and POST- 1.
    • Ordinals: OQ 10, PD 8, Prime-Directive- 2, PKCS 1.
  • ui is objectui. objectstack-ai/ui does not exist, and ui#6837, ui#6206 and ui#6207 are objectui's records on its board (objectstack#6206 answers 404, so it would have been a false death).
  • Prose form, 27 sites. For every objectui number I read objectui's board and this repository's. The objectui record is the one each sentence describes: objectui #2670 is "Flow designer: render loop / parallel / try_catch as nested", cited from loop-node.ts, and objectui PR #4264 diagnoses a path on the right side of ==, cited beside PATH_SHAPED_LITERAL. This repository's same number is unrelated on every site. The cloud sites could not be read (private) and follow their context. There were zero false positives.
  • Pair carry, 48 sites. The measurement refuses a carry rule.
    • , and and: every cross-repo pair I could judge names THIS repository's second number. cloud#1013 and #10645 is this repository's cli serve issue (4 sites), cloud#1020, #5233 its org gate issue (6 sites), objectui#2561, #3021 its lazySchema PR, and objectui#3136 and #14492 answers 404 on objectui.
    • /: mostly carries, but not always. objectui#3226 / #4827 is this repository's [移交自 objectui] 登记 ADR-0087 D2 conversion 条目 page-header-subtitle-alias(description → subtitle) #4827, a conversion entry handed over from objectui and cited from conversions/registry.ts.
    • +: exactly one distinct pair exists in the corpus, which is too thin to establish a convention.

Census of the newly judged spellings

Taken with the gate's own --census --json at a3c14755f8 against an enumerated board (184 pages, frontier #20551). The per-site transition comes from the gate's --list before and after the change. Dead means allocated-but-absent. Four of the numbers (14657, 12998, 10194 and 8692) were re-probed directly and answered 404.

spelling sites now judged dead
pre-#N 284 26
framework#N 255 0
post-#N 12 0
Pre-#N 4 1
Post-#N 4 0
Framework#N 2 0
PRE-#N 1 0
POST-#N 1 0
total 563 27

Other readings, same run:

  • Newly deferred (25 sites): the 24 prose-form sites plus the respelled field.zod.ts:370. Four of them were base census deaths that were never deaths: objectui PR #8758 three times, and the respelled #6111.
  • No longer extracted (21 ordinals): OQ#10 ×10, PD#12/PD#10 ×8, Prime-Directive-#10/#12 ×2 and PKCS#11 ×1.
  • Whole-census tally:
    • Base 288611e3e5: 38,109 judged. resolves 32,202 · resolves-as-pull 1,863 · cross-repo-unjudged 1,535 · allocated-but-absent 2,509.
    • Branch a3c14755f8: 38,088 judged. resolves 32,689 · resolves-as-pull 1,891 · cross-repo-unjudged 976 · allocated-but-absent 2,532.
    • The board moved between the two runs, so the per-site transition above is the reading, not the tally difference. The cross-repo count reconciles exactly: 1,535 − 563 − 21 + 25 = 976.

Verification

All gates below ran at a3c14755f8, the branch head.

  • Self-test. node scripts/check-issue-citations.mjs --self-test exits 0 with 114 cases across 8 batteries (base: 73 cases across 7). The new battery qualifier (floor 40) pins every spelling both ways: lit on a live number and a FINDING on a dead one for pre- post- Pre- Post- PRE- POST-, and for framework Framework objectstack-ai/framework objectstack. It also pins cross-repo even when dead for objectui OBJECTUI ui cloud hotcrm objectstack-ai/objectui better-auth/better-auth, and covers:
    • an unknown word prefix read as prose;
    • the four ordinal heads;
    • the prose form, lit and dead, including PR #N and UI #N NOT being the prose form;
    • the pair, no carry (dead second number red) and qualified number by number (both deferred);
    • boardWanted and a probed-board resolution of objectstack#20330;
    • registry hygiene.
      live-corpus gains a pin that every qualifier the live corpus keeps names a repository.
  • Ablations. Six mutations went through scripts/ablation-replace.mjs, each landing on disk with the anchor count 1 → 0 and the blob changed, each red on its own case, and each restored with blob equal to HEAD 8b6cf12653dd and git diff HEAD empty:
    • M1: the recogniser returns any bare candidate. The self-test reds on "pre- is prose".
    • M2: the probe set reverts to unqualified-only. It reds on "the board's probe set".
    • M3: the framework row is renamed. It reds on "Framework is THIS repository".
    • M4: the hyphen head is off. It reds on "Prime-Directive-#12".
    • M5: the prose form is off. It reds on "objectui PR #N names objectui".
    • M6: the OQ head is removed. It reds on "ADR-0076 OQ#10".
  • Diff-scoped verdict (node scripts/check-issue-citations.mjs, as CI runs it): exit 0 on this branch. It judged the respelled line's 2 citations, both cross-repo-unjudged.
  • One-time end-to-end proof (no permanent test; injected uncommitted and restored with blob equal to HEAD and git diff HEAD empty). I appended pre-#12248 (dead) and framework#20330 (live) to packages/cli/src/commands/generate.ts and ran the diff verdict twice:
    • The branch gate exits 2. pre-#12248 is allocated-but-absent, framework#20330 resolves on a board probed (2 citations), and the respelled pair stays cross-repo.
    • The 288611e3e5 gate, from a temporary copy, exits 0 with all 4 cross-repo-unjudged. That is the hole this closes.
  • Census (--census --json): exit 0. It is report-only and never fails.
  • Derived gates. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 97 commands, and all 97 ran. 96 exit 0, including pnpm check:pm-dispatch-gates, whose dispatch-gates.mjs --self-test passes 1,976 cases. That self-test pins this file's :204 local-env declaration line, and every edit here stays below it or is line-neutral. --ran reconciliation reads "97 derived famil(ies) accounted for — 96 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
  • NOT MEASURED: check:dual-build-cjs-loads. Reason: it loads every package's built CJS entry, and this box holds no dist for 80+ packages. The diff changes no emitted code. @objectstack/spec was rebuilt, and its entry gates (check:browser-reachable-entries, check:entry-nameability) exit 0. CI's Lint and Repo Gates owns this one.
  • Spec package. pnpm --filter @objectstack/spec typecheck returned VERDICT command-exit 0. See the report comment for the test run.

Acceptance notes

  • Dead sites the census hands over, not rewritten here. One is under packages/spec/src/** and is input for the staged sweep on packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234: packages/spec/src/meta-spelling/manifest-collection-spelling.ts:71 pre-#10194. The other 26 are outside packages/spec/src/**, and no card names them:
    • packages/cli/src/commands/generate.ts:1842 pre-#14657
    • packages/cli/src/utils/storage-driver.ts:206 pre-#6345
    • packages/drivers/driver-sql/src/schema-drift.ts:2458, :2474 pre-#12998
    • packages/drivers/driver-sql/src/sql-driver.ts:3872, :16545 pre-#17590
    • packages/drivers/driver-sql/src/sql-driver.ts:18285, :18324 pre-#12998
    • packages/drivers/driver-sql/src/sql-driver.ts:20095 Pre-#12380
    • packages/drivers/driver-turso/src/remote-transport.ts:2624 pre-#12380
    • packages/lint/src/validate-searchable-fields.ts:312 pre-#8404
    • packages/metadata-protocol/src/protocol.ts:2793 pre-#10888
    • packages/metadata-protocol/src/seed-loader.ts:1947 pre-#11674
    • packages/objectql/src/action-governance.ts:339 pre-#14423
    • packages/plugins/plugin-auth/src/auth-manager.ts:5629 pre-#14762
    • packages/plugins/plugin-security/src/bootstrap-platform-admin.ts:266, :630 pre-#8692
    • packages/plugins/plugin-security/src/per-organization-catalog.ts:314 pre-#8692
    • packages/plugins/plugin-security/src/permission-set-projection.ts:482 pre-#6483
    • packages/plugins/plugin-sharing/src/backfill-sys-record-share-organizations.ts:5 pre-#14484
    • packages/runtime/src/domains/mcp.ts:364 pre-#8726
    • packages/runtime/src/sandbox/body-runner.ts:548, :735 pre-#14758
    • packages/runtime/src/sandbox/script-runner.ts:440 pre-#14758
    • packages/types/src/driver-error-classification.ts:608 pre-#13324
    • packages/types/src/node.ts:1428 pre-#10943
  • The same objectui#6110 + #6111 pair outside the claim's surface. It still reads #6111 as this repository's (404 here), so these are existing census deaths: packages/spec/src/ui/view.zod.ts:3634 (for the packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234 sweep), packages/metadata-core/src/form-predicate-root-policy.ts:14, :120 and :205, and packages/metadata/src/plugin.ts:910. Each respells to objectui#6110 + objectui#6111.
  • Pairs that read silently wrong, not dead. Several REPO#N / #M pairs name the qualifier's own second number, which resolves here as an unrelated record. Examples: hotcrm-heimao#35/#40/#59, objectui#2715/#2717, objectui#2711/#2722, objectui#4648/#4901, objectui#5018 / #6469, cloud#957 / #962 and cloud#930/#944. No gate can see these, because they resolve. The convention in the refusal text (qualify each number) is the remedy when someone next touches the line.
  • Seat 4's objectui PR #10264 specimen. packages/spec/src/api/export-job-family-retirement.test.ts:25 sits on a DEFERRED surface (packages/**/*.test.ts), and surfaceFor answers null for it. The census never judged that site. The prose form it names is now read correctly wherever the census does look.
  • Observed once: a truncated board enumeration accepted as a reading. My first branch --census read enumerated (126 pages) with frontier runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable #13977, against 184 pages and fix(trigger-api,service-automation): refuse an api flow with no per-flow secret, at arm time and at registration (#20529) #20551 on the re-run minutes later, and reported 9,160 never-issued phantoms. enumerateBoard stops at the first page without rel="next" and trusts the maximum it saw as the frontier. This diff does not touch that code. The diff-scoped verdict enumerates only past 400 distinct numbers. Recorded, not filed; the seat decides.
  • Scope declaration. NON_CITATION_HEADS (two rows) and nonCitationHead (the hyphen) are grammar next to the qualifier, not the qualifier itself. They are here because closing the qualifier made those 13 ordinal sites judged citations of this repository's chore: version packages #10, Migrate documentation site to Fumadocs with monorepo structure and shared content #11 and Add comprehensive test suite for Zod schema validation #12, which is false. No gate was added, and the diff-scoped blocking rule is unchanged.

Generated by Claude Code

…mes a repository

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
…elling both ways

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
…h number of its objectui pair

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 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 — 137 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 b05743433b7dfac9edb5e4cb92d6998d5aecff09 → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a3c14755f844c7989082ab67305c8a26620e2c78
Local-runs: none

Inputs read: card #20330 (body; comments 5861195855, 5861229543, 5881442171, 5881863678, 5882580588), PR #20554 (body, its one bot comment, file list, the three-dot diff origin/main...a3c14755f8 from git objects; merge-base 288611e3e5, and neither touched file moved on main since), and the 33 check-runs on the head, read once at about 2026-09-29T02:45Z.

① Derived judgments

The accept set of check-issue-citations, base to head, spelling by spelling. The base grammar (288611e3e5) took ANY word#N as a repository and deferred it unless the word was exactly objectstack or objectstack-ai/objectstack; a bare #N was always judged here. The head keeps CITATION_RE and adds one recogniser, repositoryOf, that the three consumers ask.

  • Joined form, moved INTO judgment (right). pre-#N, post-#N, Pre-, Post-, PRE-, POST- and every other unknown word: repositoryOf answers null, so the number is this repository's and judged. framework#N, Framework#N, objectstack-ai/framework#N, and every case variant of objectstack (Objectstack#N, OBJECTSTACK-AI/OBJECTSTACK#N) resolve to this repository's slug and are judged. All of these were cross-repo-unjudged on the base. Strengthening.
  • Joined form, moved OUT of judgment: none. I traced every branch of repositoryOf and namesThisRepository: a joined candidate is deferred only when it is an owner/repo form or one of objectui, ui, cloud, hotcrm, hotcrm-heimao, os-tianshun-mtc, os-project-titanwind-ehr (case-insensitive). Every one of those was already deferred on the base (any qualifier was). No spelling the base judged as this repository's is deferred by the head. ui is joined-only; UI #N stays a bare citation of this repository (pinned).
  • Owner/repo form stays open, unchanged (accepted). Any a/b#N is a repository on both base and head. On the declared surfaces the distinct two-segment candidates are all real owner/repo names; the one path-shaped candidate, D11/OQ#10, sits in a package.json (not a declared surface) and is an ordinal, so deferring it is the harmless outcome. Not a regression and not a hole an arbitrary prose word can use, since a single word never carries a slash.
  • Prose form, moved OUT of judgment (right, verified on the corpus). This is the ONE class that moves from "judged as this repository" to "cross-repo, unjudged": a known non-joined name, whitespace, optional PR or issue, then a bare #N. I read every such site on the declared surfaces at the head (about 28: content/docs/releases/v14.mdx:287,291, v16.mdx:562,713, packages/lint/src/validate-list-view-mode.ts:13,34, validate-predicate-path-refs.ts:140, plugin-approvals/src/approver-org-scope.ts:123, runtime/src/domains/automation.ts:2531, service-automation/src/builtin/loop-node.ts:50, map-node.ts:66, spec/src/automation/control-flow.zod.ts:208, spec/src/contracts/ai-service.ts:647, spec/src/conversions/registry.ts:6760,9959, spec/src/kernel/public-auth-features.ts:284, spec/src/migrations/** five sites, spec/src/ui/i18n-label-resolver.ts:35,78, view.zod.ts:2354,2579,4615,4664, widget.zod.ts:62). Each sentence names the sibling's record by its own content (objectui PR #8758 ... convertSortToQueryParams, objectui #2670 Phase 3, objectui #2338 ... ADR-0047 amendment, cloud #2937 contract, cloud PR #2005 ... apply_blueprint). The move is true on every site. The list is closed: the regex is built from the table, ui is excluded, and an unknown word before a bare #N never matches (pinned by the PR #12248 and Studio UI #12248 going red). The framework #N / objectstack #N prose forms resolve to THIS repository and stay judged (pinned lit and dead). Residual, named and accepted: cloud is also an English noun, so a this-repository number written directly after the word cloud would be deferred; one site exists today and it is cloud's by its sentence, and the base misread the same shape the other way (false deaths on objectui PR #8758 three times). The sentence's own word is the only instrument either way.
  • No carry across a pair (right). objectui#6110 + #6111 reads the second number as this repository's on both base and head; the head pins it red when dead and pins the number-by-number spelling as both deferred. The dev's 48-site reading (the , and and pairs name this repository's second number) refuses a carry rule; the code matches the reading. The prose regex cannot match across a pair either (objectui #6110 + does not end in name plus whitespace).
  • Non-citation heads OQ, PKCS, trailing hyphen (right). OQ#10, PKCS#11, PD#12, Prime-Directive-#12 were deferred on the base (their candidate was a "repository"); with the closed set they would have become judged citations of this repository's chore: version packages #10, Migrate documentation site to Fumadocs with monorepo structure and shared content #11, Add comprehensive test suite for Zod schema validation #12, which is false. The two rows and the hyphen strip restore them to not-a-citation. The head computes before up to the # (refused candidate included), so the existing pd and directive rows read PD#12 and Prime-Directive without a new row. Spaced OQ #N / PKCS #N also become not-a-citation: zero sites on the declared surfaces, and both are ordinal heads by nature, the same shape as the existing clause, option, batch rows. The hyphen strip only ever acts on a refused candidate (a bare # cannot be preceded by - under CITATION_RE), so it cannot drop a citation the base judged.
  • framework is THIS repository, in all three consumers. repositoryOf is the one recogniser: extraction keeps framework as the qualifier and framework#N as raw; boardWanted includes the number in the probe set; classifyCitation and the --probe-cause condition in run() both ask namesThisRepository. KNOWN_BY_SLUG maps objectstack-ai/framework to the same slug. The git ls-remote reading (this repository's HEAD answered under the former name; controls diverged) is corroborated by AGENTS.md itself, which calls this checkout framework/. Consistent.
  • The false-red fix is correct and cannot mask a death. buildBoard probed only unqualified numbers, so a qualified this-repository number was classified against a board that never asked about it (reproduced on the base: objectstack#20330, exit 2). boardWanted now probes every number the classifier will judge here. A probe that answers 404 adds nothing to the board, so a dead citation still lands allocated-but-absent; the only change is that live qualified numbers now resolve. Pinned over a stubbed probeBoard, and structurally (const wanted = boardWanted(rows, ownerRepo);).
  • Diff-scoped blocking rule unchanged. run() still returns 2 on any FINDING_CAUSES member in diff scope (text and --json branches untouched); --census still returns 0 report-only. FINDING_CAUSES unchanged.
  • Self-test battery, both directions. New battery qualifier, floor 40, and the case count is exactly 40, so deleting any case trips the floor. Every judged spelling carries a lit case (#100 on the stub board resolves) AND a dead case (#12248 must be a finding): the six pre-/post- case variants, framework, Framework, objectstack-ai/framework, objectstack, the prose form of this repository, and the unqualified second number of a pair. Every deferred spelling is pinned dead-here-yet-deferred (objectui, OBJECTUI, ui, cloud, hotcrm, objectstack-ai/objectui, better-auth/better-auth, the prose forms). The four ordinal heads pin extraction to zero. live-corpus gains the pin that no unrecognised qualifier survives extraction (floor 3 to 4). The six ablations each land on a case that goes red for exactly that regression: M1 recogniser open, red on pre- is prose; M2 probe set unqualified-only, red on the board's probe set; M3 framework row renamed, red on Framework is THIS repository; M4 hyphen head off, red on Prime-Directive-#12; M5 prose form off, red on objectui PR #N names objectui; M6 OQ row removed, red on ADR-0076 OQ#10. Minor and non-blocking: the unknown-prefix case (foo#12248) carries only a dead case; its lit path is the same branch the pre-#100 cases exercise.
  • Scope. Exactly three files: scripts/check-issue-citations.mjs, one comment-only line at packages/spec/src/data/field.zod.ts:370, and one changeset. The respelling is inside the claim's conditional grant (only if the grammar does not carry a qualifier across a pair; measured, it does not). No dead site rewritten under packages/spec/src/**; no new gate; no check:* script, workflow or job added (package.json and .github/** untouched). The dispatch-gates line-anchor pin: line 204 of the script is byte-identical on base and head (the local-env declaration), and every edit sits below it or is line-neutral. The REMEDY text names the new grammar and carries no tracker number.
  • Public surface. scripts/ publishes nothing. KNOWN_REPOSITORIES, repositoryOf, namesThisRepository, boardWanted are new exports of an internal script consumed by its own self-test only.

② Semver level

.changeset/spec-field-option-visiblewhen-pair-qualified.md: '@objectstack/spec': patch. Right: the diff changes one docblock line in src/data/field.zod.ts, shipped text in a released package, with no type, schema, export or runtime change; AGENTS.md takes patch for a fix in a released package and reserves skip-changeset for a diff publishing nothing. scripts/** publishes nothing and needs no changeset. The changeset body carries no model identifier. Check Changeset concluded success.

Clause-②: no. No authorable key is widened or narrowed; nothing an author writes changes.

③ Boundary flags

open_questions: none declared.

Deviations, each answered:

  1. NON_CITATION_HEADS gains OQ/PKCS and nonCitationHead strips a trailing hyphen: accepted, in-grammar. Closing the qualifier made 13 ordinal sites judged citations of this repository's chore: version packages #10/Migrate documentation site to Fumadocs with monorepo structure and shared content #11/Add comprehensive test suite for Zod schema validation #12; the rows are the fix for what the closing made false, and they move nothing the base judged.
  2. buildBoard and the --probe-cause condition ask namesThisRepository: accepted. It is the classifier's own reading applied to the probe set; the blocking rule is unchanged (judged above).
  3. REMEDY and one header docblock paragraph updated, line-neutral: accepted; the :204 pin verified byte-identical.
  4. framework measured through git ls-remote because REST and web answered 403: accepted; two control legs and AGENTS.md's own framework/ naming corroborate it.
  5. Commit trailers use the model-free pair (Claude-Session plus Co-authored-by: Claude) and the PR body ends with the session-URL footer: this is AGENTS.md's prescribed form, which outranks the harness reminder. Verified on all three commits.
  6. Runner artefacts (gate 86 first run killed by the dev's own timeout; first census read truncated, re-run reported): accepted; the reported readings are the re-runs.

Out-of-scope findings, each answered or escalated:

  • Census hand-over under packages/spec/src/** (meta-spelling/manifest-collection-spelling.ts:71 pre-#10194 dead; ui/view.zod.ts:3634 pair): answered, goes to the packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234 staged sweep as the claim directs.
  • ESCALATED to the seat: 26 newly visible dead citations outside packages/spec/src/** (all pre-#N / Pre-#N, listed file:line in the PR's Acceptance notes, four re-probed 404) plus 4 objectui#6110 + #6111 pair deaths in metadata-core and metadata. The dev was right not to rewrite them (the claim's file surface is closed), and right that no card names them. Under Prime Directive chore: version packages #10 a reproducible defect takes a card; the seat names the carrier. Not a blocker for this PR: the diff-scoped verdict judges only what a change adds, so these sites do not red any PR until touched.
  • ESCALATED to the seat: enumerateBoard accepted a truncated enumeration as a reading (observed once: 126 pages, frontier runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable #13977, 9,160 never-issued phantoms; re-run 184 pages). The function stops at the first page lacking rel="next" and trusts max(numbers) as the frontier with no cross-check against the per_page=1 newest read. Untouched by this diff; the census is report-only and the diff verdict enumerates only past 400 distinct numbers, so this PR's verdict is not affected. A verifier that silently degrades is the shape the repo's route-and-surface rule 3 forbids; it needs a card.
  • Pairs that read silently wrong but resolve (hotcrm-heimao#35/#40, cloud#957 / #962, ...): answered, unreachable by any gate because they resolve; the REMEDY convention (qualify each number) is the remedy on next touch.

Gate coverage on this head, read once (33 check-runs: 15 success, 3 skipped, 15 in_progress, 0 failure):

  • Lint & Repo Gates (lint.yml job lint, step at lint.yml:4971 runs pnpm check:issue-citations && node scripts/check-issue-citations.mjs, the self-test and the diff-scoped verdict on this very diff): in_progress, not concluded. This is the family that answers the gate's own behaviour on this diff.
  • Build Core (ci.yml job build-core, hosts pnpm check:dual-build-cjs-loads at ci.yml:2116, the family the dev did NOT MEASURE locally): in_progress, not concluded. CI does answer it.
  • Also not concluded: Test Core (1/6 to 6/6), Dogfood Regression Gate (1/3 to 3/3), Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Type Check · consumer gates, Type Check · workspace.
  • Concluded success: Check Changeset, Governed Surface Queue Guard, Type Check · source gates, Type Check · debt ledger, Spec property liveness, Live half-state sweep, Check PR Size, Check Documentation Links, Flag docs affected by code changes, Auto Label, the three claim guards, filter.
  • Skipped: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in).
  • No failure on the head; nothing to attribute to this diff.

This record judges the contract. The landing precondition (every required context green) is the seat's read at enqueue time and is NOT presumed here: Lint & Repo Gates and Build Core had not concluded when this was written. The PR does not touch a governed surface, so the queue is the path once green.

Implemented-by: claude/issue-20330-citation-qualifier-grammar
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 02:59
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 199002b Sep 29, 2026
38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20330-citation-qualifier-grammar branch September 29, 2026 03:32
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… sites to the commits that decided them (stage 5) (objectstack-ai#20576)

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

## What changed

This is stage 5 of the staged sweep: the `ui/` area
(`packages/spec/src/ui/**`, 133 files), plus two sites freed since stage
4: `data/filter-subtree-provenance.ts` (PR objectstack-ai#20460 landed without
touching it) and `meta-spelling/manifest-collection-spelling.ts` (the
census hand-over, comment 5882628946 on objectstack-ai#20234).
`ui/view-grouping-query.ts` is excluded because objectstack-ai#20446's claim holds it;
it carries 4 citations and none of them is dead, so the exclusion
removes nothing. Later stages cover the other areas, so this PR says
`Part of`.

Every comment and docblock site in that population that cites a tracker
number answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123). That is **87 comment sites**: 84 re-anchored and
3 respelled.
- **84 re-anchored, over 25 numbers.** Each rewritten line now cites the
commit in `origin/main` history that decided what the line describes,
and says in its own words what that commit decided. One line
(`ui/dashboard.zod.ts:686`) quotes ADR-0087 itself; it keeps ADR-0087 as
its citation and paraphrases the amendment heading instead of quoting
its number.
- **3 respelled**, so each number of a sibling pair carries its own
qualifier: `ui/view.zod.ts:3634` now reads `objectui#6110 +
objectui#6111`, and `ui/component.zod.ts:3479` and `:4140` now read
`objectui#8221's PR objectui#8758`. Each second number answers 404 here,
and the sentence attributes it to objectui (objectui REST: `issues/6111`
200, `pulls/8758` 200, merged 2026-09-09).

Only comments changed, plus the one generated reference page they
project into and a patch changeset. Every source file keeps its line
count (90 lines out, 90 in, over 25 files). Three of the 90 lines held
no dead number; each is the other half of a rewritten sentence:
`ui/action.zod.ts:400`, `ui/action-param-carryover.test.ts:13` and
`ui/expression-bindable-text-keys.test.ts:119`. No code token moves (see
the guard below).

**No tracker number is added.** Every tracker number on an added line
was already on the lines it replaces, and no `PR #N` is added.

## Census: before and after

**Instrument.** This is the instrument of stages 1 to 4, rebuilt for
this stage. It sends REST `GET
/repos/objectstack-ai/objectstack/issues/N` without following redirects,
for every distinct number cited in the population. The population is:
- the citation gate's own exported `CITATION_RE` and
`NON_CITATION_HEADS` at the base, kept when the qualifier is none,
`objectstack`, `objectstack-ai/objectstack`, `framework`, `pre-` or
`post-`;
- matched case-insensitively (`Pre-`, `POST-`, `Framework`);
- N of 100 or more, excluding `summon` heads.

A qualifier covers only the number it is joined to. Each site is
classified by the TypeScript parser as a line comment, a docblock, a
block comment or a string.

**Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The
dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at
the start, after every 100 numbers and at the end: 18 of 18 lit (200)
and 18 of 18 dead (404) over 6 checkpoints in the base run, and 15 of 15
and 15 of 15 over 5 checkpoints in the head run.

| reading | tree | numbers probed | 200 | 404 | 301 or other | dead
sites | lines | files | of which comments | of which strings |
|---|---|---|---|---|---|---|---|---|---|---|
| before | base `487a7846df`, probed 2026-09-29T02:57:01Z to 02:59:36Z |
400 | 372 | 28 | 0 | **111** | 110 | 26 | 90 | 21 |
| after | head `83e39641d0`, probed 2026-09-29T03:15:00Z to 03:18:06Z |
383 | 372 | 11 | 0 | **24** | 23 | 8 | 3 | 21 |

The head probe found no number newly dead since the base probe: the same
372 numbers answer 200. The head was probed at `83e39641d0`; every
census file is byte-identical at the final head.

**Cross-check under the grammar that landed during this stage.** PR
objectstack-ai#20554 (`199002b3e4`) landed the closed qualifier set while this stage
ran, and it reads `objectui PR objectstack-ai#8758` as objectui's number. Re-run with
that gate's own `extractCitations` and `namesThisRepository`, the same
population reads **108** dead sites before and **21** after, all 21 test
strings. The difference is exactly the three `objectui PR objectstack-ai#8758` prose
sites below, which that PR's own header measured as "census deaths here
that are not deaths at all".

**Per file.** Cited sites are every in-repo citation the population
reads, live or dead.

| file | cited sites (base) | dead before | by class | dead after |
|---|---|---|---|---|
| `data/filter-subtree-provenance.ts` | 9 | 3 | 3 docblock | 0 |
| `meta-spelling/manifest-collection-spelling.ts` | 8 | 2 | 2 line
comment | 0 |
| `ui/action-param-carryover.test.ts` | 5 | 3 | 2 line comment, 1 string
| 1 |
| `ui/action.test.ts` | 39 | 3 | 3 line comment | 0 |
| `ui/action.zod.ts` | 90 | 7 | 5 docblock, 2 line comment | 0 |
| `ui/bulk-action.test.ts` | 9 | 4 | 2 line comment, 2 string | 2 |
| `ui/bulk-action.zod.ts` | 9 | 3 | 2 docblock, 1 line comment | 0 |
| `ui/component-element-navigation-17987.test.ts` | 8 | 5 | 1 docblock,
4 string | 4 |
| `ui/component-type-vocabulary.test.ts` | 8 | 2 | 2 docblock | 0 |
| `ui/component-type-vocabulary.ts` | 2 | 1 | 1 docblock | 0 |
| `ui/component.test.ts` | 190 | 22 | 11 line comment, 11 string | 11 |
| `ui/component.zod.ts` | 265 | 20 | 16 docblock, 4 line comment | 0 |
| `ui/dashboard.zod.ts` | 52 | 1 | 1 docblock | 0 |
| `ui/expression-bindable-text-keys.test.ts` | 3 | 2 | 2 line comment |
0 |
| `ui/expression-bindable-text-keys.zod.ts` | 4 | 2 | 2 docblock | 0 |
| `ui/form-select-option.test.ts` | 3 | 1 | 1 docblock | 0 |
| `ui/index.ts` | 15 | 2 | 2 line comment | 0 |
| `ui/interaction-config-retirement.test.ts` | 19 | 1 | 1 docblock | 0 |
| `ui/react-blocks.test.ts` | 9 | 2 | 1 docblock, 1 string | 1 |
| `ui/react-blocks.ts` | 17 | 4 | 2 docblock, 2 line comment | 0 |
| `ui/view-form-features-root.test.ts` | 4 | 1 | 1 line comment | 0 |
| `ui/view-metadata-schema.test.ts` | 31 | 3 | 2 line comment, 1 string
| 1 |
| `ui/view-submit-redirect-url.test.ts` | 8 | 1 | 1 line comment | 0 |
| `ui/view.test.ts` | 136 | 2 | 1 docblock, 1 string | 2 |
| `ui/view.zod.ts` | 331 | 13 | 10 docblock, 3 line comment | 2 |
| `ui/widget-i18n-retirement.test.ts` | 17 | 1 | 1 line comment | 0 |

`ui/` alone went from 106 dead sites in 24 files to 24. The other 107
`ui/` files carry no dead site.

## Per-number table

Anchors are 9-hex commit abbreviations. "Wrote" means the commit's own
diff added the line being rewritten.

| number | comment sites / files | anchor: what it decided |
|---|---|---|
| `objectstack-ai#5970` | 4 / 2, `action.zod.ts:819`, `action.test.ts:268`, `:304`,
`:354` | `97e7e3caa`: `ActionSchema.visible` / `disabled` speak one
condition shape; `visible` gains its boolean arm. Stage 1's anchor for
the same number |
| `objectstack-ai#6276` | 7 / 1, `component.zod.ts:34`, `:165`, `:568`, `:2455`,
`:2546`, `:2554`, `component.test.ts:2126` | `78f0be872`: declares
`element:record_picker`'s flat `sort` / `limit` on the objectstack-ai#5611 rule
(maintainer ruling 2026-08-08, direction A). It wrote `:34`, `:2455` and
the "enumerate by the renderer's read pattern" lesson |
| `objectstack-ai#8794`, `objectstack-ai#8836` | 3 / 1, `filter-subtree-provenance.ts:130`, `:131`,
`:156` | `1850ebbb0`: corrects the reuse-safety claim from the survey
and pins the invariant. It wrote `:131` itself. Stages 1 and 3 gave both
numbers this anchor |
| `objectstack-ai#9933` | 7 / 2, `view.zod.ts:2517`, `:5060`, `:5086`, `:5118`,
`:5513`, `view-metadata-schema.test.ts:398`, `:410` | `d5552ca13`:
admits `columnState` as an explicitly runtime-only view-overlay key,
rejected by name at every authoring door. It wrote "explicitly out of
objectstack-ai#9933's scope" |
| `objectstack-ai#9972` | 3 / 2 (+1 unread), `component.zod.ts:2213`,
`component.test.ts:382`, `:3565`; also `:3612`'s `objectstack-ai#9881/objectstack-ai#9972`, a
slash-joined spelling the grammar does not read | `60e0f900a`: records
the live read point of `page:tabs` `items[].icon` and its accept-pin. It
wrote the `:382` header |
| `objectstack-ai#10194` | 1 / 1, `manifest-collection-spelling.ts:71` (`pre-objectstack-ai#10194`)
| `2306a765c`: `/meta/theme` and `/meta/analytics_cube` stop storing any
JSON as success and validate at the write door. The line now says "the
store-anything branch from before commit 2306a76". Stage 1's anchor |
| `objectstack-ai#10274` | 6 / 2, `component.zod.ts:2304`, `component.test.ts:308`,
`:405`, `:3591`, `:3603`, `:3612` | `d1ba685ec`: re-measures the four
pin citations and gates the class. Its gate header records that the
re-measure found two anchors wrong since they were written, which is why
a refresh re-reads. Stage 3's anchor |
| `objectstack-ai#10485` | 4 / 4, `index.ts:51`,
`interaction-config-retirement.test.ts:116`,
`widget-i18n-retirement.test.ts:112`,
`manifest-collection-spelling.ts:67` | `35ad101bc`: retires the `themes`
carrier and `ThemeSchema` whole (ruled B, 2026-08-21; ADR-0049 stays
cited). Stage 1's anchor |
| `objectstack-ai#11284` | 5 / 2, `react-blocks.ts:39`, `:94`, `:109`, `:295`,
`react-blocks.test.ts:139` | `5383fa670`: the react tier converges on
the metadata-tier vocabulary, deprecate-first. Its changeset heads
"(objectstack-ai#11284, maintainer ruling 2026-08-23)" |
| `objectstack-ai#11350` | 1 / 1, `index.ts:105` | `ece4dad31`: records the maintainer
ruling of 2026-08-23 that a type in an entry's public declarations must
be nameable from that entry. Same wording as stage 1's
`kernel/index.ts:53` |
| `objectstack-ai#11507` | 2 / 2, `component.zod.ts:1465`, `component.test.ts:2726` |
`88b9d749a`: declares `sys_activity.type` an open, author-extensible
vocabulary (maintainer ruling 2026-08-24, direction 4). Stage 3's anchor
|
| `objectstack-ai#11658` | 2 / 2, `component.zod.ts:1464`, `component.test.ts:2725` |
`1a6a19c31`: opens `RecordActivityProps.types` to author-contributed
kinds, executing that ruling. Stage 3's anchor |
| `objectstack-ai#11703` | 3 / 2, `action.zod.ts:399` to `:400`, `:460`,
`action-param-carryover.test.ts:12` to `:13` | `5cb62d88b`:
`clone_permission_set` carries all five copied facets; its params list
had silently dropped three. The lines now name "the silent-drop shape
commit 5cb62d8 fixed" |
| `objectstack-ai#11753` | 5 / 2, `action.zod.ts:66`, `:390`, `:398`, `:409`,
`action-param-carryover.test.ts:1` | `0e4e51b0a`:
`ActionParamSchema.carryOver`, the spec half of the 2026-08-25
maintainer ruling (recommendation A). It wrote every one of these lines,
and its changeset records the `visible: false` measurement `:398` names
|
| `objectstack-ai#12194` | 1 / 1, `view.zod.ts:4838` | `311433f6b`: declares the
metadata item-name grammar (`QUALIFIED_ITEM_NAME_PATTERN` among it) and
refuses it at the publish door. Stage 2's anchor |
| `objectstack-ai#12868` | 5 / 2, `view.zod.ts:2938`, `:2966`, `:3179`, `:7087`,
`form-select-option.test.ts:4` | `c459da6bc`: narrows the per-option
`default` key out of the form-view options vocabulary. Its changeset
records the ruled census `:2966` cites ("measured ZERO occurrences").
Stages 3 and 4's anchor |
| `objectstack-ai#12950` | 3 / 2, `component-type-vocabulary.ts:4`,
`component-type-vocabulary.test.ts:4`, `:101` | `225e7690f`: created
`component-type-vocabulary.ts`; its message records the readiness read
`:101` pins (`global:search` and `global:notifications` stay declared) |
| `objectstack-ai#13156` | 2 / 2, `view-form-features-root.test.ts:70`,
`view-submit-redirect-url.test.ts:110` | `fd289be45`: strips tracker ids
from function-declaration-built refusal prose. It wrote both lines.
Stage 3's wording ("commit fd289be's strip") |
| `objectstack-ai#13670` | 1 / 1, `expression-bindable-text-keys.zod.ts:72` |
`8c6a7fc0b`: records `text.value` as deliberately omitted; its message
states the ruling that `text`'s evaluation channel is `content` alone |
| `objectstack-ai#13672` | 3 / 2, `expression-bindable-text-keys.zod.ts:89`,
`.test.ts:65`, `:118` | `e854a531a`: narrows the `button` row to the
spelling its key reaches, and records `action:button` and `ui:button` as
deliberately out |
| `objectstack-ai#16626` | 1 / 1, `component.test.ts:2336` | `30b099078`: the objectui
pin bump to `53ded82bf7a4` that ships objectui#7754's array-analytics
lowering, the door the family waited on. The association is PR objectstack-ai#16788's
body (it names objectstack-ai#16626 as the card it lands), and `30b099078` is that
PR's merge commit; neither its message nor its diff names objectstack-ai#16626 (the
stage-3 objectstack-ai#11065 precedent) |
| `objectstack-ai#17987` | 9 / 2, `component.zod.ts:10`, `:4014`, `:4062`, `:4153`,
`:4193`, `:5068`, `:5226`, `:5457`,
`component-element-navigation-17987.test.ts:4` | `e233db9db`: declares
element-level `navigation` on `object-kanban` / `object-calendar` and
gives `object-timeline` its `ComponentPropsMap` row, executing the
objectui#8652 ruling (verbatim `B`) |
| `objectstack-ai#18003` | 1 / 1, `dashboard.zod.ts:686` | **ADR-0087**, the rung
above a commit. The line quoted the ADR's own amendment heading, number
included. It now reads "(ADR-0087, its 2026-09-13 amendment, 「the level
half」)": the ADR stays the citation and the fragment it quotes is
verbatim |
| `objectstack-ai#18177` | 5 / 2, `bulk-action.zod.ts:51`, `:169`, `:262`,
`bulk-action.test.ts:61`, `:307` | `adabccf5f`: `BulkActionParamSchema`
is strict and declares `dependsOn`, executing decision batch objectstack-ai#146 item
4, letter A |
| `objectstack-ai#6111` | 1 / 1, `view.zod.ts:3634` | respelled `objectui#6111` (not
re-anchored): it is objectui's number |
| `objectstack-ai#8758` | 2 / 1, `component.zod.ts:3479`, `:4140` | respelled `PR
objectui#8758` (not re-anchored): objectui's PR objectstack-ai#8758, merged 2026-09-09
|

No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/`
records the decision behind any of the 25 re-anchored numbers except
objectstack-ai#18003. ADR-0126 mentions objectstack-ai#11703 and objectstack-ai#11753 only as references
("permission-set precedent"), not as the record of either decision.

**Anchor checks.** Every sha on an added line is one of 23, and none is
on a removed line. At the base `487a7846df`:
- each matches exactly one object (`git rev-parse --disambiguate`, count
1, 23 of 23);
- each is an ancestor (`git merge-base --is-ancestor`, exit 0, 23 of
23); the control leg `e9584681a4` also exits 0, and the repository is
not shallow;
- for 22 of the 23, a grep of the commit's own message or diff finds the
number it replaces (the message for 16; the diff for `0e4e51b0a`,
`5383fa670`, `c459da6bc`, `225e7690f`, `e854a531a`, and for objectstack-ai#8836 in
`1850ebbb0`). `30b099078` is the exception explained in the table.
- Each commit was read for the rule its line states, not only for the
number. In most cases the commit wrote the very line it now anchors.

Wordings to check, each true of its commit:
- `filter-subtree-provenance.ts:130` and `:156` read 「survey commit
1850ebb records」: the survey was the card's, and the commit's message
records its measurement. It is stage 3's wording for the same relation
(「from the survey it records」).
- `component.zod.ts:1465` and `component.test.ts:2726` read 「maintainer
ruling commit 88b9d74 declared」: that commit landed the ruling
(direction 4) as the `sys_activity.type` declaration.
- `manifest-collection-spelling.ts:71` reads 「the store-anything branch
from before commit 2306a76」: before that commit, `PUT
/meta/theme/:name` stored any JSON as success.

## Mechanical guard: no code token moves

The check compares leaf tokens with comments stripped, base `487a7846df`
against the head. It uses the TypeScript parser's leaf tokens from the
head's lockfile, so template literals are scanned in context, and it
excludes JSDoc nodes. It ran over all 25 touched `.ts` files, and every
control mutates the head text in memory only.

- Real run: 101,836 base tokens, **0 files with a token change** (exit
0).
- Comment-insertion control (`ui/index.ts`): 0 files changed (exit 0).
- Positive control (a declaration inserted into `ui/view.zod.ts`): 1
file reads DIFFER at token 19222 (exit 1).
- Positive control (one digit changed in a `component.test.ts` test
title): 1 file reads DIFFER at token 14972 (exit 1).

Line balance holds in every file, 90 out and 90 in over the 25, and
every line count is equal at base and head. Tracker numbers:
added-not-removed is empty in every file. The net-removed numbers are
the 25 in the table, 85 sites: the census's 84 comment sites, plus the
slash-joined `objectstack-ai#9972` at `component.test.ts:3612`.

## Generated page

`check:generated` proved one artifact stale:
`content/docs/references/ui/expression-bindable-text-keys.mdx`, the
projection of `expression-bindable-text-keys.zod.ts`'s module docblock.
`check:generated --fix` regenerated only that page, and a re-run read
`All 15 generated artifacts are up to date`. Its two changed lines are
the `:72` and `:89` substitutions verbatim. No other docblock here
projects into a reference page, and nothing under `skills/**` moved.

## Changeset

This change ships bytes, so a `patch` changeset for `@objectstack/spec`
is included. It says only that the provenance comments were re-anchored.
`Clause-②: no`: no export, key, value or type moves (the guard above).

Measured on the head's built package: 6 touched sources are
`src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten
comments also reach `dist`:
- `c459da6bc` appears in 32 bundled `.js` files and 2 `.d.ts`;
- `adabccf5f` in 24 `.js` and 2 `.d.ts`; `d5552ca13` and `0e4e51b0a` in
24 `.js` each; `e233db9db` and `78f0be872` in 2 `.js` and 2 `.d.ts`
each;
- the positive control, the pre-existing sentence 「the object-field face
enforces」, appears in 32 files.

## Gates (head `1b885d3c27`)

- **Citation judging pass, run as CI runs it:** `pnpm
check:issue-citations && node scripts/check-issue-citations.mjs`, both
under the grammar PR objectstack-ai#20554 landed, exit 0. The self-test passes 114
cases in 8 batteries. The live, diff-scoped run judged 13 citations
across 11 files: 3 resolve and 10 are declared cross-repo references. It
reads "every citation this change adds resolves".
- **Doc authoring:** `pnpm check:doc-authoring` exits 0. Its 16,759
customer-facing strings across 1,174 spec sources carry no internal
issue id, and the sibling-package prose-id baseline holds with no
growth.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at this head derived 112
families, and all 112 exit 0. `--ran` reports 112 run, 0 NOT MEASURED, 0
unrun. A full `turbo run build` of `./packages/*` at this head ran
first, under the shared verify lock: 71 of 71 tasks, VERDICT
command-exit 0. So no gate met an unbuilt prerequisite.
- **Five roster gates the derivation flags for this diff** (their
rosters sit in `.changeset/` or `packages/`, so their silence proves
nothing): `node scripts/check-changeset-fixed.mjs`, `pnpm --filter
@objectstack/spec run check:spec-changes`, `pnpm check:authz-resolver`,
`pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`. All
exit 0.
- `pnpm --filter @objectstack/spec run check:generated` (derived) reads
`All 15 generated artifacts are up to date`, and `check:docs` reads `226
generated files in sync`.
- **Tests and typecheck, under the lock:**
- `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui
src/meta-spelling`: Test Files 98 passed (98), Tests 3452 passed (3452),
VERDICT command-exit 0 (the chain held the lock 142s on a shared box).
- The 16 spec suites outside `src/ui` that read the touched files'
source text or pin their lines: Test Files 16 passed (16), Tests 489
passed (489). They are
`scripts/{export-origins,file-description,root-index,schema-closure,skill-map-guards,strictness-ledger}.test.ts`,
`src/ai/tool-confirmation-prescription-tense.pin.test.ts`,
`src/api/api-entry-graph.pin.test.ts`,
`src/contracts/scoped-context.test.ts`,
`src/data/filter-subtree-provenance.test.ts`,
`src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence,union-author-message-pins}.test.ts`,
`src/system/constants/platform-object-names.test.ts` and
`src/type-alias-convention.pin.test.ts`. Three more suites matched the
reader scan and are not run here:
`scripts/build-schemas-check-mode.test.ts` only imports `ViewItemSchema`
(code the guard proves unchanged) and rebuilds schemas in a temp tree;
`scripts/def-key-collisions.test.ts` names `ui/view.zod.ts` only in a
comment; `scripts/published-projection-choke-point.test.ts` matched on
`build-react-blocks-contract.ts`, not a touched file. They are left to
CI.
- `pnpm --filter @objectstack/spec typecheck`: exit 0, including
`check:test-typecheck` (53 files, 251 errors, 138 pinned signatures
held). The same three runs also passed, with the same counts, on the
pre-merge tree.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 25 touched `.ts` files gives 25 files, 0 errors and 0
warnings. All 25 are in eslint's own population (`isPathIgnored` is
false for each, read through eslint's API). `eslint.config.mjs` never
enables type-aware linting (no `parserOptions.project`, which its own
lines 327 to 328 state), so a comment edit here cannot move the verdict
on any untouched file. The repo-wide `pnpm lint` is CI's run.
- **Merge probe:** a `merge-tree` of the head onto `origin/main`
`f572a7eb3c`, from a bare shared clone with no merge driver registered,
exits 0 (2026-09-29T04:15Z).

## Acceptance notes

- **Base and merge.** The branch forked from `487a7846df`, one commit
past the claim's stamp `6154165484` (PR objectstack-ai#20551, outside the surface).
`origin/main` then moved three commits, and `199002b3e4` (PR objectstack-ai#20554)
changed `scripts/check-issue-citations.mjs`, so the gate derivation read
STALE TREE. `origin/main` `dee9b26f6c` was merged in (`1b885d3c27`): a
clean merge with no driver-routed path and no lockfile change, touching
none of this diff's files. The PR's delta against `dee9b26f6c` is
exactly its 27 files. `origin/main` has since moved one more commit,
`1c761c0d71` (PR objectstack-ai#20565, tests in two other packages), which touches
none of them.
- **One site beyond the hand-over's list.** The hand-over named
`manifest-collection-spelling.ts:71` (`pre-objectstack-ai#10194`). The same comment's
first line, `:67`, cites `objectstack-ai#10485`, which also answers 404, and it is
rewritten too. The claim names this file; the fix is the same defect
class, mechanical in the form stages 1 to 4 fixed, in a file no other
claim holds, under the same gates. Reverting it would be one line.
- **Open PRs, re-read at 2026-09-29T04:22Z:** 8 open PRs, and none
touches any file in this diff. The one that touches `ui/` is objectstack-ai#20570
(objectstack-ai#20446's), on the excluded `view-grouping-query.ts`. The in-flight
`Claim:` comments on the 12 `pm:dispatched` cards were read too: only
objectstack-ai#20446's names a `ui/` file (`view-grouping-query.ts`, excluded above).
- **Hypothesis 2, measured.** In `ui/`, 14 sibling-qualified pairs leave
the second number bare: 13 on one line (`+`, `and`, `,`, `/` or `'s PR`
between them) and `component.zod.ts:3478` to `:3479`, split across a
line break. In 3 of them the second number answers 404 here and the
sentence attributes it to objectui (`objectstack-ai#6111` once, `objectstack-ai#8758` twice); they
are respelled above. In the other 11 the second number answers 200 here,
so it is judged as this repository's and left: `action.zod.ts:1603`,
`component.test.ts:2052`, `:2199`, `:2315`, `component.zod.ts:3276`,
`:3793`, `:4812`, `expression-bindable-text-keys.zod.ts:33`,
`page.test.ts:696`, `react-blocks.ts:256` and `widget.zod.ts:34`. The PR
objectstack-ai#20554 header measured `,` and `and` pairs as naming this repository's
number and `/` pairs as mostly, but not always, the qualifier's. Whether
any `/` pair here names objectui's number is not measured; a 200 here
cannot tell.
- **What stays in this population: 24 dead sites** (21 under the landed
grammar).
- **21 test strings**, left as tokens (vitest `it` / `describe` titles
in 7 test files): `objectstack-ai#6276` ×6, `objectstack-ai#11658` ×3 and `objectstack-ai#11507` ×1 in
`component.test.ts`; `objectstack-ai#9972` in `component.test.ts:411`; `objectstack-ai#17987` ×4 in
`component-element-navigation-17987.test.ts`; `objectstack-ai#18177` ×2 in
`bulk-action.test.ts`; `objectstack-ai#9933` in `view-metadata-schema.test.ts:406`;
`objectstack-ai#11284` in `react-blocks.test.ts:147`; `objectstack-ai#11753` in
`action-param-carryover.test.ts:17`; `objectstack#11195` in
`view.test.ts:3396`. None is a Zod `.describe()` text, an exported
string or a migration-entry field, so no form D site arises here.
- **3 comments that name objectui's live PR objectstack-ai#8758 in prose** (`objectui
PR objectstack-ai#8758`): `view.zod.ts:2354`, `:2579` and `view.test.ts:426`. They are
not pairs, and the landed grammar reads them as objectui's.
- **Left for later stages of objectstack-ai#20234** (the stage-4 landing comment
5882686893's list, unchanged): the migrations area, the `liveness/**`
notes, the `why` strings and the `PROVENANCE_WAIVERS` reason,
`rest-server.zod.ts`, the held `analytics*` and `driver/turso.*` files,
the 2 `AGGREGATION_CASES` note strings, and `data/`'s test strings and
deliberate markers.
- **The same rot outside `packages/spec/src`** is objectstack-ai#20556's, not this
card's. Examples met here: ADR-0087's own amendment heading
(`docs/adr/0087-metadata-protocol-upgrade-contract.md:390`, `:397`)
cites the dead `objectstack-ai#18003`, and ADR-0126 cites `objectstack-ai#11703` and `objectstack-ai#11753`; both
are governed. `packages/spec/scripts/strictness-ledger.test.ts:375`,
`:380` cite `objectstack-ai#9933`, and `check-objectui-pin-citations.ts` cites
`objectstack-ai#10274` and `objectstack-ai#9972`.
- **Unchanged wording.** `react-blocks.test.ts:140` says the 2026-08-23
ruling was "recorded on-card". The card is gone, and the changeset of
`5383fa670` (now cited on `:139`) records the ruling. The line holds no
number, so it is left.
- **The citation gate's reach.** It defers `packages/**/*.test.ts`. The
11 touched non-test files are in its judging population.

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

---------

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 protocol:data size/m tooling

Projects

None yet

2 participants