Skip to content

Commit d6a8444

Browse files
fix(scripts): lint-coverage's ratchet banner stops reciting which other gates carry the closing-keyword shape (objectui#9906) (#9937)
Fixes #9906 ## The claim, and what it measured `scripts/check-lint-coverage.mjs`'s rule-2 DEBT ratchet banner said, in the present tense, that the closing-keyword shape *"still stands in `scripts/check-type-check-coverage.mjs` and `scripts/check-action-forward-parity.mjs`"*. Re-measured on this branch's base with the `CLOSING_TRIGGER` the closing-keyword pins share byte-for-byte: | file the banner named | hits | |:--|--:| | `scripts/check-action-forward-parity.mjs` | **0** | | `scripts/check-type-check-coverage.mjs` | **1** — the source comment `` the fix objectstack#4074 applied in `types` ``, a **noun**, not an emitter | Both halves false. The first went false when the sibling repair before this one landed; the second when objectui#9898 landed. Neither could be noticed, because nothing read the sentence. ## The route taken, and why Triage named two acceptable repairs: restate the claim as a **point-in-time reading carrying its own timestamp**, or restate it as something that **cannot rot**. This takes the second. A timestamp does not fix the shape it stamps. Root `AGENTS.md` commandment #9 is explicit that the remedy for a derived-once figure is to *"point at the instrument that re-derives it — the script, the gate, the test — and never write down its answer"*; a dated reading is still the answer written down, and #9's own warning is that **a figure that is still correct is the dangerous case** — a reader who spot-checks a dated sentence confirms it and is still wrong the day after. So the banner now answers neither question from here: - **The trade-off it exists to explain is untouched** — keep the instruction, lose the keyword, spell the anchor `objectui#` rather than a bare `#` so a quoted message cannot match GitHub's closing grammar. - **The scope record survives as history**, not as a reading of the tree: the siblings the same repair would reach were reported rather than widened into (objectui#9595). - **The precedent moves to the pin**, which already records it by the names of the things — the rule-2 stale-entry message in `scripts/check-spec-symbol-derivation.mjs` and the pin block that holds it. Nothing is lost; it is one file away, beside the code that reads it. - Everything cited is cited by name, never by line address (root `AGENTS.md` commandment #11). ## The pin now covers the sentence, not only the keyword `scripts/__tests__/check-lint-coverage-closing-keyword.test.ts` already sliced the rule-2 region out of the gate source (`staleEntryRatchet`). The new assertion is an extension of that read, **not a new gate**: within that region, no gate may be cited but this one and its own pin — in any of the three spellings a banner reaches for (gate source file, pin file, `package.json` script key), with the directory prefix optional, because dropping `scripts/` is the cheapest way to reword around a scan that demands it. ⛔ **No tree is scanned and none may be.** The 2026-09-18 ruling closed the door on a new required gate for keeping cross-file claims honest, and this is deliberately the weaker rule: not *"the claim is true"* but *"the banner does not make the claim"*. That is the only version of this that needs no population to re-derive. ### Seen red first, on the pre-repair wording The new assertion was written and run **before** the banner was touched, against the unrepaired file: ``` × …and the banner names no OTHER gate, so it cannot recite a tree that moves expected [ …(3) ] to deeply equal [] + [ + "scripts/check-spec-symbol-derivation.mjs", + "scripts/check-type-check-coverage.mjs", + "scripts/check-action-forward-parity.mjs", + ] ``` ### …and four ablations, from the committed state, each proved on disk Every leg mutated the gate, proved the mutation landed (occurrence counts both directions plus `git hash-object` differing from the `HEAD` blob), ran, then restored under a `trap` and proved the restore by the blob hash returning to `fa7d4f9c1` with `git diff HEAD` empty. | ablation | result | |:--|:--| | baseline (committed) | `Tests 6 passed (6)` | | **A** — the removed sentence put back verbatim | `Tests 2 failed \| 4 passed` | | **B** — reworded to a **bare** filename, no `scripts/` prefix | `Tests 2 failed \| 4 passed` | | **C** — reworded to the **package script key**, no filename at all | `Tests 2 failed \| 4 passed` | | **D** — the scanned slice collapsed, claim pushed outside it | `Tests 2 failed \| 4 passed`, and on D the failure is the slice guard's own message: `the ratchet slice no longer reaches the banner it is scanning` | D is the leg that answers *"could this pass for the wrong reason?"*: `staleEntryRatchet` only proves its two markers exist in order, so a region collapsed to a few bytes would satisfy an emptiness check by containing nothing. The live leg now asserts the banner's own last sentence is inside the slice before it scans it. ### What the scan deliberately does not reach — stated, not assumed - A gate named **in prose** with no filename and no script key ("the type-check coverage gate"). Closing that means a hand-maintained list of every gate's English name — a second copy of the gate roster, which is this card's defect wearing a pin's clothes. - **Anything outside the rule-2 region** — the same boundary the `CLOSING_TRIGGER` source leg already had. The gate's leading docblock names its sibling in its opening sentence; that is a standing architectural relation ("the sibling of X, for the same reason"), not a reading of the sibling's contents, so it stays. A rotting claim written *there* would be out of reach and is covered by nothing. Both limits are now written in the pin's own header. ## The falsifiable zone — every item measured ABSENT - **An emitter in either named file the shared `CLOSING_TRIGGER` misses** — ⛔ none. The regex covers GitHub's full keyword set (`close`/`closes`/`closed`, `fix`/`fixes`/`fixed`, `resolve`/`resolves`/`resolved`). Re-run with JS string-concatenation seams, `\n` escapes and `//` comment continuations collapsed, so an anchor assembled across a line break is visible: `check-action-forward-parity.mjs` **0**, `check-type-check-coverage.mjs` **1** (the same noun). No `#` is built at runtime in either file — no `"#"`, no `'#'`, no `String.fromCharCode`, no `%23`. The nearest miss is a `reason:` string in `check-action-forward-parity.mjs` reading `…a path that is already closed " +` / `"(objectui#3842 …` — a keyword and an anchor across a concatenation seam, separated by `(`. `CLOSING_TRIGGER` does not match it and neither does GitHub's grammar, so the ruler and the parser agree: not a miss. - **A consumer that reads this banner's text programmatically, other than the pin** — ⛔ none. `scripts/__tests__/lint-workflow.test.ts` does read this gate, on two coordinates: its **leading docblock** (for hand-counted `object-ui/*` rule figures) and its **whole text** (for `object-ui/*` rule *names*). Neither reaches the rule-2 banner's prose, and this diff introduces no rule name and no count. The only slice of the banner anything parses is the pin's own. - **A reason the present-tense phrasing was deliberate** — ⛔ none. No ADR, no review comment, no test asserts the sentence as written. Its provenance is the opposite: the pull request that wrote it (objectui#9595) records the sentence's content in its own *"Out of scope, and deliberately not widened here"* section as the result of a one-off sweep — *"Lines printed, not counted"* — i.e. a point-in-time measurement transcribed into prose. That is commandment #9's rot shape by construction, not a design choice. - **A way the extended pin goes green on a rotted banner** — ⛔ none found at citation level; ablations A–D above are the evidence, and the two reaches the scan does *not* have are named rather than left implicit. ## Turned back rather than widened into - **Five byte-identical `CLOSING_TRIGGER` declarations** across `scripts/__tests__` (`check-action-forward-parity-closing-keyword`, `check-lint-coverage-closing-keyword`, `check-type-check-coverage-closing-keyword`, `spec-symbol-ledger-anchor-liveness-9537`, `spec-symbol-ledger-plugin-detail-7265`). Not this card, not this surface, and ⛔ not consolidated here. - **The sibling gates themselves** are untouched. This branch reaches exactly one gate's banner and one pin. ## Verification Affected tests, through the shared verification lock — **`Test Files 12 passed (12)`, `Tests 347 passed (347)`** at `37480e15f`: the pin itself plus every test that reads the edited gate or the changeset surface (`lint-workflow`, `check-lint-rule-coverage`, the two sibling closing-keyword pins, `check-test-path-roots`, `check-changeset-presence`, `check-changeset-claims`, `check-changeset-no-major`, `check-control-bytes`, `check-new-cross-file-line-citations`, `check-pending-changeset-literals`). Gates, each exit code captured by redirect-then-capture and never through a pipe — all **exit 0**: `node scripts/check-lint-coverage.mjs` (the edited gate itself, `46/46 packages linted`), `node scripts/check-changeset-presence.mjs`, `check:control-bytes`, `check:new-line-citations` (`0 new citation(s)`), `check:changeset-claims`, `check:pending-changeset-literals`, `check:test-path-roots`, `check:lint-rule-coverage`, `check:shell-escape-residue`, `check:entry-guard`, `check:unreferenced-sources`, `type-check:scripts`. Plus a control-byte self-scan over the three changed files: no match. ESLint, **narrowed and declared as such**: `eslint --no-inline-config` over the two changed source files reports **2 files linted, 0 errors, 0 warnings** (count read from `--format json`, not estimated). The narrowing is a measurement rather than a gap because `eslint.config.js` enables no type-aware linting — no `projectService`, no `parserOptions.project` — so this diff cannot move any untouched file's verdict. The repo-wide `pnpm lint` (`turbo run lint`) over the population `eslint.config.js` defines remains CI's run. Changeset: `.changeset/9906-lint-coverage-banner-cross-file-claim.md`, empty front matter — the explicit "no release" declaration. `check-changeset-presence` agrees on its own terms: *"3 file(s) changed, 0 of them published source of a package the release covers … no changeset is owed"*, and the declaration is filed anyway so the decision is recorded once rather than inferred. Not a governed surface: `check-governed-queue-guard --test` over the three changed paths exits 0 with `NOT GOVERNED`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz --- _Generated by [Claude Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0c789a4 commit d6a8444

3 files changed

Lines changed: 166 additions & 9 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
---
3+
4+
`scripts/check-lint-coverage.mjs`'s rule-2 ratchet banner no longer asserts, in
5+
the present tense, which other gates carry the closing-keyword shape
6+
(objectui#9906). It named two sibling gates and said the shape "still stands" in
7+
both; both halves were true when the sentence was written and both had gone
8+
false before anyone read it again, each falsified by a different pull request
9+
and neither by the one that wrote the sentence. The trade-off the banner exists to explain —
10+
spell the anchor `objectui#` rather than a bare `#`, so a quoted message cannot
11+
match GitHub's closing grammar — is unchanged, and the scope record of the
12+
repair that wrote it survives as history rather than as a reading of the tree.
13+
14+
`scripts/__tests__/check-lint-coverage-closing-keyword.test.ts` now also fails
15+
if the banner names any other gate at all, with a control that restores the
16+
removed sentence and proves the scan fires on it. Scripts and tests only; no
17+
package is released by this change.

‎scripts/__tests__/check-lint-coverage-closing-keyword.test.ts‎

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,14 @@ const FIXTURE_ANCHOR = 424242;
6868
const CLOSING_TRIGGER =
6969
/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:[A-Za-z0-9-]+\/[A-Za-z0-9-]+|objectui|objectstack)?#(?:\$\{|\d)/i;
7070

71-
/** The rule-2 ratchet, from its banner comment to the report banner after it. */
72-
function staleEntryRatchet(): string {
73-
const text = fs.readFileSync(gateSource, 'utf8');
71+
/**
72+
* The rule-2 ratchet, from its banner comment to the report banner after it.
73+
*
74+
* `rewrite` lets a control leg put a PRE-repair wording back, so a scan over
75+
* this region is proven able to go red on the same slice that reports green.
76+
*/
77+
function staleEntryRatchet(rewrite: (source: string) => string = (source) => source): string {
78+
const text = rewrite(fs.readFileSync(gateSource, 'utf8'));
7479
const start = text.indexOf('// 2. Ratchet — a declared gap that has been closed');
7580
expect(start, "rule 2's ratchet banner is gone").toBeGreaterThan(-1);
7681
const end = text.indexOf('// ── Report', start);
@@ -116,6 +121,79 @@ function renderStaleEntry(rewrite: (source: string) => string = (source) => sour
116121
}
117122
}
118123

124+
/**
125+
* The `scripts/` files this banner is allowed to name: the gate the banner is
126+
* written in, and the pin that reads it. Both travel WITH the sentence — a
127+
* reader of either is looking at the other, and whoever moves one is the person
128+
* the citation is in front of.
129+
*
130+
* Every other `scripts/` path is a different gate's source, and a sentence here
131+
* about a different gate's source is a reading of a population that moves
132+
* without this file and re-derives never. objectui#9906 is that shape landed:
133+
* the banner said the closing-keyword spelling "still stands" in two named
134+
* sibling gates. Measured with the `CLOSING_TRIGGER` below, over each named
135+
* file as of each merge commit rather than inferred from pull-request titles:
136+
* both halves were TRUE the day the sentence was written, and it went stale in
137+
* two steps — the first about 35 hours later, the second the same day after
138+
* that — each step a different pull request and ⛔ neither of them the one that
139+
* wrote the sentence. Nothing could have noticed, because nothing read it.
140+
*
141+
* ⚠️ That last reading is itself dated, and it lives HERE and on the card
142+
* rather than in the banner, on purpose. This file is what a person opens when
143+
* they are deciding how to word that banner, and the card carries the commits
144+
* the reading was taken at. A first draft of the repair put a version of it in
145+
* the banner instead — and got it backwards, claiming a half was already false
146+
* on the day of writing. ⭐ A dated reading asserted where nothing re-reads it
147+
* was wrong within one review round, which is the argument for this whole rule
148+
* arriving by being committed rather than by being made.
149+
*
150+
* ⛔ This is NOT a check that any such claim is TRUE — no tree is scanned here
151+
* and none may be (the 2026-09-18 ruling closed the door on a new gate for
152+
* cross-file claims). It is the weaker, cheaper, unrottable rule: the banner
153+
* does not make the claim at all. If a future reader needs to know which gates
154+
* carry the shape today, the answer is those gates and their own pins, not a
155+
* sentence in this one.
156+
*/
157+
const CITABLE_IN_THE_BANNER = new Set([
158+
'check-lint-coverage.mjs',
159+
'check-lint-coverage-closing-keyword.test.ts',
160+
'lint:coverage',
161+
]);
162+
163+
/**
164+
* The three spellings a banner reaches for when it names a gate: the gate's
165+
* source file, a pin's file, and the `package.json` script that runs the gate.
166+
* Directory prefixes are optional on purpose — dropping `scripts/` is the
167+
* cheapest way to reword around a scan that demands it, and a bare
168+
* `check-type-check-coverage.mjs` cites exactly as hard.
169+
*
170+
* ⚠️ What this deliberately does NOT reach: a gate named in prose with no
171+
* filename and no script key ("the type-check coverage gate"). Closing that
172+
* would mean a list of every gate's English name — a second copy of the gate
173+
* roster, maintained by hand, which is the defect this pin exists to stop
174+
* wearing a pin's clothes. The guarantee here is bounded and stated rather
175+
* than assumed: the banner carries no CITATION of another gate.
176+
*
177+
* ⚠️ Nor does it reach outside the rule-2 region — the same boundary the
178+
* `CLOSING_TRIGGER` source leg below already has. The gate's leading docblock
179+
* names its sibling in the opening sentence, and that citation is a standing
180+
* architectural relation rather than a reading of the sibling's contents, so it
181+
* is left where it is; a rotting claim written THERE would be out of this
182+
* scan's reach and is not covered by anything.
183+
*/
184+
const GATE_CITATION =
185+
/(?:\bscripts\/(?:__tests__\/)?)?(?:[A-Za-z0-9._-]+\.test\.tsx?|check-[A-Za-z0-9._-]+\.mjs)\b|\b(?:check|lint):[a-z][a-z0-9-]*\b/g;
186+
187+
/** The same citation with any directory prefix dropped, so the two spellings compare equal. */
188+
const basename = (citation: string) => citation.slice(citation.lastIndexOf('/') + 1);
189+
190+
/** The gates the ratchet region names that are neither this gate nor its pin. */
191+
function foreignGateCitations(region: string): string[] {
192+
return [...new Set(region.match(GATE_CITATION) ?? [])].filter(
193+
(citation) => !CITABLE_IN_THE_BANNER.has(basename(citation)),
194+
);
195+
}
196+
119197
describe('the stale-entry message may not carry a card-closing keyword', () => {
120198
it('the harness and the regex can both fire — the control, on the wording this card replaced', () => {
121199
// ⛔ Without this leg every assertion below is unfalsifiable: a substitution
@@ -159,6 +237,63 @@ describe('the stale-entry message may not carry a card-closing keyword', () => {
159237
expect(rendered).toContain('can be ended once that work is done');
160238
});
161239

240+
it('the foreign-citation scan fires on the sentence this card removed — the control', () => {
241+
// ⛔ Without this leg the assertion below is unfalsifiable: a regex that
242+
// matches nothing and a banner that names nothing render identically.
243+
//
244+
// The pre-repair sentence is restored byte for byte, as objectui#9906
245+
// measured it — including its two present-tense halves, which were false
246+
// on the tree the day this control was written. Restoring it HERE plants no
247+
// claim: it lives inside a string this file feeds to its own scan and never
248+
// reaches the gate on disk.
249+
const region = staleEntryRatchet((source) => {
250+
const restored = source.replace(
251+
"// alone; the siblings the same repair would reach were reported rather than\n"
252+
+ "// widened into (objectui#9595). The precedent this wording came from is\n"
253+
+ "// recorded in the pin named below, beside the code that reads it.",
254+
"// The landed precedent is the rule-2\n"
255+
+ "// stale-entry message in scripts/check-spec-symbol-derivation.mjs; the same\n"
256+
+ "// shape still stands in scripts/check-type-check-coverage.mjs and\n"
257+
+ "// scripts/check-action-forward-parity.mjs, which this change deliberately does\n"
258+
+ "// not reach.",
259+
);
260+
expect(restored, 'the live wording this control rewrites is gone').not.toBe(source);
261+
return restored;
262+
});
263+
264+
expect(foreignGateCitations(region)).toEqual([
265+
'scripts/check-spec-symbol-derivation.mjs',
266+
'scripts/check-type-check-coverage.mjs',
267+
'scripts/check-action-forward-parity.mjs',
268+
]);
269+
// …and the pin that reads this banner is still not counted as foreign, so
270+
// the scan below cannot be satisfied by the banner simply losing its pin.
271+
expect(region).toContain('scripts/__tests__/check-lint-coverage-closing-keyword.test.ts');
272+
expect(foreignGateCitations(staleEntryRatchet())).not.toContain(
273+
'scripts/__tests__/check-lint-coverage-closing-keyword.test.ts',
274+
);
275+
});
276+
277+
it('…and the banner names no OTHER gate, so it cannot recite a tree that moves', () => {
278+
const region = staleEntryRatchet();
279+
// ⛔ The slice first: `staleEntryRatchet` only proves its two markers exist
280+
// in order, so a region collapsed to a few bytes would satisfy the scan
281+
// below by containing nothing at all. The banner's own last sentence is
282+
// inside it, or this leg is not reading the paragraph it claims to read.
283+
expect(region, 'the ratchet slice no longer reaches the banner it is scanning').toContain(
284+
'scripts/__tests__/check-lint-coverage-closing-keyword.test.ts',
285+
);
286+
expect(
287+
foreignGateCitations(region),
288+
"the rule-2 banner names another gate's source file again. Whatever it says about that "
289+
+ 'file is a reading taken once, by a person who is not looking at it, and re-derived '
290+
+ 'never — the sentence that stood there went stale without anyone touching this '
291+
+ 'file, which is objectui#9906. Say it at that gate, or in this pin, where the words '
292+
+ 'sit beside the thing they describe. ⛔ Not by adding a tree scan here: the ruling '
293+
+ 'of 2026-09-18 closed that door, and this assertion is deliberately the weaker one.',
294+
).toEqual([]);
295+
});
296+
162297
it('the ratchet SOURCE carries no trigger either, comment included', () => {
163298
// The cheap leg, and not a duplicate of the rendered one: it also covers the
164299
// prose beside the emitter, which reaches a reader who is deciding how to

‎scripts/check-lint-coverage.mjs‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,18 @@ for (const pkg of packages) {
113113
// was being careful. GitHub's parser does no sentence parsing, so hedging the
114114
// sentence around it buys nothing. Keep the instruction, lose the keyword, and
115115
// spell the anchor `objectui#` rather than a bare `#` so the reference cannot
116-
// match the closing grammar at all. The landed precedent is the rule-2
117-
// stale-entry message in scripts/check-spec-symbol-derivation.mjs; the same
118-
// shape still stands in scripts/check-type-check-coverage.mjs and
119-
// scripts/check-action-forward-parity.mjs, which this change deliberately does
120-
// not reach. Pinned by
116+
// match the closing grammar at all. This gate was reworded onto that shape
117+
// alone; the siblings the same repair would reach were reported rather than
118+
// widened into (objectui#9595). The precedent this wording came from is
119+
// recorded in the pin named below, beside the code that reads it. ⛔ Do not
120+
// re-answer that here, and ⛔ do not name which other gates carry the shape
121+
// today: a sentence in this banner about another gate's source is a reading
122+
// taken once, by someone who is not looking at that file, and re-derived
123+
// never. Such a sentence does not stay true; it only waits. The one that stood
124+
// here, and how it went stale, are on objectui#9906. Pinned by
121125
// scripts/__tests__/check-lint-coverage-closing-keyword.test.ts, which reads
122-
// this module's text rather than trusting this comment.
126+
// this module's text rather than trusting this comment — this paragraph
127+
// included, for the naming of any other gate.
123128
for (const name of Object.keys(DEBT)) {
124129
const pkg = byName.get(name);
125130
if (!pkg) {

0 commit comments

Comments
 (0)