Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/9906-lint-coverage-banner-cross-file-claim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
---

`scripts/check-lint-coverage.mjs`'s rule-2 ratchet banner no longer asserts, in
the present tense, which other gates carry the closing-keyword shape
(objectui#9906). It named two sibling gates and said the shape "still stands" in
both; both halves were true when the sentence was written and both had gone
false before anyone read it again, each falsified by a different pull request
and neither by the one that wrote the sentence. The trade-off the banner exists to explain —
spell the anchor `objectui#` rather than a bare `#`, so a quoted message cannot
match GitHub's closing grammar — is unchanged, and the scope record of the
repair that wrote it survives as history rather than as a reading of the tree.

`scripts/__tests__/check-lint-coverage-closing-keyword.test.ts` now also fails
if the banner names any other gate at all, with a control that restores the
removed sentence and proves the scan fires on it. Scripts and tests only; no
package is released by this change.
141 changes: 138 additions & 3 deletions scripts/__tests__/check-lint-coverage-closing-keyword.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,14 @@ const FIXTURE_ANCHOR = 424242;
const CLOSING_TRIGGER =
/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:[A-Za-z0-9-]+\/[A-Za-z0-9-]+|objectui|objectstack)?#(?:\$\{|\d)/i;

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

/**
* The `scripts/` files this banner is allowed to name: the gate the banner is
* written in, and the pin that reads it. Both travel WITH the sentence — a
* reader of either is looking at the other, and whoever moves one is the person
* the citation is in front of.
*
* Every other `scripts/` path is a different gate's source, and a sentence here
* about a different gate's source is a reading of a population that moves
* without this file and re-derives never. objectui#9906 is that shape landed:
* the banner said the closing-keyword spelling "still stands" in two named
* sibling gates. Measured with the `CLOSING_TRIGGER` below, over each named
* file as of each merge commit rather than inferred from pull-request titles:
* both halves were TRUE the day the sentence was written, and it went stale in
* two steps — the first about 35 hours later, the second the same day after
* that — each step a different pull request and ⛔ neither of them the one that
* wrote the sentence. Nothing could have noticed, because nothing read it.
*
* ⚠️ That last reading is itself dated, and it lives HERE and on the card
* rather than in the banner, on purpose. This file is what a person opens when
* they are deciding how to word that banner, and the card carries the commits
* the reading was taken at. A first draft of the repair put a version of it in
* the banner instead — and got it backwards, claiming a half was already false
* on the day of writing. ⭐ A dated reading asserted where nothing re-reads it
* was wrong within one review round, which is the argument for this whole rule
* arriving by being committed rather than by being made.
*
* ⛔ This is NOT a check that any such claim is TRUE — no tree is scanned here
* and none may be (the 2026-09-18 ruling closed the door on a new gate for
* cross-file claims). It is the weaker, cheaper, unrottable rule: the banner
* does not make the claim at all. If a future reader needs to know which gates
* carry the shape today, the answer is those gates and their own pins, not a
* sentence in this one.
*/
const CITABLE_IN_THE_BANNER = new Set([
'check-lint-coverage.mjs',
'check-lint-coverage-closing-keyword.test.ts',
'lint:coverage',
]);

/**
* The three spellings a banner reaches for when it names a gate: the gate's
* source file, a pin's file, and the `package.json` script that runs the gate.
* Directory prefixes are optional on purpose — dropping `scripts/` is the
* cheapest way to reword around a scan that demands it, and a bare
* `check-type-check-coverage.mjs` cites exactly as hard.
*
* ⚠️ What this deliberately does NOT reach: a gate named in prose with no
* filename and no script key ("the type-check coverage gate"). Closing that
* would mean a list of every gate's English name — a second copy of the gate
* roster, maintained by hand, which is the defect this pin exists to stop
* wearing a pin's clothes. The guarantee here is bounded and stated rather
* than assumed: the banner carries no CITATION of another gate.
*
* ⚠️ Nor does it reach outside the rule-2 region — the same boundary the
* `CLOSING_TRIGGER` source leg below already has. The gate's leading docblock
* names its sibling in the opening sentence, and that citation is a standing
* architectural relation rather than a reading of the sibling's contents, so it
* is left where it is; a rotting claim written THERE would be out of this
* scan's reach and is not covered by anything.
*/
const GATE_CITATION =
/(?:\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;

/** The same citation with any directory prefix dropped, so the two spellings compare equal. */
const basename = (citation: string) => citation.slice(citation.lastIndexOf('/') + 1);

/** The gates the ratchet region names that are neither this gate nor its pin. */
function foreignGateCitations(region: string): string[] {
return [...new Set(region.match(GATE_CITATION) ?? [])].filter(
(citation) => !CITABLE_IN_THE_BANNER.has(basename(citation)),
);
}

describe('the stale-entry message may not carry a card-closing keyword', () => {
it('the harness and the regex can both fire — the control, on the wording this card replaced', () => {
// ⛔ Without this leg every assertion below is unfalsifiable: a substitution
Expand Down Expand Up @@ -159,6 +237,63 @@ describe('the stale-entry message may not carry a card-closing keyword', () => {
expect(rendered).toContain('can be ended once that work is done');
});

it('the foreign-citation scan fires on the sentence this card removed — the control', () => {
// ⛔ Without this leg the assertion below is unfalsifiable: a regex that
// matches nothing and a banner that names nothing render identically.
//
// The pre-repair sentence is restored byte for byte, as objectui#9906
// measured it — including its two present-tense halves, which were false
// on the tree the day this control was written. Restoring it HERE plants no
// claim: it lives inside a string this file feeds to its own scan and never
// reaches the gate on disk.
const region = staleEntryRatchet((source) => {
const restored = source.replace(
"// alone; the siblings the same repair would reach were reported rather than\n"
+ "// widened into (objectui#9595). The precedent this wording came from is\n"
+ "// recorded in the pin named below, beside the code that reads it.",
"// The landed precedent is the rule-2\n"
+ "// stale-entry message in scripts/check-spec-symbol-derivation.mjs; the same\n"
+ "// shape still stands in scripts/check-type-check-coverage.mjs and\n"
+ "// scripts/check-action-forward-parity.mjs, which this change deliberately does\n"
+ "// not reach.",
);
expect(restored, 'the live wording this control rewrites is gone').not.toBe(source);
return restored;
});

expect(foreignGateCitations(region)).toEqual([
'scripts/check-spec-symbol-derivation.mjs',
'scripts/check-type-check-coverage.mjs',
'scripts/check-action-forward-parity.mjs',
]);
// …and the pin that reads this banner is still not counted as foreign, so
// the scan below cannot be satisfied by the banner simply losing its pin.
expect(region).toContain('scripts/__tests__/check-lint-coverage-closing-keyword.test.ts');
expect(foreignGateCitations(staleEntryRatchet())).not.toContain(
'scripts/__tests__/check-lint-coverage-closing-keyword.test.ts',
);
});

it('…and the banner names no OTHER gate, so it cannot recite a tree that moves', () => {
const region = staleEntryRatchet();
// ⛔ The slice first: `staleEntryRatchet` only proves its two markers exist
// in order, so a region collapsed to a few bytes would satisfy the scan
// below by containing nothing at all. The banner's own last sentence is
// inside it, or this leg is not reading the paragraph it claims to read.
expect(region, 'the ratchet slice no longer reaches the banner it is scanning').toContain(
'scripts/__tests__/check-lint-coverage-closing-keyword.test.ts',
);
expect(
foreignGateCitations(region),
"the rule-2 banner names another gate's source file again. Whatever it says about that "
+ 'file is a reading taken once, by a person who is not looking at it, and re-derived '
+ 'never — the sentence that stood there went stale without anyone touching this '
+ 'file, which is objectui#9906. Say it at that gate, or in this pin, where the words '
+ 'sit beside the thing they describe. ⛔ Not by adding a tree scan here: the ruling '
+ 'of 2026-09-18 closed that door, and this assertion is deliberately the weaker one.',
).toEqual([]);
});

it('the ratchet SOURCE carries no trigger either, comment included', () => {
// The cheap leg, and not a duplicate of the rendered one: it also covers the
// prose beside the emitter, which reaches a reader who is deciding how to
Expand Down
17 changes: 11 additions & 6 deletions scripts/check-lint-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,18 @@ for (const pkg of packages) {
// was being careful. GitHub's parser does no sentence parsing, so hedging the
// sentence around it buys nothing. Keep the instruction, lose the keyword, and
// spell the anchor `objectui#` rather than a bare `#` so the reference cannot
// match the closing grammar at all. The landed precedent is the rule-2
// stale-entry message in scripts/check-spec-symbol-derivation.mjs; the same
// shape still stands in scripts/check-type-check-coverage.mjs and
// scripts/check-action-forward-parity.mjs, which this change deliberately does
// not reach. Pinned by
// match the closing grammar at all. This gate was reworded onto that shape
// alone; the siblings the same repair would reach were reported rather than
// widened into (objectui#9595). The precedent this wording came from is
// recorded in the pin named below, beside the code that reads it. ⛔ Do not
// re-answer that here, and ⛔ do not name which other gates carry the shape
// today: a sentence in this banner about another gate's source is a reading
// taken once, by someone who is not looking at that file, and re-derived
// never. Such a sentence does not stay true; it only waits. The one that stood
// here, and how it went stale, are on objectui#9906. Pinned by
// scripts/__tests__/check-lint-coverage-closing-keyword.test.ts, which reads
// this module's text rather than trusting this comment.
// this module's text rather than trusting this comment — this paragraph
// included, for the naming of any other gate.
for (const name of Object.keys(DEBT)) {
const pkg = byName.get(name);
if (!pkg) {
Expand Down
Loading