Skip to content

feat(ci): differential gate for newly added cross-file line citations, and retire two stored line-number ledgers (objectui#8875) - #8974

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-8875-differential-citation-gate
Sep 10, 2026
Merged

baozhoutao merged 2 commits into
mainfrom
claude/issue-8875-differential-citation-gate

Conversation

@claude

@claude claude Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Refs #8875

⛔ Refs, not a closing keyword, and that is deliberate. This is PR 1 of two for ruling C. Clause 1 — the convention text in AGENTS.md — is a governed surface and lands as PR 2, which is third in this lane's AGENTS.md serial queue. A closing keyword here would take objectui#8875 out of every open-issue sweep the moment this merges, and the convention half would be lost rather than visibly pending. The card stays open for it.

Clause-②: no

The ruling, and its provenance

Maintainer, 2026-09-10, verbatim and untranslated (objectui#8875 comment 5617612419, on the decision analysis at comment 5617468956):

跨文件的「某文件第几行」引用, 这种完全没必要吧,是否应该避免

Direction C. Five clauses. This pull request implements 2, 3 and 5.

clause what it asks here
1 the convention text in AGENTS.md ⛔ not this PR — governed surface, PR 2
2 a differential gate on newly added cross-file line citations ✅
3 the two stored ledgers stop storing line numbers ✅
4 repair existing addresses when a file is touched; released CHANGELOG paragraphs are ⛔ never re-addressed ⛔ nothing swept here, by ruling
5 keep a known-wrong citation as the firing control ✅ — and a premise correction, below

Base origin/main fb828773d (tip committed 2026-09-10T11:53:26Z); head 45f6523a4 (committed 2026-09-10T12:20:50Z), a merge commit — see the section below. The branch point was 2c208d5bb (10:57:11Z) and origin/main moved once underneath it.

Clause 2 — the gate is DIFFERENTIAL, and that word is the whole design

scripts/check-new-cross-file-line-citations.mjs reports the cross-file line-address citations a pull request adds, against its merge base. The 540 existing citations are ⛔ not its denominator and it sweeps ⛔ none of them.

⛔ An absolute count was refused on a measurement, not on taste. PR #8887's line shifts flipped one citation from drifted to resolves by accident, moving the tree-wide false count 540 → 539, and the executing seat had to name that in its own body as an unearned green belonging to nobody. A total-reading gate scores that as progress.

Identity is content-shaped, not positional. A citation is keyed, within one citing file, by syntax | written path | cited line, compared as a multiset against the base blob. The citing file's own line number is deliberately not in the identity — if it were, re-indenting a file or adding an import above would report every citation below it as newly added, which is the positional fragility this card is about rebuilt inside the instrument meant to measure it. Renames are followed (git diff -M).

All five spellings the card measured, inherited from the census by import rather than re-implemented (two readers over one population is how two answers start disagreeing): the dominant NAME.ext:NNN, the NAME.ext#LNNN permalink, line NNN of NAME.ext, NAME.ext line NNN, and the continuation address — a bare :NNN carrying no filename at all, which no basename-anchored probe can see. Each has its own test.

unresolvable is a third answer and never collapses into false. A citation into a regenerated dist/*.d.ts is untracked and rebuilt, so nothing can ever decide it; calling it false would be an assertion. It is still reported as added — the convention is that the address is not written, not that it is written accurately — but its verdict column says unresolvable. A synthetic control pins the split so it cannot regress silently.

The state this lands in, and what flips it

Report-only. ENFORCEMENT in the script is the whole switch; its test reads the landed value, so the flip cannot arrive as a one-word edit. It ships report-only so pull requests already in flight are not failed by a rule whose convention text has not landed yet — an author failed by a rule is owed a document to be failed against.

What flips it: the gate reading zero new across the in-flight population, plus clause 1 landing in AGENTS.md. When it flips, the workflow owes a merge_group leg before the context may be required (objectui#3523: a required check that never reports on a queue build stalls the queue until the ruleset's 60-minute timeout).

⛔ Report-only does not mean "cannot fail". A control failure exits 1 in both modes: a differential gate reporting zero through a broken differ is indistinguishable from a clean branch, which is this card's own subject one level up.

Wiring, and the obligations that came with it

Its own workflow (line-citation-gate.yml), no path filter, pull_request only. ⚠️ Not a step in lint.yml: that job's in-job short-circuit skips every expensive step on a markdown-only, docs-only or changeset-only change — and the ruling names 注释、消息串、文档、changeset、脚本 in that order, so those are exactly the pull requests that carry the population. A gate that cannot see the shape most likely to trip it rebuilds the hole it exists to close.

The page objectui#3653 pins is satisfied in both directions: a ## heading naming the workflow file, a Workflow Inventory row, the command written out, and the paragraph declaring it report-only and that it exits 0 regardless of what it finds — the pin check-doc-snippet-emitted-census.test.ts already reads for the sibling census, asserted here too. scripts/dependabot-merge-gate.mjs classifies the new context in NOT_A_GATE with its reason, which its partition test requires.

Clause 3 — the two stored ledgers, and the proof they still behave identically

⚠️ This is the clause that can red main, so it is proven by running, ⛔ never by reasoning about the key shape.

UNGATED_EXAMPLES (check-doc-example-types.mjs) was keyed path:line symbol, and the line was a stored literal compared for equality. PR #8895 added three imports to packages/types/src/objectql.ts, every collected block moved down by three, and this gate reddened on a branch that had not touched an example — objectui#8614 is the same failure one card earlier. It is now keyed path symbol #ordinal, where the ordinal is the block's position among the examples of that symbol in that file. 114 of the 124 blocks are the only example on their symbol and carry #1; the ordinal exists for the five symbols that document more than one, and it moves only when a sibling example on the same symbol is added or removed.

The zh-only family citations (check-i18n-designer-table-parity.mjs) were three stored path:line addresses pointing into another file, printed in the report and followed by nothing. They are now { file, anchor }, and the pin was strengthened rather than relaxed: the old test asserted the citation matched a path:line shape and that the file existed; the new one asserts the anchor text is actually present in the file, exactly once, with an anti-vacuity control. A line number could never have been checked that way.

Before / after, exit codes captured before any pipe

gate before after
node scripts/check-doc-example-types.mjs 0 — 124 blocks, 35 compile, 89 fail, 89 declared (89 rows) 0 — the entire report byte-identical, diff empty
node scripts/check-i18n-designer-table-parity.mjs 0 — 1702 shared keys, 55 zh-only, all 3 families explained, en ⊆ zh 0 — counts and verdict identical; the only differing bytes are the three citation renderings
pnpm exec vitest run scripts/__tests__/check-doc-example-types.test.ts — 0
pnpm exec vitest run scripts/__tests__/check-i18n-designer-table-parity.test.ts — 0

⭐ The doc-example gate's before/after diff is empty. Not "the same verdict" — the same bytes, funnel counts, controls, injection census and closing sentence included. Separately, a row-by-row comparison of the two ledgers under the derived key map: 89 rows before, 89 after, 89 of 89 carrying an unchanged card / codes / reason, 0 unmapped.

⚠️ The gate needs the built closure to run at all; both readings were taken after turbo run build over the 35-package filter it names, so neither is a couldNotRun exit 2 read as a verdict.

Clause 5 — what the firing control actually is

The ruling asks that a known-wrong citation be kept as the firing control and names it as a path:line reference to check-doc-component-types.mjs.

⚠️ That address does not resolve. The line the ruling gives reads 'Not a node type.', — an unrelated string. Control: check-doc-component-types appears in 45 files, so the zero is a reading and not a failed grep.

⭐ Found by content, and it is intact. The known-wrong citation is the delete: entry of that file's key table, now at line 615, citing the action vocabulary in packages/core/src/actions/ActionRunner.ts at line 112. Verified by content: line 112 of that file is the closing */ of a docblock and export interface ActionDef { opens on the next line. Still wrong, still unrepaired, and the census still reports it [non-substantive] as its firing control.

⇒ ⭐ The ruling's own citation of a line-address reference could not be resolved as written — which is, without irony, the defect this card exists to stop. ⛔ No synthetic citation was planted in the tree and ⛔ the real one was not touched.

⚠️ But it cannot be this gate's firing control, and that is structural rather than a choice: a differential gate is blind to citations that already exist in the tree — that is the point of it. So this gate's firing control is synthetic: four fixture cases run through the real differ on every invocation, fatal in both modes.

Proving the gate can fail — a gate that cannot fail is worse than no gate

Two ablations on the real tree, each with on-disk mutation proof and a trapped restore verified by blob hash.

① A new citation is injected → it reds. Appended one cross-file address to scripts/check-lint-coverage.mjs.

marker occurrences   0 -> 1
blob HEAD            e34992f3c9a63bb135ec66eccd7b1cd1e1563f95
blob worktree        c25f510f46d8412a6460fecc5f8b7723f5f3acfe
gate (report-only)   EXIT=0   1 new citation(s)
gate (--strict)      EXIT=1   scripts/check-lint-coverage.mjs:139 adds ... [colon] -> false (non-substantive)
restore              blob back to e34992f3…, `git diff HEAD` empty

② An existing citation is merely SHIFTED → it stays silent. Pushed 40 lines above the clause-5 firing control in check-doc-component-types.mjs, moving that citation from line 615 to 655 without touching its text.

citation line        615 -> 655
blob HEAD            7d7f394b07d8ca42d0204302360fb8a1b4b361d1
blob worktree        dc03bfa000e30902e69ababa363ab38868578761
gate (--strict)      EXIT=0   0 new citation(s)
restore              blob back to 7d7f394b…, `git diff HEAD` empty

⭐ ② is the anti-absolute-count proof on the live differ, and it is the exact shape the ruling refused a total for. The 32 tests in the gate's own file pair every non-firing assertion with a firing one on the same code path.

Every gate run

gate exit
node scripts/check-new-cross-file-line-citations.mjs (this branch) 0 — 0 new, 4/4 controls PASS
node scripts/cross-file-line-citation-census.mjs 0 — both controls PASS; population 1267 → 1174
node scripts/check-doc-example-types.mjs 0 before and after, byte-identical report
node scripts/check-i18n-designer-table-parity.mjs 0 before and after
pnpm exec vitest run scripts/ 0 — 135 files, 3955 tests passed, 2 skipped
pnpm lint:root 0 — 0 errors, 32 pre-existing warnings
node scripts/check-entry-guard.mjs 0 — 83 files, 75 export bindings all inert on import
node scripts/check-control-bytes.mjs 0 — 7167 files
node scripts/check-changeset-presence.mjs 0 — 8 files changed, 0 published source of a released package ⇒ no changeset is owed
node scripts/check-pre-install-import-graph.mjs 0 — every non-relative leaf a node builtin
node scripts/check-action-ref-convention.mjs 0 — 112 refs in 36 workflows, 0 off-convention
node scripts/check-doc-links.mjs 0
node scripts/check-doc-fence-languages.mjs 0
node scripts/check-doc-component-types.mjs 0
node scripts/check-shell-escape-residue.mjs 0
node scripts/check-governed-queue-guard.mjs --test (11 paths) 0 — NOT GOVERNED

turbo ls --affected against the base reports 0 packages: this diff is root tooling, one workflow and one guide page, so the affected suite is scripts/ and it was run whole rather than filtered.

⚠️ One measured side effect worth stating rather than burying: retiring the two ledgers' stored line numbers removed 92 stored path:line literals from the tree, which is why the census population reads 1267 → 1174 (the four reader files' own fixtures account for the rest). ⛔ That is a consequence of clause 3, ⛔ not a sweep of clause 4's population — no prose citation anywhere was re-addressed.

⭐ The merge, and the argument it handed this change

origin/main moved to fb828773d (committed 2026-09-10T11:53:26Z) while this branch was open, and the two diffs collided in exactly one file — scripts/check-doc-example-types.mjs — for exactly one reason.

fb828773d (objectui#8106, PR #8965) had to RE-DERIVE a line-keyed UNGATED_EXAMPLES row. Its own diff moved lines above an @example block it never touched, so the row's key had to be edited from ListView.tsx:808 to :830 to keep main green. That is the third instance of this defect in a day — after useExpression.ts:163 on objectui#8867 and objectql.ts:1614 on PR #8895 — and it happened while clause 3 was abolishing the key shape that causes it. ⭐ It is the strongest argument this change has, and it is measured rather than asserted: an unrelated pull request paid a round trip to a number that no longer exists after this one.

How the conflict was resolved: onto the new key scheme, ⛔ never by picking a side. The row is now packages/plugin-list/src/ListView.tsx ListViewHandle #1, and the line number fb828773d had to re-derive is simply gone. The ordinal was re-derived against the merged tree rather than carried across: that symbol has exactly one @example block in that file, at line 830, so its ordinal is 1. ⛔ Merge, ⛔ not rebase — the branch was already pushed, so history is not rewritten.

Everything clause 3 rests on, re-measured against the NEW base

reading on fb828773d
merged tree, generated keys 124 blocks → 124 unique keys, 89 ledger rows, 0 rows naming no block
bijection against origin/main fb828773d's own ledger 89 rows before, 89 after, 89 of 89 carrying an unchanged card / codes / reason, 0 unmapped
node scripts/check-doc-example-types.mjs before vs after exit 0 both, and the full report diff is EMPTY again — byte-identical, not merely the same verdict
node scripts/check-i18n-designer-table-parity.mjs before vs after exit 0 both; the only differing bytes are still the three citation renderings

⭐ The row count did not change: 89 before, 89 after. The before/after pair was taken by swapping only the two gate scripts to fb828773d in place — the corpus under packages/** is byte-identical between this branch and that commit (git diff --name-only origin/main HEAD -- packages apps examples returns 0 files), so the swap isolates the gate change and nothing else. Both scripts were restored by blob hash (4230f632…, 2f64df94…) with git diff HEAD empty.

Re-run on the merged tree

gate exit
pnpm exec vitest run scripts/ 0 — 135 files, 3955 tests passed, 2 skipped
pnpm lint:root 0 — 0 errors, 32 pre-existing warnings
node scripts/check-new-cross-file-line-citations.mjs 0 — merge base fb828773d, 8 files compared, 0 new, 4/4 controls PASS
node scripts/check-changeset-presence.mjs 0 — 11 files changed, 0 published source of a released package ⇒ still no changeset owed
node scripts/check-control-bytes.mjs 0 — 7170 files
node scripts/cross-file-line-citation-census.mjs 0 — both controls PASS
turbo ls --affected (base = merge base) 0 packages

⚠️ CI never started on the previous head — the conflict meant no merge ref could be built, so a10d237a5 had zero workflow runs. On 45f6523a4 it does: 22 runs created, Line Citation Gate among them. ⛔ Queued is not green; the PM reads the conclusions.

What this PR does not do

  • ⛔ AGENTS.md and every other governed surface. Clause 1, PR 2.
  • ⛔ The 540 existing citations. The ruling says they are ⛔ not the gate's denominator and ⛔ not swept in. Shifting an already-false address by a hunk delta moves a wrong pointer to a differently wrong place while making the diff look diligent.
  • ⛔ Published CHANGELOG paragraphs. Historical record; the census's released-heading carve-out is inherited by the gate and pinned by a test in both directions.
  • ⛔ Widening the objectui#8047 ESLint rule to comments and message strings. That is option A and was ⛔ not ruled. ⭐ The rule is respected here rather than worked around: it went red on this branch's own it.each table, and the table was rewritten so the address stays data instead of reaching a test name.
  • ⛔ An absolute-count gate. Option B, ⛔ not ruled.
  • ⛔ Flipping this ready, arming auto-merge, or merging. The PM runs the landing pipeline.

Session reference, written as prose in a code span because a footer link does not survive an edit: session_01FhBNJcLRZLe8M87VcUgpKr.


Generated by Claude Code

…, and retire two stored line-number ledgers (objectui#8875)

Ruling C on objectui#8875, clauses 2, 3 and 5. Clause 1 (the convention text in
AGENTS.md) is a governed surface and lands as a separate, human-merged PR.

Clause 2 — a DIFFERENTIAL gate. `scripts/check-new-cross-file-line-citations.mjs`
reports the cross-file line-address citations a pull request ADDS, against its
merge base. The 540 existing citations are not its denominator and it sweeps
none of them. An absolute count was refused on a measurement: PR #8887's line
shifts flipped one citation from `drifted` to `resolves` by accident, moving the
tree-wide false count 540 -> 539, an unearned green that belonged to nobody.

It ships report-only (`ENFORCEMENT`), wired into its own workflow so it can see
the markdown-, docs- and changeset-only pull requests `lint.yml` short-circuits.
Its four synthetic controls are fatal in both modes — a differential gate that
reports zero through a broken differ is indistinguishable from a clean branch.
`unresolvable` (chiefly regenerated `dist/*.d.ts`) is a third verdict and is
never counted false.

Clause 3 — the two stored ledgers stop storing line numbers.
`UNGATED_EXAMPLES` is re-keyed from `path:line symbol` to `path symbol
#ordinal`, and the zh-only family citations in the designer-table parity gate
become `{ file, anchor }` with the anchor asserted present in the file exactly
once. Behaviour-preserving on today's tree, proven by before/after runs: the
doc-example gate's whole report is byte-identical, and 89 of 89 ledger rows map
onto the new keys with unchanged values.

Clause 5 — the firing control the ruling asked to keep is intact, but not at
the address the ruling gives. Reported on the card.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
One conflict, one line, in `scripts/check-doc-example-types.mjs`.

`fb828773d` (objectui#8106, PR #8965) had to RE-DERIVE a line-keyed
`UNGATED_EXAMPLES` row — `ListView.tsx:808` to `:830` — because its own diff
moved lines above an `@example` block it never touched. That is the third
instance of this defect in a day, and it landed while clause 3 was abolishing
the key shape that causes it.

Resolved onto the new key scheme, ⛔ not by picking a side: the row is now
`packages/plugin-list/src/ListView.tsx ListViewHandle #1`, and the line number
that had to be re-derived is simply gone. Re-derived against the MERGED tree
rather than carried: that symbol has exactly one `@example` block in that file,
at line 830, so its ordinal is 1.

Verified on the merged tree: 124 blocks, 124 unique generated keys, 89 ledger
rows, 0 rows naming no block; and against `origin/main` `fb828773d` itself,
89 rows before, 89 after, 89 of 89 carrying an unchanged card / codes / reason
under the derived key map, 0 unmapped.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
@baozhoutao
baozhoutao marked this pull request as ready for review September 10, 2026 12:43
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 10, 2026
@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Armed — SQUASH, in the queue at 2026-09-10T12:43:49Z, and the landing probe is written BEFORE the merge

domain:devx @ objectui PM seat (session_01FhBNJcLRZLe8M87VcUgpKr), 2026-09-10T12:45Z. Card objectui#8875, PR 1 of two.

step reading
CI on 45f6523a4 converged all green, 30 checks, Live E2E (informational) excluded — 12:43:18Z. ⭐ Includes Line Citation Gate: completed/success — the new gate ran in the runner, ⛔ not merely on this container
flip ready ready_for_review @ 12:43:26Z
post-flip Governed Surface Queue Guard completed/success @ 12:43:42Z — after the flip, ⛔ not the pre-flip run
auto-merge armed SQUASH
queue added_to_merge_queue @ 12:43:49Z, from the timeline event — ⛔ not from the auto_merge field

The probe, stated now so it cannot be fitted to the outcome

Baselines read on origin/main = 3ff3fc63f, tip committed 2026-09-10T12:13:05Z, at 12:44Z. After the merge each leg is re-read on a re-fetched origin/main.

# leg today expected after
A scripts/check-new-cross-file-line-citations.mjs in the tree absent present
B .github/workflows/line-citation-gate.yml in the tree absent present
C1 UNGATED_EXAMPLES rows keyed path:line symbol 89 0
C2 UNGATED_EXAMPLES rows keyed path symbol 0 89
D ENFORCEMENT = 'report-only' in the new gate 0 (file absent) 1

⭐ C1 and C2 are the same 89 rows read from both sides, and both transitions are clean — 89→0 and 0→89. A leg that only counted the new shape would pass while old rows survived alongside; this pair cannot. ⇒ it is the leg that actually proves clause 3, because the clause is 「stop storing line numbers」, ⛔ not 「also store symbols」.

Controls: leg A's firing control is the sibling scripts/cross-file-line-citation-census.mjs, present today and staying, so an empty answer later would be a broken path rather than a reading; C1 and C2 are each other's control, each firing exactly where the other is silent; leg D's baseline zero is a file-absent zero and is labelled as such, ⛔ not passed off as a measured absence of the string.

⚠️ ⛔ Not a probe leg, deliberately: the census population figure (1267 → 1174). It moves as a consequence of clause 3 retiring 92 stored literals, and a leg on it would be measuring the side effect rather than the change — and would drift on any unrelated edit, which is the whole property this ruling refused an absolute count for.

What lands, in one line

A differential gate, report-only, that reds only on cross-file line-address citations a pull request adds against its base — plus the two stored ledgers ceasing to store line numbers at all. ⛔ The 540 existing citations are untouched and are not its denominator. ⛔ The convention text in AGENTS.md is PR 2 and is third in this lane's serial queue on that file.

⚠️ ⛔ Refs #8875, not a closing keyword — the card stays open for PR 2. ⇒ on merge this seat posts LANDED and ⛔ does not strip pm:* or the assignee, because the card is not done.

Next: merge-wait, then the five legs on a re-fetched origin/main, then the LANDED note.


Generated by Claude Code

Merged via the queue into main with commit efead6c Sep 10, 2026
33 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-8875-differential-citation-gate branch September 10, 2026 12:59
This was referenced Sep 10, 2026
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 17, 2026
… clause 1) (objectstack-ai#9301)

Part of objectstack-ai#8875 — **PR 2 of two, clause 1 only**. PR 1 landed as objectstack-ai#8974
(`efead6c60`) and carried clauses 2, 3 and 5; this one carries the
human-facing convention that gate enforces. ⛔ This PR does not close
objectstack-ai#8875 on its own.

## ⛔ Governed surface — draft only

The diff touches `AGENTS.md`, so the whole PR is governed: ⛔ not flipped
ready, ⛔ not enqueued, ⛔ no auto-merge, ⛔ no self-approval. It waits for
a human merge, and that is the correct terminal state, not a failure.
Confirmed mechanically rather than asserted:

```
node scripts/check-governed-queue-guard.mjs --test AGENTS.md .changeset/issue-8875-cite-by-content-convention.md
⛔ GOVERNED — 1 of 2 path(s) are on a governed surface:
   AGENTS.md x1 — the repo-root agent instruction file
```

## The premise was re-derived on today's tip, with a firing control

The dispatch that queued this measured **zero** cite-by-content
sentences in `AGENTS.md` on `2c208d5bb` (2026-09-10). That reading is
two days old, so it was re-taken on `243fc8317e` (2026-09-12T14:56:42Z)
with the control in the same run:

| pattern in `AGENTS.md` | hits |
|:--|--:|
| `cite` (case-insensitive) | **0** |
| `按内容` | **0** |
| `行号` | **0** |
| `line address` | **0** |
| `line-address` | **0** |
| `path:line` | **0** |
| `8047` / `7853` | **0** |
| `\.(ts\|tsx\|mjs\|md\|js):[0-9]+` | **0** |
| **firing control** — `objectui#` | **26** ✅ |

The control fires (26, up from the dispatch's 22 — the file grew in
between), so the zero is a reading and not a broken grep. The only three
hits for `引用` in the file are `session 引用` and `issue 引用` in the
GitHub-body-rewriting section — a different sense of the word, not a
cite-by-content sentence. ⇒ the clause's 「若已有 cite-by-content 句则引之」
branch does not apply; this is new text.

## What was added

One new commandment in `## 5. Coding Standards`, **`objectstack-ai#10 — Cite by
CONTENT, not by line address; a CROSS-FILE `path:line` is banned
outright`**, placed after `objectstack-ai#9` (the closest neighbour in kind — "point
at the instrument, never write down its answer"). Five sub-points:

1. **The rule and its history.** Cite by a quoted distinctive string, a
symbol name, a test name, an anchor — never a `path:line`. A line
address is read by nothing, so it cannot fail; it rots the first time a
line is inserted above what it cites. objectstack-ai#7853 ruled the class
(`fa7d66c45`); objectstack-ai#6548, objectstack-ai#6998, objectstack-ai#7289, objectstack-ai#7913, objectstack-ai#8045 were the one-at-a-time
repairs, and the class kept recurring.
2. **⭐ The point this card adds.** objectstack-ai#8047 mechanized the ruling for
**test names only**, deliberately exempting comments and failure
messages on the ground quoted from that rule's own header — *"A human
reads them beside the code they annotate, and the next reader of that
code corrects a wrong one."* That justification is **positional**: it
holds for a SAME-FILE citation and does not survive the citation
crossing a file boundary. The reader of `packages/types/src/crud.ts` is
not looking at `packages/core/src/actions/ActionRunner.ts`; nothing puts
the cited line in front of them and nothing tells them it moved. The
blast-radius measurement (73 hits / 4 syntaxes, 14 moved by one branch's
line shifts, **10 of the 14 already false before the branch existed**)
is recorded explicitly as a historical reading, ⛔ not as a live count —
§5 `objectstack-ai#9` applied to the new text.
3. The maintainer's ruling, **原文照录、不翻译**.
4. **The gate is DIFFERENTIAL.** Points at `pnpm
check:new-line-citations` and its `ENFORCEMENT` constant rather than
restating today's mode, and says why an absolute count was refused.
Existing citations are ⛔ not the denominator.
5. **Existing citations** — repair when you touch the file, ⛔ never as a
sweep; ⛔ never re-address a published `CHANGELOG`; `dist/*.d.ts` is
`unresolvable`, not a finding. Plus the **stored vs computed** dividing
line for line-number ledger keys.

## Fences observed

- ⛔ `AGENTS.md` only (+ its changeset). `scripts/**` untouched — PR
objectstack-ai#8974 already landed that half.
- ⛔ The objectstack-ai#8047 ESLint rule was **not** widened to comments or message
strings (option A, not ruled).
- ⛔ No absolute-count gate added (option B, not ruled).
- ⛔ The existing citations were **not** swept — 0 of them touched.
- ⛔ No published `CHANGELOG` paragraph re-addressed.
- ⛔ `content/docs/releases/**`, `.claude/**`, `docs/adr/**`,
`skills/**`, `CLAUDE.md` untouched.
- ⛔ The new text itself contains **zero** `path:line` forms, and adds
**zero** code fences.

## Changeset

`.changeset/issue-8875-cite-by-content-convention.md`, **empty
frontmatter** (`---` immediately followed by `---`) — AGENTS.md §9's
first-class way to declare "no release". A `patch` would version-bump
the 39-package fixed group for a change no consumer can observe. `git
status` showed `A` (new file), not `M`: the pre-existing
`.changeset/8875-cross-file-line-citation-census.md` was not clobbered.

## Gates — exit codes captured **before** any pipe (`cmd > log 2>&1;
EXIT=$?`)

Run on the merged tree (`origin/main` merged in immediately before
opening, `AGENTS.md` being hot; merge was clean and brought no
`AGENTS.md` change).

| gate | exit | verdict line |
|:--|--:|:--|
| `check-changeset-presence` | **0** | `✅ No source or published
contract of a released package changed in this range, so no changeset is
owed.` |
| `check-changeset-no-major` | **0** | `✅ No changeset declares a major
bump.` |
| `check-control-bytes` | **0** | `✅ OK (scanned 7494 tracked text
file(s); skipped 85 binary)` |
| `check-doc-links` | **0** | `Links are valid across 17 scan roots.` |
| `check-shell-escape-residue` | **0** | `✅ OK (5/5 root(s) resolved —
AGENTS.md: 1 file(s), 15 fence(s) …)` |
| `check-doc-fence-languages` | **0** | `✅ every TypeScript block in 227
document(s) is fenced ts/tsx/typescript …` |
| `check-doc-expression-carriage` | **0** | `✅ Controls pass … ✅ Blind
spot: none` |
| `check-new-cross-file-line-citations` | **0** | `VERDICT
new-cross-file-line-citations: 0 new citation(s), enforcement
report-only -> exit 0` — all four synthetic controls PASS |
| `check-governed-queue-guard --test` | **3** | `⛔ GOVERNED` — the
classification this PR wants, not a failure |

**⚠️ One declared narrowing, not a silent skip:** `check:doc-examples` /
`check:doc-snippets` exit **2 = PREREQUISITE NOT MET** locally — they
need all 39 packages built (`… declares types at
packages/*/dist/index.d.ts and it is not on disk`). That is NOT
MEASURED, neither green nor red. They type-check TypeScript fences in
documents, and this diff adds **zero** fences (`git diff … | grep -c
'^+```'` → `0`), so there is nothing new for them to judge. CI runs them
on the full build.

Also **0 control bytes** by an independent scan of the changed files:
`grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` → exit 1 (no match).

## 维护者速读(草稿)

**改了什么** — 给 `AGENTS.md` 的 §5 加一条
`objectstack-ai#10`:引用代码时按**内容**指(引一段有辨识度的字符串、符号名、测试名、锚点),⛔ 不写
`路径:行号`;**跨文件**的行号引用一律禁止,同文件的保留 objectstack-ai#8047 的既有豁免。另加一个空 frontmatter 的
changeset(声明不发版)。⛔ 只动这一个文件,`scripts/**` 一行没碰。

**为什么改** — 这是维护者 2026-09-10 裁决 C 的第 1 条,objectstack-ai#8974 已经把第 2/3/5
条(那道差分门)落地了,门有了但**人读的约定还不在规程里**。本卡真正新增的那一点是:objectstack-ai#8047
之所以豁免注释和消息串,理由是「人读它的时候它就在被注释的代码旁边」——这个理由是**位置性的**,跨了文件就不成立:读 A 文件的人不会看着
B 文件,没有任何东西告诉他被引的那行已经挪了。一次 PR 范围内实测:73 条跨文件引用,14 条被那一个分支的行位移挪偏,**其中 10
条在分支存在之前就已经是错的**。

**风险与代价(含回滚)** — 风险极低:纯规程文本,不影响任何运行时、不发版、不改门禁行为。代价是 `AGENTS.md` 又长了 7
行。回滚就是 revert 这一个 commit,没有任何下游依赖它。⚠️ 一个真实但已知的副作用:这条约定把差分门(目前
report-only)的**规范依据**写死了,以后有人想把门翻成 blocking 时,不需要再重新裁一次。

**席位意见** — *(留空,等席位定稿成评论)*

**你要做的** — 读一遍 §5 新增的 `objectstack-ai#10`,确认措辞符合你 2026-09-10
那句裁决的本意(那句原话已逐字照录在条文里,未翻译),然后**由你来合并**——这是受管面,agent 不翻 ready、不入队、不自批。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr

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

---------

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants