Skip to content

Commit 0cb7a58

Browse files
committed
fix(pm): name the ratchet invocation CI runs in the dispatch-gates test-file entry
The convention entry for a change that adds a test file emitted check:type-check-coverage with a rationale that describes the TEST_DEBT ratchet — a half only check:type-check-debt (--re-measure) runs. A dev seat that ran the named command in good faith reported green while the ratchet had never run locally; CI then found four new type errors. Both invocations are now emitted, each stating the question it answers, and the ratchet line carries its built-closure prerequisite inline with the exact build command lint.yml runs before that step — an unexplained throw on a cold worktree reads as "not applicable to me". Fixes #8545 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018WuTtyckQa1VcXwgd52JpN
1 parent 8123eb7 commit 0cb7a58

1 file changed

Lines changed: 53 additions & 5 deletions

File tree

scripts/pm/dispatch-gates.mjs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,30 @@ export function i18nBundlePackageDirs() {
719719
* CI does not silently stop being suggested — the run prints it as STALE and
720720
* says to fix this table. A hand-written list that reports its own rot is a
721721
* different object from one that quietly ages.
722+
* - Every `name` here is an INVOCATION, not a script. One check script can be
723+
* wired into CI under two package scripts that answer different questions, and
724+
* a rationale that names the script instead of the invocation sends a seat to
725+
* a command which cannot reproduce the failure it describes.
726+
* `check:type-check-coverage` and `check:type-check-debt` are one file
727+
* (`scripts/check-type-check-coverage.mjs`); only the second passes
728+
* `--re-measure`, which is the half a new test file's type errors move. This
729+
* entry named the first while explaining the second, so a dev seat ran it in
730+
* good faith, reported the union green, and CI found four new type errors.
731+
* Swept over this tree when that was fixed: of the 73 families the workflows
732+
* discover, 8 resolve to a script some other family also resolves to — and 7
733+
* of those 8 are the other shape, a `check:` script beside a direct
734+
* `node scripts/check-x.mjs` step in a second workflow, which `derive`
735+
* discovers as its own family and prints with its own runnable invocation.
736+
* The pair below is the only one where two ROOT SCRIPTS differ by a flag, so
737+
* this is a one-off today and the rule is what generalises, not the fix.
738+
* - Prose in a `why` is a MODULE-BODY string, so it is scanned for watch hints
739+
* like any other literal — comment masking cannot reach it. The ratchet
740+
* entry's remedy command therefore spells its `--filter` values unquoted (and
741+
* says to quote them for the shell): measured, the shell-quoted spelling adds
742+
* both of its glob filter values to THIS file's own hint set as hints, inert
743+
* only because `hintCovers` rejects one that collapses to a bare top-level
744+
* directory. A gate list that fabricates hints out of its own explanations is
745+
* the failure this whole script is written against.
722746
* - Each entry is deletable, with a stated criterion:
723747
* - test-file entry: when a gate on it grows a discoverable path literal,
724748
* the ordinary derivation names it and its line becomes redundant.
@@ -741,7 +765,11 @@ export const CHANGE_KIND_GATES = [
741765
},
742766
{
743767
name: 'check:type-check-coverage',
744-
why: "TEST_DEBT ratchets a package's test-layer type errors, so a new test file that does not typecheck cleanly moves it",
768+
why: "the STRUCTURAL half: a package whose test files sit outside every tsc program accounting for it must carry a TEST_DEBT entry, so a new test file no tsconfig reaches moves this one. It re-measures no count — the ratchet is the invocation below",
769+
},
770+
{
771+
name: 'check:type-check-debt',
772+
why: "the RATCHET half, and the invocation CI runs for it: `--re-measure` re-runs tsc per ledger entry and fails when a count drifts up, so a new test file that does not typecheck cleanly moves it. Needs the workspace closure BUILT — on an unbuilt worktree it refuses outright, and that throw means NOT MEASURED, never `not applicable to me`. Build first, exactly as lint.yml does: pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/* (quote the filter values for your shell)",
745773
},
746774
],
747775
},
@@ -1077,8 +1105,8 @@ function selfTest() {
10771105

10781106
const resolved = (name) => `pnpm ${name}`;
10791107
const kindHit = changeKindLines(['packages/objectql/src/engine.test.ts'], resolved);
1080-
t('a test path emits the convention section', kindHit.length === 3 && kindHit[0].includes('adds or edits a test file'));
1081-
// Both halves anchor on the rendered DELIMITERS (`- pnpm x —`), for the
1108+
t('a test path emits the convention section', kindHit.length === 4 && kindHit[0].includes('adds or edits a test file'));
1109+
// All three halves anchor on the rendered DELIMITERS (`- pnpm x —`), for the
10821110
// reason the i18n entry's pins below state at length: a bare `includes` is
10831111
// satisfied by every name that merely STARTS WITH the expected one, so a
10841112
// prefix-preserving rename is invisible to it — the single rot class the STALE
@@ -1087,7 +1115,20 @@ function selfTest() {
10871115
// `check:type-check-coverage-v2` in CHANGE_KIND_GATES left the substring form
10881116
// green at 61/61 while the live run printed both as STALE; anchored, the same
10891117
// rename fails this case. The two conventions in this file now agree.
1090-
t('the section names both convention gates, runnably', kindHit.some((l) => l.includes('- pnpm check:query-options-erasure —')) && kindHit.some((l) => l.includes('- pnpm check:type-check-coverage —')));
1118+
//
1119+
// The coverage/debt PAIR is pinned as a pair on purpose (#8545): they are two
1120+
// invocations of one script, and the anchored form is what tells them apart —
1121+
// `includes('pnpm check:type-check-coverage')` is satisfied by the debt line's
1122+
// absence AND by a `-v2` rename, which is how a rationale describing the
1123+
// ratchet went on naming the invocation that never runs it.
1124+
t('the section names all three convention gates, runnably', kindHit.some((l) => l.includes('- pnpm check:query-options-erasure —')) && kindHit.some((l) => l.includes('- pnpm check:type-check-coverage —')) && kindHit.some((l) => l.includes('- pnpm check:type-check-debt —')));
1125+
// The ratchet line's prerequisite is part of the product, not decoration: a
1126+
// seat that runs `--re-measure` on an unbuilt worktree gets a throw, and an
1127+
// unexplained throw reads as "not applicable to me" — which is a green report
1128+
// over a gate that never ran. So the printed line must carry both the
1129+
// condition and a command that satisfies it.
1130+
const debtLine = kindHit.find((l) => l.includes('- pnpm check:type-check-debt —')) ?? '';
1131+
t('the ratchet line states its built-closure prerequisite', /closure BUILT|BUILT closure/.test(debtLine) && debtLine.includes('turbo run build'));
10911132
t('a non-test path emits nothing', changeKindLines(['scripts/pm/dispatch-gates.mjs'], resolved).length === 0);
10921133

10931134
// i18n change-kind derivation — the pure judgments first, each mirroring one
@@ -1287,7 +1328,14 @@ function selfTest() {
12871328
// The table's own rot detector: a name no live run discovers must say so,
12881329
// never disappear quietly.
12891330
const stale = changeKindLines(['a.test.ts'], () => null);
1290-
t('an undiscoverable gate renders as STALE', stale.filter((l) => l.includes('STALE')).length === 2);
1331+
t('an undiscoverable gate renders as STALE', stale.filter((l) => l.includes('STALE')).length === 3);
1332+
// Per NAME, anchored on both sides of the rendered name (`⚠ x: STALE`), so the
1333+
// pair that shares one script is reported apart: a count alone stays green if
1334+
// one of the two is dropped from the table and something else is added, and a
1335+
// leading substring stays green through a `-v2` rename — the two ways this
1336+
// table has actually rotted.
1337+
t('the coverage half renders STALE under its own name', stale.some((l) => l.includes('⚠ check:type-check-coverage: STALE')));
1338+
t('the ratchet half renders STALE under its own name', stale.some((l) => l.includes('⚠ check:type-check-debt: STALE')));
12911339
const i18nStale = changeKindLines(['packages/services/service-messaging/scripts/i18n-extract.config.ts'], () => null);
12921340
t('an undiscoverable check:i18n renders as STALE', i18nStale.filter((l) => l.includes('⚠ check:i18n: STALE')).length === 1);
12931341
t('every declared convention gate carries a reason', CHANGE_KIND_GATES.every((k) => k.gates.every((g) => g.name && g.why)));

0 commit comments

Comments
 (0)