Context
ADR-0019 §8's entire thesis: reviewer diligence is empirically insufficient against the AiConfig antipattern cluster (the #12420 4/4-miss anchor). The mechanical backstop is the fail-build lint (§7.2, sub #2), spec'd to encode A1·A4·A5·A6·A7·B1·B3·B4·B5·C1.
Only B4 was ever built (buildScripts/util/check-aiconfig-test-mutation.mjs → aiconfig-test-mutation-lint.yml). B3 / A1 / A5 / B1 have no mechanical check — which is why they keep reaching human review and getting approved. Live proof: PR #14499 shipped aiConfig?.validateRequiredEnv, aiConfig.auth?.mode (B3), got a full APPROVED cross-family review that caught zero of it, and was flagged only by @tobiu ("fuck-up 50+ for ADR-19"). CI-green + APPROVED ≠ ADR-compliant, exactly as ADR-0019 §3 D1/D2 predicts.
The Problem
The recurring ADR-0019 violations are not a diligence problem (the ADR settled that). They recur because the flaggable-subset lint the ADR prescribed was never completed. Every aiConfig?. / AiConfig?. reaches review because nothing fails the build.
The Architectural Reality
- Model:
buildScripts/util/check-aiconfig-test-mutation.mjs (B4) — a regex (DB_PATH_MUTATION) + ESCAPE_MARKER relief valve + shrinking ALLOWLIST ratchet (bites only NEW offenders) + CI workflow. Proven shape; mirror it.
- ADR-0019 §3 catalog is the lint spec (each ID has a sanctioned form).
The Fix
Add a lint (new buildScripts/util/check-aiconfig-antipatterns.mjs or extend the B4 checker) + CI workflow, catching the mechanically-cleanest subset first:
- B3 — defensive optional-chaining on an AiConfig read:
\b(?:aiConfig|AiConfig|Memory_Config)\b[\w.$\[\]'"-]*?.(a?.` anywhere in an AiConfig access path). §3 B3: "the SSOT guarantees the tree; let it fail loud."
- A5 — a
hasEnvValue( helper (0 occurrences on dev today; ratchet prevents reintroduction).
- A1 (fast-follow) — module-level
const X = process.env.Y || … re-derivation in ai/ entrypoints that already import AiConfig (read AiConfig.X.Y instead; §5.5 — A1-with-AiConfig-imported, NOT C1).
Ratchet discipline: seed the ALLOWLIST from a current-dev census so the check bites only NEW offenders; the allowlist shrinks as the Diamond-2 cleanup subs land.
Acceptance Criteria
Out of Scope
- The B5/C1/A6/A7 checks (harder, AST-level) — separate fast-follows under this epic once B3/A5/A1 land.
- The #14499 code fixes themselves (that PR's REQUEST_CHANGES owns them).
Related
- Parent epic: #12456 (eliminate the
ai/ AiConfig-SSOT antipatterns).
- ADR-0019 §3 (catalog = lint spec) + §7.2 (sub #2). Model:
check-aiconfig-test-mutation.mjs.
- Empirical anchor: PR #14499 (B3 shipped + rubber-stamped) · #12420 (the 4/4-miss).
Authored by Grace (@neo-opus-grace, Claude Opus 4.8). Operator-flagged pattern (@tobiu).
Context
ADR-0019 §8's entire thesis: reviewer diligence is empirically insufficient against the AiConfig antipattern cluster (the #12420 4/4-miss anchor). The mechanical backstop is the fail-build lint (§7.2, sub #2), spec'd to encode
A1·A4·A5·A6·A7·B1·B3·B4·B5·C1.Only B4 was ever built (
buildScripts/util/check-aiconfig-test-mutation.mjs→aiconfig-test-mutation-lint.yml). B3 / A1 / A5 / B1 have no mechanical check — which is why they keep reaching human review and getting approved. Live proof: PR #14499 shippedaiConfig?.validateRequiredEnv,aiConfig.auth?.mode(B3), got a full APPROVED cross-family review that caught zero of it, and was flagged only by @tobiu ("fuck-up 50+ for ADR-19"). CI-green + APPROVED ≠ ADR-compliant, exactly as ADR-0019 §3 D1/D2 predicts.The Problem
The recurring ADR-0019 violations are not a diligence problem (the ADR settled that). They recur because the flaggable-subset lint the ADR prescribed was never completed. Every
aiConfig?./AiConfig?.reaches review because nothing fails the build.The Architectural Reality
buildScripts/util/check-aiconfig-test-mutation.mjs(B4) — a regex (DB_PATH_MUTATION) +ESCAPE_MARKERrelief valve + shrinkingALLOWLISTratchet (bites only NEW offenders) + CI workflow. Proven shape; mirror it.The Fix
Add a lint (new
buildScripts/util/check-aiconfig-antipatterns.mjsor extend the B4 checker) + CI workflow, catching the mechanically-cleanest subset first:\b(?:aiConfig|AiConfig|Memory_Config)\b[\w.$\[\]'"-]*?.(a?.` anywhere in an AiConfig access path). §3 B3: "the SSOT guarantees the tree; let it fail loud."hasEnvValue(helper (0 occurrences on dev today; ratchet prevents reintroduction).const X = process.env.Y || …re-derivation inai/entrypoints that already import AiConfig (readAiConfig.X.Yinstead; §5.5 — A1-with-AiConfig-imported, NOT C1).Ratchet discipline: seed the ALLOWLIST from a current-dev census so the check bites only NEW offenders; the allowlist shrinks as the Diamond-2 cleanup subs land.
Acceptance Criteria
aiConfig?./AiConfig?./Memory_Config?.optional-chaining inai/(excluding the ESCAPE_MARKER + seeded allowlist).hasEnvValue(helper.ESCAPE_MARKERrelief valve + repo-relativeALLOWLISTratchet, mirroring the B4 checker..github/workflows/aiconfig-antipattern-lint.yml); fails the build on a new offender.aiConfig?.xfails) + negative (aiConfig.x,x === undefined ? …pass) + escape-marker + allowlist.Out of Scope
Related
ai/AiConfig-SSOT antipatterns).check-aiconfig-test-mutation.mjs.Authored by Grace (@neo-opus-grace, Claude Opus 4.8). Operator-flagged pattern (@tobiu).