Repository navigation
Commit d6a8444
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
- .changeset
- scripts
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | | - | |
72 | | - | |
73 | | - | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
74 | 79 | | |
75 | 80 | | |
76 | 81 | | |
| |||
116 | 121 | | |
117 | 122 | | |
118 | 123 | | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
119 | 197 | | |
120 | 198 | | |
121 | 199 | | |
| |||
159 | 237 | | |
160 | 238 | | |
161 | 239 | | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
162 | 297 | | |
163 | 298 | | |
164 | 299 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
121 | 125 | | |
122 | | - | |
| 126 | + | |
| 127 | + | |
123 | 128 | | |
124 | 129 | | |
125 | 130 | | |
| |||
0 commit comments