Context
Found while running a reviewer falsifier on PR #15314 (server config overlays inherit, #15294). Not that PR's defect — it neither introduced this hole nor claimed to close it, and its own split is provably clean. But the falsifier only exists because I happened to run it, and that is the problem.
The Problem
Nothing mechanically proves a config leaf survives a refactor. PR #15314 moved ~2,000 lines out of five config.template.mjs files into sibling configBase.mjs files. A leaf dropped in that move reads undefined at runtime, and none of the three guards would have noticed:
ai/scripts/lint/lint-config-template-ssot.mjs classifies template↔proxy paths. Grepped it: no parity check of any kind. It cannot see a leaf that vanished.
test/playwright/unit/ai/config.template.spec.mjs asserts ledger placement — that config ledgers live inside config classes. PR #15314's diff simply redirects that assertion from the templates to the new base files. It never counts leaves.
- CI green says nothing about a leaf whose only consumer has no test.
I verified PR #15314's split by hand — dev template leaf-count vs PR (base + thin template), per server:
| server |
dev |
PR (base+tpl) |
delta |
| github-workflow |
41 |
41 |
0 |
| gitlab-workflow |
7 |
7 |
0 |
| knowledge-base |
56 |
56 |
0 |
| memory-core |
106 |
106 |
0 |
| neural-link |
17 |
17 |
0 |
227 leaves, zero lost. That census is the evidence the PR deserved — and it exists only because a reviewer thought to run it at 23:40. The next base/template edit has no such guard.
Why this class is worth mechanizing rather than remembering: a dropped leaf is silent at every gate and loud only at runtime, in a peer's process, as undefined. That is the same shape as the drift class PR #15314 just eliminated — a config truth that no gate could see. ADR-0019 §D/E is explicit that the structural answer is the lint, not reviewer diligence, which is "empirically insufficient" (4/4 missed on #12420). A hand-run census is reviewer diligence wearing a table.
The Architectural Reality
ai/mcp/server/*/configBase.mjs — post-#15314, defaults and leaves live here.
ai/mcp/server/*/config.template.mjs — thin subclass claiming the runtime namespace.
ai/scripts/lint/lint-config-template-ssot.mjs — already parses both files and computes the union of declared paths for classification (its own JSDoc says so). It already has the data. It just never asserts anything about the set surviving.
- ADR-0019 §7.2 — the lint is the sanctioned home for the flaggable subset of §3.
The lint already reads what a parity check needs. This is closer to an assertion than a new capability.
The Fix
Give the leaf set a guard that outlives the reviewer who noticed:
- Add a parity assertion to the SSOT lint family: the declared leaf-path set of a
configBase.mjs + its thin config.template.mjs must be checkable against a committed expectation, so a leaf cannot silently leave the union.
- Fail loud with the missing path(s) — not a count. "106 → 105" tells nobody which leaf died.
- A deliberate removal updates the expectation in the same commit, which is the point: leaf removal becomes a reviewable act rather than an invisible one.
Explicitly rejected: a count-only check. A count is satisfied by deleting one leaf and adding another, which is exactly the refactor that would hide the bug.
Open question worth deciding rather than guessing: whether the expectation is a committed snapshot of declared paths, or a base↔template invariant checked structurally. The former catches removals across time; the latter catches only same-commit split errors. The removal-across-time case is the one that bit us, so the snapshot shape is likelier correct — but that is a design call for whoever takes this.
Acceptance Criteria
Out of Scope
- PR #15314 itself — approved, merge-eligible, split provably lossless.
- The overlay-drift class — #15294 removed it by construction (inheritance), which is the correct fix and not to be re-litigated with a detector.
- Broader ADR-0019 catalog lint coverage (#12456 sub #2).
Avoided Traps
- Blaming PR #15314. It didn't dig this hole; its own census is clean.
- Proposing a drift detector for the drift class. That class is already dead by construction. This ticket is about leaf loss, a different failure.
- Trusting a hand-run census. ADR-0019 §D/E settles that reviewer diligence is not the structural answer. If this stays a thing reviewers remember to do, it will be missed.
Related
- PR #15314 / #15294 — where this surfaced; its 227-leaf census is the baseline.
- ADR-0019 §3, §7.2, §D/E — the lint is the sanctioned home; diligence is empirically insufficient.
- #12456 / #12457 — the ADR + lint stack.
Live latest-open sweep: open issues checked 2026-07-16 ~23:42Z; searched config leaf parity lint — no equivalent found.
Retrieval Hint: config leaf parity lint configBase template split silent leaf loss 227 leaves
Origin Session ID: ad475320-6bdc-4555-ba3f-b78d51de0b17
Context
Found while running a reviewer falsifier on PR #15314 (server config overlays inherit, #15294). Not that PR's defect — it neither introduced this hole nor claimed to close it, and its own split is provably clean. But the falsifier only exists because I happened to run it, and that is the problem.
The Problem
Nothing mechanically proves a config leaf survives a refactor. PR #15314 moved ~2,000 lines out of five
config.template.mjsfiles into siblingconfigBase.mjsfiles. A leaf dropped in that move readsundefinedat runtime, and none of the three guards would have noticed:ai/scripts/lint/lint-config-template-ssot.mjsclassifies template↔proxy paths. Grepped it: no parity check of any kind. It cannot see a leaf that vanished.test/playwright/unit/ai/config.template.spec.mjsasserts ledger placement — that config ledgers live inside config classes. PR #15314's diff simply redirects that assertion from the templates to the new base files. It never counts leaves.I verified PR #15314's split by hand —
devtemplate leaf-count vs PR (base + thin template), per server:227 leaves, zero lost. That census is the evidence the PR deserved — and it exists only because a reviewer thought to run it at 23:40. The next base/template edit has no such guard.
Why this class is worth mechanizing rather than remembering: a dropped leaf is silent at every gate and loud only at runtime, in a peer's process, as
undefined. That is the same shape as the drift class PR #15314 just eliminated — a config truth that no gate could see. ADR-0019 §D/E is explicit that the structural answer is the lint, not reviewer diligence, which is "empirically insufficient" (4/4 missed on #12420). A hand-run census is reviewer diligence wearing a table.The Architectural Reality
ai/mcp/server/*/configBase.mjs— post-#15314, defaults and leaves live here.ai/mcp/server/*/config.template.mjs— thin subclass claiming the runtime namespace.ai/scripts/lint/lint-config-template-ssot.mjs— already parses both files and computes the union of declared paths for classification (its own JSDoc says so). It already has the data. It just never asserts anything about the set surviving.The lint already reads what a parity check needs. This is closer to an assertion than a new capability.
The Fix
Give the leaf set a guard that outlives the reviewer who noticed:
configBase.mjs+ its thinconfig.template.mjsmust be checkable against a committed expectation, so a leaf cannot silently leave the union.Explicitly rejected: a count-only check. A count is satisfied by deleting one leaf and adding another, which is exactly the refactor that would hide the bug.
Open question worth deciding rather than guessing: whether the expectation is a committed snapshot of declared paths, or a base↔template invariant checked structurally. The former catches removals across time; the latter catches only same-commit split errors. The removal-across-time case is the one that bit us, so the snapshot shape is likelier correct — but that is a design call for whoever takes this.
Acceptance Criteria
configBase.mjsfails the lint, and the failure names the exact missing path — verified by deleting one and observing the message.Out of Scope
Avoided Traps
Related
Live latest-open sweep: open issues checked 2026-07-16 ~23:42Z; searched
config leaf parity lint— no equivalent found.Retrieval Hint:
config leaf parity lint configBase template split silent leaf loss 227 leavesOrigin Session ID:
ad475320-6bdc-4555-ba3f-b78d51de0b17