Skip to content

Commit c91e19a

Browse files
Jack Qclaude
andauthored
fix(devx): mark the ledger-expanding remedy maintainer-only in check-test-typecheck (#8538) (#8549)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5c04b2a commit c91e19a

1 file changed

Lines changed: 193 additions & 3 deletions

File tree

scripts/check-test-typecheck.mts

Lines changed: 193 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@
4848
// Growing the ledger is possible (add the file and its count) but it is a
4949
// visible line in this repo's diff and needs the same justification any DEBT
5050
// entry needs — the idiom of `scripts/check-type-check-coverage.mjs`, applied
51-
// per file rather than per package.
51+
// per file rather than per package. That authority rule is stated to the AUTHOR
52+
// too, not only here: the unledgered-file message marks the ledger-expanding
53+
// path `⛔ MAINTAINER-ONLY` per the #8435 convention, and the self-test holds
54+
// the marker in place. See the convention block below `LEDGER_COMMENT`.
5255
//
5356
// Usage (`--package` is repo-relative and required for everything but
5457
// `--self-test`, which judges the ledger semantics alone):
@@ -111,6 +114,61 @@ const LEDGER_COMMENT =
111114
'its entry is deleted, and a file NOT listed here may have no errors at all. Regenerate with: ' +
112115
UPDATE_COMMAND;
113116

117+
// ── The ratchet-remedy authority convention (#8435) ─────────────────────────
118+
//
119+
// The unledgered-file verdict's second remedy is "add the file to the ledger",
120+
// which EXPANDS a shrink-only ratchet. The rule saying that is a maintainer's
121+
// call was written only in this file's prose — where a maintainer reading the
122+
// script sees it and the author who trips the gate never does — while the
123+
// message itself offered the path as the plain second of two things to do.
124+
// The convention landed for check-engine-double-contract.mjs and
125+
// check-type-check-coverage.mjs; the twin blocks there are the reference.
126+
//
127+
// The words in the marked message are lifted from this file's own two
128+
// statements of the rule — `LEDGER_COMMENT`'s "EXACT ratchet" and the GREW
129+
// verdict's "the ledger only ratchets down" — rather than invented: one rule
130+
// stated twice in two voices is two rules by the next reading.
131+
//
132+
// Deliberately NOT extended to this file's other two ledger-naming verdicts.
133+
// SHRANK tells the author to re-record a number that already fell, and
134+
// GRADUATED tells them to DELETE an entry; both are the ratchet TIGHTENING and
135+
// squarely the author's job. Note GRADUATED names the ledger file too, so the
136+
// detector below is keyed on the EXPANDING phrasing ("add the file to …") and
137+
// not on the ledger's name — a detector that caught GRADUATED would stamp
138+
// maintainer-only onto the improvement path and teach the opposite of the rule.
139+
//
140+
// ⛔ This STRENGTHENS ratchet governance and weakens nothing. No verdict moves,
141+
// no ledger entry is added, and the problems `evaluate()` reports are the same
142+
// set on the same inputs — only the diagnostic text of one of them changes.
143+
144+
/** Kept identical to the other gates' token so the convention is greppable. */
145+
const RATCHET_AUTHORITY_MARKER = '⛔ MAINTAINER-ONLY';
146+
147+
/**
148+
* How this gate OFFERS the privileged path, as a detector rather than a string
149+
* compare, so the self-test can prove it still reaches its subject: a reworded
150+
* offer that stopped matching would make the convention check pass vacuously on
151+
* every message.
152+
*
153+
* The optional `(?:\S*\/)?` accepts the ledger named by PATH as well as by bare
154+
* filename — a gap spelled to exclude the dot would silently stop matching the
155+
* moment the message qualified the name with a directory, and the self-test
156+
* carries a path-spelled control precisely because that failure is invisible.
157+
*/
158+
const RATCHET_EXPANSION_OFFER = new RegExp(
159+
`add the file to\\s+(?:\\S*\\/)?${LEDGER_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
160+
);
161+
162+
/**
163+
* The convention: a message that hands the author the ledger-expanding path
164+
* must say in the same breath that the path is not theirs. A message offering no
165+
* such path is unaffected — this is an authority label, not a vocabulary ban.
166+
*/
167+
export function ratchetRemedyCarriesAuthority(message: string): boolean {
168+
if (!RATCHET_EXPANSION_OFFER.test(message)) return true;
169+
return message.includes(RATCHET_AUTHORITY_MARKER);
170+
}
171+
114172
type Ledger = { _comment: string; entries: Record<string, number> };
115173

116174
/** `path(line,col): error TSxxxx: …` — continuation lines of a multi-line message never match. */
@@ -144,7 +202,11 @@ export function evaluate(actual: Map<string, number>, ledger: Record<string, num
144202
problems.push(
145203
`${file}: ${count} type error(s) in a file the ledger does not cover. Fix them — this file is ` +
146204
`inside the checked zone, which is the point of ${PROJECT}. (A deleted \`@ts-expect-error\` ` +
147-
`shows up exactly here, as TS2578/TS2694.) Only with a reason: add the file to ${LEDGER_NAME}.`,
205+
`shows up exactly here, as TS2578/TS2694.) That is the fix, and the only one of the two you ` +
206+
`can take on your own. ${RATCHET_AUTHORITY_MARKER}, NOT a co-equal option: add the file to ` +
207+
`${LEDGER_NAME}. This is an EXACT ratchet and the ledger only ratchets down, so a new entry ` +
208+
`EXPANDS it — that needs a maintainer to agree the debt is legitimate first (${ISSUE}). Do ` +
209+
`not take this path to get CI green.`,
148210
);
149211
continue;
150212
}
@@ -301,12 +363,140 @@ function selfTest(): void {
301363
failures.push(`parseDiagnostics mis-read a multi-line transcript: ${JSON.stringify([...parsed])}`);
302364
}
303365

366+
// ── The ratchet-remedy authority convention (#8435) ────────────────────────
367+
//
368+
// Asserted against the text `evaluate()` REALLY emits rather than a copy of
369+
// it: a hand-copied fixture would prove the convention about a string no
370+
// author ever reads. The assertions are deliberately non-overlapping, so each
371+
// way this can rot is caught by exactly one NAMED failure:
372+
//
373+
// (1) the detector still reaches its subject — the only one that fails if
374+
// the offer is reworded out from under RATCHET_EXPANSION_OFFER, which
375+
// would make (3) pass vacuously forever after;
376+
// (2) the real emitted verdict carries the marker — the only one that fails
377+
// if the label is dropped from the unledgered-file message;
378+
// (3) an offer WITHOUT the marker is REJECTED — the only one that fails if
379+
// the predicate stops discriminating (e.g. is reduced to `return true`);
380+
// (4) an offer naming the ledger by PATH is still matched — the only one
381+
// that fails if the detector's gap is narrowed to exclude directories;
382+
// (5)/(6)/(7) the detector does NOT reach the SHRANK, GRADUATED and GREW
383+
// verdicts. Those are the ratchet TIGHTENING, squarely the author's
384+
// job. (6) is the one that earns its keep: GRADUATED names the ledger
385+
// FILE while telling the author to delete an entry, so a detector keyed
386+
// on the name rather than on the act would over-reach onto it and stamp
387+
// maintainer-only on the improvement path.
388+
//
389+
// (3) is what makes (2) worth having: without it, a predicate that approved
390+
// everything would keep this block green while the convention is gone.
391+
const expect = (label: string, cond: boolean): void => {
392+
if (!cond) failures.push(label);
393+
};
394+
395+
const unledgered = evaluate(new Map([['pin.test.ts', 1]]), {})[0] ?? '';
396+
expect(
397+
'#8435 — the ratchet-offer DETECTOR still matches the unledgered-file verdict (else every '
398+
+ 'assertion below it passes vacuously)',
399+
RATCHET_EXPANSION_OFFER.test(unledgered),
400+
);
401+
expect(
402+
`#8435 — the unledgered-file verdict marks the ledger path ${RATCHET_AUTHORITY_MARKER} (the ledger `
403+
+ 'is an EXACT, shrink-only ratchet, so ADDING a file to it is a maintainer action — the author '
404+
+ "must be told that where they read it, not only in this file's prose)",
405+
ratchetRemedyCarriesAuthority(unledgered),
406+
);
407+
408+
{
409+
// SYNTHETIC — and specifically the pre-#8538 wording — rather than the real
410+
// verdict with the marker stripped out: derived, it would also fire on a
411+
// rewording, giving two named failures for one rot with the second
412+
// misdescribing the cause. if/else, not two flat asserts, so exactly one of
413+
// the two below can fire.
414+
const unmarkedOffer = `pin.test.ts: 1 type error(s) … Only with a reason: add the file to ${LEDGER_NAME}.`;
415+
if (!RATCHET_EXPANSION_OFFER.test(unmarkedOffer)) {
416+
expect(
417+
'#8435 — the synthetic unmarked-offer fixture is no longer recognised as an offer, so it '
418+
+ 'cannot test discrimination at all. Re-spell it to match RATCHET_EXPANSION_OFFER',
419+
false,
420+
);
421+
} else {
422+
expect(
423+
'#8435 — ratchetRemedyCarriesAuthority() REJECTS an offer carrying no marker (proves the '
424+
+ 'predicate discriminates rather than approving everything)',
425+
!ratchetRemedyCarriesAuthority(unmarkedOffer),
426+
);
427+
}
428+
}
429+
430+
// The hand-classified control for the detector's GAP. A sibling gate's
431+
// first-cut regex spelled its gap to exclude the dot, so every offer naming
432+
// its registry BY PATH silently stopped matching — a vacuous pass that no
433+
// green run can reveal, because the fail path is where these strings live.
434+
const pathSpelledOffer = `pin.test.ts: 1 type error(s) … add the file to packages/spec/${LEDGER_NAME}.`;
435+
expect(
436+
'#8435 — the detector still matches an offer that names the ledger by PATH rather than by bare '
437+
+ 'filename (a gap narrowed to exclude directories would make the convention unenforceable the '
438+
+ 'moment the message qualified the name)',
439+
RATCHET_EXPANSION_OFFER.test(pathSpelledOffer),
440+
);
441+
442+
const shrank = evaluate(new Map([['a.test.ts', 2]]), { 'a.test.ts': 3 })[0] ?? '';
443+
if (!shrank.includes('SHRANK')) {
444+
expect(
445+
'#8435 — the ratchet-DOWN control is no longer the SHRANK verdict, so it cannot prove the '
446+
+ 'detector leaves the improvement path alone. Re-point it at the re-record message',
447+
false,
448+
);
449+
} else {
450+
expect(
451+
'#8435 — the detector does NOT reach the SHRANK verdict (re-recording a number that already fell '
452+
+ "is the ratchet tightening and squarely the author's job; a maintainer-only marker there "
453+
+ 'would teach the opposite of the rule)',
454+
!RATCHET_EXPANSION_OFFER.test(shrank) && ratchetRemedyCarriesAuthority(shrank),
455+
);
456+
}
457+
458+
const graduated = evaluate(new Map<string, number>(), { 'a.test.ts': 3 })[0] ?? '';
459+
if (!graduated.includes(LEDGER_NAME)) {
460+
expect(
461+
`#8435 — the GRADUATED control no longer names ${LEDGER_NAME}, so it cannot prove the detector is `
462+
+ "keyed on the ACT rather than on the ledger's name. Re-point it at the delete-the-entry message",
463+
false,
464+
);
465+
} else {
466+
expect(
467+
'#8435 — the detector does NOT reach the GRADUATED verdict, which names the ledger file while '
468+
+ `telling the author to DELETE an entry. This is the over-reach control: a detector keyed on `
469+
+ `${LEDGER_NAME} instead of on the "add the file to …" ACT would mark the ratchet-tightening `
470+
+ 'path maintainer-only',
471+
!RATCHET_EXPANSION_OFFER.test(graduated) && ratchetRemedyCarriesAuthority(graduated),
472+
);
473+
}
474+
475+
const grew = evaluate(new Map([['a.test.ts', 4]]), { 'a.test.ts': 3 })[0] ?? '';
476+
if (!grew.includes('GREW')) {
477+
expect(
478+
'#8435 — the GREW control is no longer the debt-grew verdict, so it cannot prove the detector '
479+
+ 'leaves it alone. Re-point it at the grew message',
480+
false,
481+
);
482+
} else {
483+
expect(
484+
'#8435 — the detector does NOT reach the GREW verdict (it offers no ledger-expanding path at all; '
485+
+ 'it tells the author to fix the new errors)',
486+
!RATCHET_EXPANSION_OFFER.test(grew) && ratchetRemedyCarriesAuthority(grew),
487+
);
488+
}
489+
304490
if (failures.length) {
305491
console.error(`✗ check:test-typecheck --self-test — ${failures.length} failure(s)\n`);
306492
for (const f of failures) console.error(' • ' + f);
307493
process.exit(1);
308494
}
309-
console.log(`✓ check:test-typecheck --self-test — ${cases.length} semantic case(s) + the parser hold.`);
495+
console.log(
496+
`✓ check:test-typecheck --self-test — ${cases.length} semantic case(s), the parser, and the #8435 `
497+
+ 'convention hold (the unledgered-file verdict keeps its ledger offer marked maintainer-only, '
498+
+ 'and the SHRANK / GRADUATED / GREW verdicts stay unmarked).',
499+
);
310500
}
311501

312502
if (process.argv.includes('--self-test')) {

0 commit comments

Comments
 (0)