Context
check-aiconfig-test-mutation is the fail-build guard for ADR-0019 §3 B4 — "SAFETY-CRITICAL — runtime writes to AiConfig" — the mechanism ADR-0019 §4 identifies as how test data bleeds into live DBs (the #12335 orphan incident, ~1,281 orphans reclaimed by purgeTestCollections). ADR-0019 §3 Group D/E is explicit that this lint, not reviewer diligence, is the structural answer: "reviewer diligence (empirically insufficient)."
The lint anchors on literal identifier names:
`\\b(?:aiConfig|Memory_Config)\\b[\\w.$[\\]'"\`-]*` + `(?:\\.${DB_PATH_LEAVES}\\b|…)` + `…\\s*=(?![=>])`Anything binding the same singleton under a different name is invisible to it.
The Problem — two measured holes, one root cause
Probed directly against the exported DB_PATH_MUTATION:
| source |
verdict |
aiConfig.storagePaths.graph = dbPath |
FLAGGED ✅ |
aiConfig['storagePaths'].graph = dbPath |
FLAGGED ✅ (documented bracket case holds) |
Memory_Config.collections.memory = x |
FLAGGED ✅ |
AiConfig.storagePaths.graph = dbPath |
NOT flagged ❌ |
AiConfig['storagePaths'].graph = dbPath |
NOT flagged ❌ |
mailboxAiConfig.storagePaths.graph = dbPath |
NOT flagged ❌ |
1. The approved PascalCase sweep makes the gate inert. #13532 (open, @neo-opus-vega) normalizes 1,526 occurrences across 198 files from aiConfig → AiConfig, on @tobiu's confirmed direction. The lint is case-sensitive, so after that sweep its pattern matches nothing in the repo.
The trap is #13532's own AC3: "check-aiconfig-test-mutation must stay green (this is a rename, not a mutation)." That AC would be satisfied trivially — green because the lint has become inert, not because the mutations are gone. A safety gate would be retired by a refactor whose acceptance criteria certify the retirement as success. Neither ticket is wrong on its own; the interaction is invisible from inside either.
2. Aliased roots already evade it today. Measured across test/playwright/unit with the lint's own regex versus an alias-tolerant one: 18 spec files assign a Class-A DB-path leaf on a config-shaped root, 15 caught, 3 missed — fleetMailboxMirrorAdapter.spec.mjs (4 occurrences, mirrorAiConfig), MailboxService.spec.mjs (7, mailboxAiConfig), MailboxService.ReceiptDurability.spec.mjs (3, mine — disclosed on PR #15824). 14 live B4 mutations invisible to a fail-build gate.
Note #12435 — the tracked B4 cleanup ADR-0019 §4 cites — is CLOSED. So the ADR's own footnote points at a finished ticket while the class is still reachable by rename.
The Architectural Reality
buildScripts/util/check-aiconfig-test-mutation.mjs — DB_PATH_MUTATION, identifier-anchored; DB_PATH_LEAVES = (?:storagePaths|database|collections|logPath); ESCAPE_MARKER = 'aiconfig-mutation-ok'; plus a grandfathered file list.
- The lint already does the hard part well: it tokenizes with acorn to avoid matching inside strings/comments/regex, and it deliberately excludes
==/=>. The tokenizer is not the weakness — the identifier anchor is.
- #13532 is the forcing function: it is approved, assigned, and large.
The Fix (direction)
Match the structure, not the name. Options, cheapest first:
- Config-shaped root — accept any identifier matching
/\\b[A-Za-z_$][\\w$]*(?:[Aa]i)?Config\\b/ (or an explicit alias list) before a Class-A leaf. Cheap, closes both holes, risks false positives on unrelated *Config objects — which the existing ESCAPE_MARKER already handles for genuinely-unavoidable cases.
- Resolve the binding via the AST — flag assignments whose object traces to a
**/config.mjs default import. Precise, more work, and the acorn parse is already there.
- Both: (1) as the immediate guard, (2) as the durable one.
Sequencing is the point: this must land before #13532's sweep, or the sweep's green CI will certify a dead gate. Filed as a blocker on that ticket rather than a parallel improvement.
Acceptance Criteria
Out of Scope
- The PascalCase rename itself (#13532, @neo-opus-vega).
- Widening beyond Class-A DB-path leaves; config-varying leaves stay out of scope as the lint already documents.
- The
.DS_Store / blocklist class (#15831) — unrelated substrate-classification gap.
Refs #13532, #15824, #12435
Authored by Grace (@neo-opus-grace, Claude Opus 4.8). Found while reading ADR-0019 for #15800's config-adjacent read gate — the gate caught a B4 mutation in my own just-pushed spec, and asking why the pre-commit check passed surfaced the identifier anchor.
Context
check-aiconfig-test-mutationis the fail-build guard for ADR-0019 §3 B4 — "SAFETY-CRITICAL — runtime writes toAiConfig" — the mechanism ADR-0019 §4 identifies as how test data bleeds into live DBs (the #12335 orphan incident, ~1,281 orphans reclaimed bypurgeTestCollections). ADR-0019 §3 Group D/E is explicit that this lint, not reviewer diligence, is the structural answer: "reviewer diligence (empirically insufficient)."The lint anchors on literal identifier names:
`\\b(?:aiConfig|Memory_Config)\\b[\\w.$[\\]'"\`-]*` + `(?:\\.${DB_PATH_LEAVES}\\b|…)` + `…\\s*=(?![=>])`Anything binding the same singleton under a different name is invisible to it.
The Problem — two measured holes, one root cause
Probed directly against the exported
DB_PATH_MUTATION:aiConfig.storagePaths.graph = dbPathaiConfig['storagePaths'].graph = dbPathMemory_Config.collections.memory = xAiConfig.storagePaths.graph = dbPathAiConfig['storagePaths'].graph = dbPathmailboxAiConfig.storagePaths.graph = dbPath1. The approved PascalCase sweep makes the gate inert. #13532 (open, @neo-opus-vega) normalizes 1,526 occurrences across 198 files from
aiConfig→AiConfig, on @tobiu's confirmed direction. The lint is case-sensitive, so after that sweep its pattern matches nothing in the repo.The trap is #13532's own AC3: "
check-aiconfig-test-mutationmust stay green (this is a rename, not a mutation)." That AC would be satisfied trivially — green because the lint has become inert, not because the mutations are gone. A safety gate would be retired by a refactor whose acceptance criteria certify the retirement as success. Neither ticket is wrong on its own; the interaction is invisible from inside either.2. Aliased roots already evade it today. Measured across
test/playwright/unitwith the lint's own regex versus an alias-tolerant one: 18 spec files assign a Class-A DB-path leaf on a config-shaped root, 15 caught, 3 missed —fleetMailboxMirrorAdapter.spec.mjs(4 occurrences,mirrorAiConfig),MailboxService.spec.mjs(7,mailboxAiConfig),MailboxService.ReceiptDurability.spec.mjs(3, mine — disclosed on PR #15824). 14 live B4 mutations invisible to a fail-build gate.Note
#12435— the tracked B4 cleanup ADR-0019 §4 cites — is CLOSED. So the ADR's own footnote points at a finished ticket while the class is still reachable by rename.The Architectural Reality
buildScripts/util/check-aiconfig-test-mutation.mjs—DB_PATH_MUTATION, identifier-anchored;DB_PATH_LEAVES = (?:storagePaths|database|collections|logPath);ESCAPE_MARKER = 'aiconfig-mutation-ok'; plus a grandfathered file list.==/=>. The tokenizer is not the weakness — the identifier anchor is.The Fix (direction)
Match the structure, not the name. Options, cheapest first:
/\\b[A-Za-z_$][\\w$]*(?:[Aa]i)?Config\\b/(or an explicit alias list) before a Class-A leaf. Cheap, closes both holes, risks false positives on unrelated*Configobjects — which the existingESCAPE_MARKERalready handles for genuinely-unavoidable cases.**/config.mjsdefault import. Precise, more work, and the acorn parse is already there.Sequencing is the point: this must land before #13532's sweep, or the sweep's green CI will certify a dead gate. Filed as a blocker on that ticket rather than a parallel improvement.
Acceptance Criteria
AiConfig.storagePaths.graph = xandAiConfig['storagePaths'].graph = xare FLAGGED — spec-pinned against the exported regex/checker, not asserted in prose.mailboxAiConfig,mirrorAiConfig) is FLAGGED.Out of Scope
.DS_Store/ blocklist class (#15831) — unrelated substrate-classification gap.Refs #13532, #15824, #12435
Authored by Grace (@neo-opus-grace, Claude Opus 4.8). Found while reading ADR-0019 for #15800's config-adjacent read gate — the gate caught a B4 mutation in my own just-pushed spec, and asking why the pre-commit check passed surfaced the identifier anchor.