diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b09e86486f..0ab076bdba 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3887,6 +3887,39 @@ jobs: - name: Stall-guard headroom self-test (self-test half only, never the measurement) run: pnpm check:stall-guard-headroom + # The #12981 swallow census's own controls (#13919, maintainer ruling of + # 2026-09-01). Same shape as the step above and for the same reason: a + # `measure-*` instrument whose SELF-TEST is the only thing CI runs, never a + # bare invocation. `measure-durability-swallow-family.mjs` declares four + # control families and, measured on `origin/main` when #13919 was filed, + # NOTHING ran any of them -- zero references to the script anywhere in + # `package.json` or `.github/**`, against a control that must return + # non-zero (`measure-stall-guard-headroom`, 2 hits) so the zero was a + # reading and not a broken query. Meanwhile the resolver those controls pin + # was repaired three times (#13459, #13474, PR #13915), each time with no + # gate holding the previous repair, and the census's numbers feed #12981's + # repair worklist -- so a wrong denominator propagates into that programme + # with nobody watching. + # + # ONLY THE `gated` FAMILIES, and that asymmetry is the ruling's boundary 1 + # rather than a convenience. `--self-test=gated` runs RESOLUTION + NEGATIVE + # + REGRESSION; it omits POSITIVE_CONTROLS, which pin members of the + # #12981 worklist that the repair programme exists to REMOVE. That is + # recorded, not forecast: batch 2 repaired the file the dark control then + # named and turned the self-test red for doing exactly what the ruling + # asked. ⛔ A gate that reddens when the programme succeeds is not a gate, + # and it trains the fleet to route around gates -- so ⛔ never "tidy" this + # step into a bare `--self-test`. The three wired families cannot be + # destroyed by a successful repair: a repair moves a member from tier + # `dark` to tier `channelled` and it stays a member. + # + # The census itself stays a MEASUREMENT: no membership count can redden + # this step, and the gated run prints the instrument's own "a MEASUREMENT, + # not a gate" banner. It reads `packages/**` source text through ts-parse; + # no network, no token, no build. + - name: Swallow-census control families (gated families only, never POSITIVE) + run: pnpm check:swallow-census-controls + # The dev-preflight gate's self-test — and deliberately only HALF of that # gate (#8170, the second and only other member of #8110's split-gate # family). Same neighbour as the step above for the same reason: both are diff --git a/package.json b/package.json index b2f7666356..81df05e49a 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "check:stall-guard": "node scripts/run-with-stall-guard.mjs --self-test", "check:stall-guard-budget": "node scripts/check-stall-guard-budget.mjs --self-test && node scripts/check-stall-guard-budget.mjs", "check:stall-guard-headroom": "node scripts/measure-stall-guard-headroom.mjs --self-test", + "check:swallow-census-controls": "node scripts/measure-durability-swallow-family.mjs --self-test=gated", "gen:system-context-census": "node scripts/check-system-context-census.mjs --fix", "check:system-context-census": "node scripts/check-system-context-census.mjs --self-test && node scripts/check-system-context-census.mjs" }, diff --git a/scripts/measure-durability-swallow-family.mjs b/scripts/measure-durability-swallow-family.mjs index e2a7fa5b8b..71f3ae1e54 100644 --- a/scripts/measure-durability-swallow-family.mjs +++ b/scripts/measure-durability-swallow-family.mjs @@ -7,14 +7,27 @@ // node scripts/measure-durability-swallow-family.mjs --sites # every site // node scripts/measure-durability-swallow-family.mjs --json # machine // node scripts/measure-durability-swallow-family.mjs --file

# one file -// node scripts/measure-durability-swallow-family.mjs --self-test # controls +// node scripts/measure-durability-swallow-family.mjs --self-test # all 4 control families +// node scripts/measure-durability-swallow-family.mjs --self-test=gated +// # the 3 families CI runs // -// It is NOT a gate: it is not wired into any workflow, it exits 0 on any -// membership count, and it is deliberately not named `check:*` or `gen:*` so -// the #4203 script ledger has nothing to classify (the shape -// `measure-partial-retirement-annotation.mjs` established). The only non-zero -// exit is `--self-test` failing its declared controls, and `ts-parse`'s -// EXIT_UNPARSEABLE. +// THE CENSUS is NOT a gate: it exits 0 on any membership count, it prints "a +// MEASUREMENT, not a gate" on every run of it, and the file is deliberately not +// named `check:*` or `gen:*` so the #4203 script ledger has nothing to classify +// (the shape `measure-partial-retirement-annotation.mjs` established). The only +// non-zero exits are a `--self-test` failing its declared controls, an unknown +// `--self-test=` mode (REFUSED, exit 2), and `ts-parse`'s EXIT_UNPARSEABLE. +// +// WHAT CI RUNS, since #13919 (maintainer ruling of 2026-09-01, verbatim 「同意」): +// the root alias `check:swallow-census-controls` runs `--self-test=gated` from +// `lint.yml` -- the same shape as the sanctioned precedent +// `check:stall-guard-headroom` -> `measure-stall-guard-headroom.mjs --self-test`, +// and the same asymmetry: the SELF-TEST leg only, never a bare invocation. What +// is gated is THIS INSTRUMENT'S OWN CONTROLS; no membership count can redden it, +// so the census stays a measurement. That ruling also amends #12981's sanctioned +// single-entry-point shape to "single entry point + a family flag" -- read +// SELF_TEST_MODES for the split's whole rationale and for why POSITIVE_CONTROLS +// is excluded permanently rather than pending. // // ## Why it exists // @@ -536,6 +549,70 @@ const RESOLUTION_CONTROLS = [ }, ]; +/** + * Which control families a `--self-test` run asserts (#13919). + * + * ## Why there is a subset at all + * + * All four families were green only while somebody remembered to run them by + * hand: measured on `origin/main`, nothing in `package.json` or `.github/**` + * named this script. The same resolver was then repaired three times (#13459, + * #13474, PR #13915) with no gate holding any of the previous repairs, and the + * census's numbers feed #12981's repair worklist, so a wrong denominator + * propagates into that programme unwatched. + * + * ## Why the subset is not simply "all of them" + * + * Wiring `all` into CI would redden the farm WHEN THE REPAIR PROGRAMME + * SUCCEEDS. `POSITIVE_CONTROLS` pins members, and its tier-1 DARK entry pins a + * member of the #12981 worklist -- precisely what that programme exists to + * remove. That is not a forecast: the `dark` control's own `why` above records + * it already happening once, when batch 2 repaired the file the control then + * named and turned this self-test red for doing exactly what the ruling asked. + * Repointing at another dark member only moves the breakage to the batch that + * repairs THAT file. ⛔ A gate that reddens on success is not a gate, and worse, + * it teaches the fleet to route around gates. + * + * So `gated` is the three families the repair programme CANNOT destroy, and the + * exclusion is PERMANENT rather than pending (#13919 ruling of 2026-09-01, + * boundary 1): repairs move a member from tier `dark` to tier `channelled` and + * it stays a member, so neither the negative controls, the regression controls, + * the resolution controls, nor the zero-member floor can be destroyed by a + * successful repair. + * + * ⛔ `gated` is NOT a lighter self-test to reach for by default. Anything that + * asserts membership at a declared TIER lives in `all`, and `all` is what a + * human or an agent touching this instrument runs. + * + * ⛔ This subset is also not, and must not be used to bring forward, the + * ruling's reserved handover step (`tryInsert`/`tryUpdate` into the real gate's + * `DURABILITY_CRITICAL_CALLEES`, still gated on `outstanding == 0`) -- see "The + * handover" above. + */ +const SELF_TEST_MODES = new Set(['all', 'gated']); + +/** The banner the instrument prints on every run that is not a bare `--file`/`--json` dump. */ +const MEASUREMENT_BANNER = 'durability swallow-family census (#12981) — a MEASUREMENT, not a gate'; + +/** + * Read the self-test mode out of argv. + * + * @returns `null` (not a self-test run), a member of `SELF_TEST_MODES`, or + * `{ unknown }` -- REFUSED rather than silently falling through to the + * census, because a typo'd mode reaching `report()` would exit 0 and + * read in a CI log exactly like a self-test that passed. + */ +function selfTestMode(argv) { + for (const arg of argv) { + if (arg === '--self-test') return 'all'; + if (arg.startsWith('--self-test=')) { + const mode = arg.slice('--self-test='.length); + return SELF_TEST_MODES.has(mode) ? mode : { unknown: mode }; + } + } + return null; +} + /* ------------------------------------------------------------------------- * * AST helpers * ------------------------------------------------------------------------- */ @@ -1186,7 +1263,7 @@ function report({ sites = false } = {}) { const carries = members.filter((m) => m.tier === 'carries-error'); const channelled = members.filter((m) => m.tier === 'channelled'); const out = []; - out.push('durability swallow-family census (#12981) — a MEASUREMENT, not a gate\n'); + out.push(`${MEASUREMENT_BANNER}\n`); out.push(` scanned ${stats.files} non-test source file(s) under packages/`); out.push(` try/catch statements ${stats.tryStatements}`); out.push(` ...guarding an awaited write ${stats.guardedWrites}`); @@ -1296,7 +1373,8 @@ function checkResolutionControl(control) { return { matched, wrong }; } -function selfTest() { +function selfTest(mode = 'all') { + const gated = mode === 'gated'; const problems = []; const { members } = census(); const byFile = new Map(); @@ -1304,7 +1382,7 @@ function selfTest() { if (!byFile.has(m.file)) byFile.set(m.file, []); byFile.get(m.file).push(m); } - for (const control of POSITIVE_CONTROLS) { + for (const control of gated ? [] : POSITIVE_CONTROLS) { const hits = byFile.get(control.file) ?? []; if (hits.length === 0) { problems.push(`positive control found NO member: ${control.file}\n ${control.why}`); @@ -1351,10 +1429,25 @@ function selfTest() { problems.push('census found ZERO members — on this tree that is a broken matcher, not a clean repo.'); } if (problems.length > 0) { - process.stderr.write(`x measure-durability-swallow-family self-test FAILED\n\n${ + process.stderr.write(`x measure-durability-swallow-family self-test${gated ? ' (gated families)' : ''} FAILED\n\n${ problems.map((p) => ` - ${p}`).join('\n\n')}\n\n`); return 1; } + if (gated) { + process.stdout.write( + `${MEASUREMENT_BANNER}\n` + + '✓ measure-durability-swallow-family self-test, gated families (#13919): ' + + `${NEGATIVE_CONTROLS.length} negative control(s) yield none, ` + + `${REGRESSION_CONTROLS.length} regression control(s) stay clear, ` + + `${RESOLUTION_CONTROLS.length} resolution control(s) resolve as declared, ` + + `${members.length} member site(s) total\n` + + ` ${POSITIVE_CONTROLS.length} positive control(s) are NOT asserted here, permanently: they pin ` + + 'members of the #12981 worklist, which\n that programme exists to remove — a control the repair ' + + 'is designed to destroy cannot hold a CI gate.\n Run `--self-test` for all four families; see ' + + '`SELF_TEST_MODES` for the whole rationale.\n', + ); + return 0; + } process.stdout.write( `✓ measure-durability-swallow-family self-test: ${POSITIVE_CONTROLS.length} positive control(s) ` + `yield members at their declared tier, ${NEGATIVE_CONTROLS.length} negative control(s) yield none, ` @@ -1366,7 +1459,19 @@ function selfTest() { } function main(argv) { - if (argv.includes('--self-test')) return selfTest(); + const mode = selfTestMode(argv); + if (mode !== null) { + if (typeof mode === 'object') { + process.stderr.write( + `x REFUSED: unknown self-test mode \`${mode.unknown}\`. Known modes: ` + + `${[...SELF_TEST_MODES].map((m) => `--self-test=${m}`).join(', ')} (bare \`--self-test\` means ` + + '`=all`).\n Refused rather than run, because falling through to the census would exit 0 and ' + + 'read in a CI log\n exactly like a self-test that passed.\n', + ); + return 2; + } + return selfTest(mode); + } const fileIdx = argv.indexOf('--file'); if (fileIdx !== -1) { const { members, quiet } = census({ only: argv[fileIdx + 1] }); diff --git a/scripts/pm/bare-root-worklist.mjs b/scripts/pm/bare-root-worklist.mjs index f287ac0dc4..05725498f8 100644 --- a/scripts/pm/bare-root-worklist.mjs +++ b/scripts/pm/bare-root-worklist.mjs @@ -485,6 +485,23 @@ const TRIAGE = new Map([ verdict: 'REFUSE-WIDE', why: 'non-test sources plus manifests, 1898 of 4903 (39%) — same trade', }], + ['check:swallow-census-controls SCAN_ROOT packages', { + verdict: 'REFUSE-WIDE', + why: 'FRESH on 2026-09-02, when #13919 wired the #12981 census own controls into CI for the ' + + 'first time; the constant is as old as the instrument and only the family is new. The ' + + 'census walk admits every non-test, non-declaration TypeScript source under the root at ' + + 'four extensions — 2193 of 5755 (38%), numerator from the instrument own printed scan ' + + 'count and denominator from git ls-files, both measured on this tree and NOT carried from ' + + 'the row above. Same class and nearly the same ratio as check:authz-resolver: the ' + + 'population is not a part of the root, it IS every source file in it, so a bare-root ' + + 'declaration would be TRUE and is refused for width alone — it would name this gate for ' + + 'every card in the repo that touches a package, and the coupling it would buy is already ' + + 'carried by CI, whose lint.yml job has no paths filter. ⚠️ What actually reddens the ' + + 'gated leg is narrower than the walk and is NOT a subtree either: ten named control files ' + + 'plus this instrument own source, and — through the zero-member floor alone — the corpus ' + + 'entire. A declaration naming the ten would be a hint the sweep judges FALSE of the walk, ' + + 'which is the costlier error this file prices by name', + }], ['check:engine-double-contract SCAN_ROOTS packages', { verdict: 'REFUSE-WIDE', why: 'tests AND sources, 4408 of 4903 (90%): the most nearly-true declaration on this list, ' diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index a1621fe4fc..5a1df1b5af 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -2316,6 +2316,7 @@ const COMPOUND_ANCHOR_LEDGER = [ ['scripts/check-self-test-workflow-commands.mjs', 'runSelfTest', true], ['scripts/check-step-collectors.mjs', 'selfTestTargets', true], ['scripts/check-step-collectors.mjs', 'selfTestDiscoveries', true], + ['scripts/measure-durability-swallow-family.mjs', 'selfTestMode', true], ['scripts/measure-self-test-floor.mjs', 'selfTestDefs', true], ['scripts/pm/dispatch-gates.mjs', 'selfTestOnlyCallables', true], ['scripts/pm/dispatch-gates.mjs', 'maskSelfTests', true], @@ -15986,7 +15987,18 @@ function selfTest() { // anyone to pass — which is the whole defect. Only a real run can tell them // apart, and only a real run can measure the two REMEDIES against each other. { - const seamCard = 'scripts/measure-durability-swallow-family.mjs'; + // The card has to be one NO convention KIND hits, so the only difference + // between the snippet's block harvest and `--commands` is the SPELLING + // split this pair measures; the strictly-better direction is driven + // separately, on `testCard` below, and conflating the two would leave + // neither measured. This was `measure-durability-swallow-family.mjs` until + // #13919 wired that instrument's controls as `check:swallow-census-controls` + // — which made it a GATE SCRIPT, so the "adds or edits a GATE SCRIPT" + // convention started hitting it and the two counts stopped agreeing for a + // reason that is not about spelling at all. ⛔ Do not repair a future + // recurrence by loosening the equality: repoint the card, and pick one that + // no check family runs. + const seamCard = 'scripts/measure-partial-retirement-annotation.mjs'; const humanRun = runCli([seamCard]); const humanOut = humanRun.stdout ?? ''; t('the seam card still derives at all', humanRun.status === 0 && humanOut.trim().length > 0);