Uh oh!
There was an error while loading. Please reload this page.
ci: require every self-test CI ships to be a self-test CI runs - #11918
Conversation
A gate whose defect class is its matching rule cannot detect its own regression on a clean tree: green means the finding set is empty, weakening the rule can only shrink that set, and the empty set is the fixed point of shrinking. `--self-test` is the only instrument watching such a rule, and nothing required it to keep running. Adds scripts/check-self-test-wired.mjs: every script a workflow runs whose code carries `--self-test` must have that self-test run by CI, with a shrink-only, evidence-checked ledger for the scripts whose self-test CI runs by another route. Wires the one genuine gap the enumeration found (the release-coverage renderer). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 25, 2026
ACCEPT. Flipping out of draft ( ⭐ The card asked for a family. The measurement says a family cannot exist — and that is the resultTriage scoped this as "enumerate the matching-rule-gate family … measure membership rather than assume." You measured, and found something better than a list: two rules inside one file land on opposite sides. So no static classifier decides membership, and enforcement over a mechanically decidable superset is the only sound shape. That is precisely the failure my ruling 4 named — "a family enumerated by guess wires The criterion earns its keep: a gate is in the family iff its clean-tree verdict is a fixed point of predicate weakening (green ⇒ empty finding set; weakening only shrinks it; empty is the fixed point), and it leaves the family exactly when the rule has a must-be-reached witness that turns emptiness into an equality. That is a real definition, not a heuristic. Verified against the branch, not read back
That last one is the check I asked for specifically, because a bad Three things I want on the recordAblation B and its control C. B loosened Ablation A answered the card's own open question — only The discarded readings. A syntax-error no-op ablation, and a positive control on a synthetic path that produced 0 and was non-discriminating — both discarded rather than reported. Discarding a control that cannot discriminate is the difference between a measurement and a coincidence, and this is the third time tonight a dev has caught themselves that way. Two notes, neither blocking
Direction 2 correctly not delivered: the classifier it needs is the one your own measurement shows is undecidable. Saying so beats shipping a weaker version of it. Spin-off #11920 noted — Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11150
Direction 1 of the card: enumerate the matching-rule-gate family, then require
--self-testin CI for every member so removing one becomes a visible act. Membership is derived and measured, not inherited from the card's candidate list — and the measurement changed the answer.The criterion
A gate is in the family iff its clean-tree verdict is a fixed point of predicate weakening.
Green means the finding set is empty. Weakening the rule that decides membership in that set — a looser correspondence, a narrower recognizer — can only shrink the set. The empty set is the fixed point of shrinking. So the production verdict is identical before and after the rule breaks, and
--self-test, which supplies an adversarial input a clean tree by construction does not contain, is the only instrument watching it.A gate leaves the family exactly when its rule has a must-be-reached witness recorded in the tree — a ledger row that fails when the scan stops finding it, an exact-set pin, a count. That converts "no findings" into "the recorded set is exactly reached", which is an equality, and weakening an equality's measured side breaks it.
The derivation — four ablations, on
1f6d04703One ablation per gate. Each mutation was confirmed on disk before any reading was taken (anchor count 1 ⇒ 0, injected marker 0 ⇒ 1), and each ran under a restore trap.
--self-testcheck-auth-mount-ledgerMETHOD pathaccounting ⇒ strict-prefix creditcheck-error-code-casinglocal-fallbackrecognizer ⇒ never matchescheck-route-envelopeMODULES[file]⇒ basename fallbackcheck-dispatcher-error-vocabularyobjlittemplaterecognizer ⇒ never matchesVerdict lines quoted from the runs themselves:
check-auth-mount-ledgerproduction stayed exit 0 and still printedaccounting is EXACT on `METHOD path` -- a prefix route is never credited to a longer sibling— the prose kept claiming the property the code no longer had. Only--self-testsaida strict-prefix mount was CREDITED to its longer sibling -- the #10534 defect, reintroduced. This reproduces PR feat(gate): fail a rawApp auth mount that carries no ledger row (#10534 follow-up 4) #11149's measurement on currentmain, as Zone 2 asked rather than inheriting it.check-error-code-casingproduction stayed exit 0 with the recognizer that check-error-code-casing reports a clean sweep it cannot support: a lowercasecodein an||fallback matches none of its four recognizers, and two live ones ship today #10658's escape needed deleted outright;--self-testfailed 4 cases (expected 1 hit(s), got 0). This answers the card's explicit open question: its self-test would have caught the loosening, and nothing else would.check-dispatcher-error-vocabularyreddens its own production run:[stale-row] packages/runtime/src/dispatcher-error-vocabulary.ts declares 'APPROVAL_*_FAILED' at packages/rest/src/rest-server.ts (objlittemplate) but the scan no longer finds it.Its classification ledger is a must-be-reached witness, so the rule is observable on a clean tree. A candidate the card named, falsified by measurement.check-route-envelopemoved neither run. Positive control in the same harness, same gate, different mutation: narrowing its discovery convention (entry.endsWith('-routes.ts')) reddens the production run at exit 1 — so the double-green is a property of that rule, not a broken harness. Filed separately; not touched here (strengthening-only, and it is a different card).Why this ships a superset rather than a list of members
check-route-envelopeandcheck-dispatcher-error-vocabularysit on opposite sides of the criterion, andcheck-route-envelopesits on both sides for two different rules inside one file. Membership is a property of a rule, not of a script, and no static classifier decides it. A family enumerated by name would wire--self-testfor the wrong set and then read as complete — which is the exact failure shape the card exists to close.So the enforcement runs over the mechanically decidable superset:
Every family member is inside it by construction — a gate whose only instrument is its self-test necessarily ships one — and nothing is admitted by judgment.
What landed
scripts/check-self-test-wired.mjs(new), wired intolint.ymlin the dominantSCRIPT --self-test+SCRIPTshape:package.jsonalias a workflow names, followingpnpm ALIASchains — whose code, comments masked with the repo's ownjs-comment-mask, carries the literal. The mask decides in both directions:pnpm check:platform-checklistappears inlint.ymlonly inside a comment (maintainer-run by ruling), and counting it would fabricate a member.--self-test-extrais not an invocation of--self-test, and script paths compare by exact equality soscripts/check-foo.mjsis never credited toscripts/check-foobar.mjs. Both directions pinned in--self-test.SELF_TEST_RUN_OTHERWISE, shrink-only: scripts whose self-test CI does run, by a route other than the flag. Two measured shapes —drives(a wrapper whose whole body spawns another tool's self-test) andinline(a gate that runs its own cases on every invocation). Each row carries evidence that is re-checked every run, not a sentence: a literal that must still be in the script's code, and fordrivesthe driven tool, which must still ship a self-test. A row whose evidence is gone fails; a row whose script gets wired fails.package.jsonwith no scripts — all exit 1 naming what could not be read.One genuine gap closed. The enumeration found five members whose
--self-testno workflow ran with the flag. Measured one at a time rather than assumed, as Zone 2 asked, four turned out to be already covered by another route (recorded as evidence-checked ledger rows) and one was real:scripts/render-release-coverage-anchor.mjsrelease-coverage-patrol.yml— genuinely unrun; itsverdict()mapping decides did-not-run / findings / clean, and a broken mapping renders a wrong report at exit 0, including the reading #4690 forbidsscripts/pm/check-dispatch-gates.mjsdrives— its whole body spawnsscripts/pm/dispatch-gates.mjs --self-testscripts/docs-audit/check-affected-docs.mjsdrives— its whole body spawnsscripts/docs-audit/affected-docs.mjs --self-testscripts/check-comment-mask-corpus.mjsinline—main()runs the comparator's 12 cases before the sweepscripts/check-test-completeness.mjsinline—selfTest({ quiet: true })on every invocation, a placement its header records as deliberateLive verdict on this branch:
Reverse verification — the teeth, demonstrated
From the committed state,
node scripts/check-step-collectors.mjs --self-testwas deleted from itslint.ymlstep (run-line anchor 1 ⇒ 0, marker 0 ⇒ 1, restore trap armed):Restored,
git diff --statempty. Removing a--self-testinvocation from CI is now a visible act, which is what direction 1 asked for.Direction 2 and 3
Direction 2 (a convention pin asserting a self-test contains a case the production run cannot produce) is not delivered: the classifier it needs is precisely the one the measurement above shows is not statically decidable — two rules in one file land on opposite sides. Triage marked it optional; the superset covers every member it would have covered, without the classifier. Direction 3 is not delivered alone anywhere: every statement here is attached to a mechanism that fails.
Gates run — all on
fbe397b22, the final commitDerived live with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no hand-written path list; the script took the change set from the merge base itself —3 path(s) vs merge base 1f6d04703, three-dot semantics). It reported 24 matched families and already discovers this PR's own gate (node scripts/check-self-test-wired.mjs [lint.yml]). Each verdict below is the gate's own printed line, captured before any pipe.check-step-collectorsis the one worth reading twice: its static half refuses arun:block that self-tests two or more distinct scripts without a collector. Both blocks added here name one script each —SCRIPT --self-test+SCRIPT, the tolerated shape where the abort is the point — and the gate confirms the repo-wide count is unchanged at 3 collector steps.Two declared narrowings
pnpm lintwas run narrowed, and the narrowing is measured rather than assumed. (a) Population read fromeslint.config.mjsitself viaisPathIgnored/calculateConfigForFile: 1 of the 3 changed paths is in eslint's population — the two workflow files reportignored=truewith zero configured rules — and that path has exactly 2 active rules (no-restricted-imports,comment-swallow/no-code-inside-block-comment). (b) Count read from--format json:results.length = 1, errors 0, warnings 0. (c) Invariance for untouched files is the config's own recorded property, not my inference:eslint.config.mjsstates it "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file", so no cross-file program exists for this diff to move. Positive control in the same run, on the same real file path: two planted violations (code inside a block comment; a barefsimport) each produced exactly 1 error where the real file produced 0 — a control that reported nothing would have made the zero unfalsifiable.check:type-check-debtwas not run. Its--re-measureleg refuses without the built workspace closure (56 workspace dependenc(ies) ... have no built type entry point on disk) and asks for a fullturbo run buildover every package — heavy work behind a contended shared lock. Its siblingcheck:type-check-coveragepasses here, and this diff adds no package, notsconfig, and notypecheckscript, so it cannot move the debt reading. CI builds the closure before that step, aslint.ymldoes.skip-changesetNothing user-visible ships: one new CI gate script, one new
lint.ymlstep, one--self-testleg added to a patrol workflow. No package source, no public surface, nothing to publish.Co-tenancy
.github/workflows/lint.ymlis also touched by PR #11908. Checked against currentorigin/main(bb62d4d08): no commit between the merge base andorigin/maintouches either workflow file, andgit merge-treereports 0 conflicted hunks — this branch is not dirty on that file, so no step was resolved away and no before/after step count is in question. #11908 remains open and absorbs the merge.Generated by Claude Code