Measured while enumerating the matching-rule-gate family for #11150 (PR #11918). Filed unassigned — recording, not claiming.
The measurement
scripts/check-route-envelope.mjs reconciles the route modules it discovers on disk against the MODULES table it declares, by exact path key:
constdeclared=MODULES[file];
Ablation on 1f6d04703: loosen that lookup to fall back to a basename match, so a discovered module can be credited to a declaration for a different path with the same file name.
production run, clean tree -> exit 0 (GREEN)
--self-test -> exit 0 (GREEN)
Mutation confirmed on disk before the readings (anchor count 1 to 0, injected marker 0 to 1); restored under a trap, git status clean afterwards.
Positive control, same harness, same gate, different mutation. Narrowing the discovery convention instead (entry.endsWith('-routes.ts') restricted so some modules stop being discovered):
production run, clean tree -> exit 1 (RED)
--self-test -> exit 0
So the harness can redden this gate. The double-green above is a property of the declared-vs-discovered rule, not of a broken probe.
Why it matters
On a clean tree every discovered module already has an exact declaration, so the fallback never fires and the verdict cannot move — that is the #11150 family shape. But the rule is not inert in the future: when a route module moves to another package keeping its file name, the loosened form silently credits the old declaration's counts to the new path, and both the NOT DECLARED finding and the declared in MODULES but not found finding disappear at once. That is exactly the class #10534's own census fell into (/admin/sso/register scored accounted-for on the strength of /admin/sso/register-saml), one level up: a correspondence with no right boundary.
Today that rule has no instrument at all. The gate's --self-test exercises scanSource, auditPluginRouteModule, auditDialectOnly and the dialect ratchets, but does not drive audit()'s declared-vs-discovered reconciliation against a fixture population in which a loose and a strict rule would disagree.
What would close it
A --self-test case that drives the reconciliation over a synthetic pair — one discovered path with no exact declaration, and a declaration for a same-named file at a different path — and asserts the finding is produced. That is the adversarial input the clean tree by construction does not supply. Strengthening only; the gate's live verdicts are correct today.
Not fixed in PR #11918 on purpose
That PR's scope is direction 1 of #11150 (require --self-test in CI for every member), and its ruling is strengthening-only with no scope for editing other gates' cases. #11918's gate does make the wiring of this self-test non-removable; it cannot make an absent case exist.
Repro
# in a worktree, on a clean tree
node scripts/check-route-envelope.mjs # exit 0
node scripts/check-route-envelope.mjs --self-test # exit 0
# then loosen `const declared = MODULES[file];` to a basename fallback and re-run both
Generated by Claude Code
Measured while enumerating the matching-rule-gate family for #11150 (PR #11918). Filed unassigned — recording, not claiming.
The measurement
scripts/check-route-envelope.mjsreconciles the route modules it discovers on disk against theMODULEStable it declares, by exact path key:Ablation on
1f6d04703: loosen that lookup to fall back to a basename match, so a discovered module can be credited to a declaration for a different path with the same file name.Mutation confirmed on disk before the readings (anchor count 1 to 0, injected marker 0 to 1); restored under a trap,
git statusclean afterwards.Positive control, same harness, same gate, different mutation. Narrowing the discovery convention instead (
entry.endsWith('-routes.ts')restricted so some modules stop being discovered):So the harness can redden this gate. The double-green above is a property of the declared-vs-discovered rule, not of a broken probe.
Why it matters
On a clean tree every discovered module already has an exact declaration, so the fallback never fires and the verdict cannot move — that is the #11150 family shape. But the rule is not inert in the future: when a route module moves to another package keeping its file name, the loosened form silently credits the old declaration's counts to the new path, and both the
NOT DECLAREDfinding and thedeclared in MODULES but not foundfinding disappear at once. That is exactly the class #10534's own census fell into (/admin/sso/registerscored accounted-for on the strength of/admin/sso/register-saml), one level up: a correspondence with no right boundary.Today that rule has no instrument at all. The gate's
--self-testexercisesscanSource,auditPluginRouteModule,auditDialectOnlyand the dialect ratchets, but does not driveaudit()'s declared-vs-discovered reconciliation against a fixture population in which a loose and a strict rule would disagree.What would close it
A
--self-testcase that drives the reconciliation over a synthetic pair — one discovered path with no exact declaration, and a declaration for a same-named file at a different path — and asserts the finding is produced. That is the adversarial input the clean tree by construction does not supply. Strengthening only; the gate's live verdicts are correct today.Not fixed in PR #11918 on purpose
That PR's scope is direction 1 of #11150 (require
--self-testin CI for every member), and its ruling is strengthening-only with no scope for editing other gates' cases. #11918's gate does make the wiring of this self-test non-removable; it cannot make an absent case exist.Repro
Generated by Claude Code