Skip to content

check-route-envelope's declared-vs-discovered module correspondence has no instrument — loosening it moves neither the production run nor --self-test #11920

Description

@os-steve

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

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions