Status: Accepted Date: 2026-02-10 Deciders: Reflections Maintainers
Context
The platform has several critical architectural invariants: read-only realtime plane, gated learning, vendor SDK isolation, process.env confinement, and deep-import prohibition. Early in development, enforcement relied on a single mechanism (ESLint rules or convention). This created bypass vectors:
- ESLint rules can be disabled with
eslint-disable comments.
- Architecture guard only runs in CI, providing no IDE feedback.
- File-scan tests catch everything but are slower and give less guidance than lint errors.
- Dynamic
import() and require() bypass ESLint’s static no-restricted-imports rule.
No single mechanism covers all vectors.
Decision
Adopt a three-layer enforcement architecture with asymmetric allocation based on blast radius:
| Layer | Mechanism | Feedback time | Bypass vector | Mitigation |
|---|
| L1: ESLint | no-restricted-imports, no-restricted-syntax | IDE + pre-commit | eslint-disable comments | Reason guard test (L3) |
| L2: Architecture guard | simplicity-guard.mjs | CI only | PR merge without CI | Branch protection rules |
| L2b: Design guard | design-guard.mjs | CI only | PR merge without CI | Branch protection rules |
| L3: File-scan tests | Vitest tests scanning source files | CI + local test | Modifying the test file itself | Visible in PR diff |
Layer allocation per invariant:
| Invariant | L1 | L2 | L3 | Rationale |
|---|
| Admin query boundary | Yes | Yes | Yes | Data integrity risk; highest blast radius |
| Vendor SDK isolation | Yes | Yes | - | Architectural coupling; L1+L2 sufficient |
| Deep imports | Yes | Yes | - | Package hygiene; self-correcting on rebuild |
| process.env restriction | Yes | - | Yes | Secrets protection; file-scan catches bypass |
| eslint-disable governance | - | - | Yes | Meta-enforcement of the lint layer |
| Hex color isolation | - | L2b | - | Visual consistency; scoped to web |
| Inline style semantics | - | L2b | - | Design system separation of concerns |
| Meta-label consistency | - | L2b | - | Design system coherence |
| Transition timing | - | L2b | - | Performance consistency |
| WebGL budget | - | L2b | - | GPU memory protection |
Allocation criteria:
- All 3 layers: Invariants where violation causes silent data corruption or security breach.
- L1 + L2: Core architectural boundaries where violation is functionally visible in testing.
- L1 + L3: Secrets/env isolation where
eslint-disable bypass is the primary risk vector.
- L3 only: Meta-enforcement of the lint layer itself.
- L2b only: Design-system invariants where violation is visually detectable and scoped to web frontend.
Alternatives considered
Alternative 1: ESLint-only enforcement
Pros:
- Fastest developer feedback (IDE integration).
- Single configuration file.
Cons:
- Completely bypassable with
eslint-disable.
- Doesn’t catch dynamic imports or require().
- No protection during CI pipeline failures.
Alternative 2: Architecture guard only (CI-only enforcement)
Pros:
- Catches static and dynamic imports.
- Single script to maintain.
Cons:
- No IDE feedback (errors only visible after push).
- No pre-commit enforcement.
- Slower developer feedback loop.
Pros:
- Maximum defense in depth.
- Simple mental model.
Cons:
- Maintenance overhead for invariants where extra layers add no value.
- Violates 0-user operating principle: add complexity only where there’s a measured need.
Alternative 4: Pre-commit hooks with custom scripts
Pros:
- Runs before code enters git.
Cons:
- Slow feedback during development (only fires on commit, not in IDE).
- Duplication with lint-staged (which already runs ESLint on staged files).
- Harder to maintain than declarative ESLint config.
Consequences
Benefits:
- Defense in depth: no single bypass vector defeats all layers.
- Fast feedback: ESLint provides IDE-time red squiggles for import violations.
- Proportional enforcement: higher-risk invariants get more layers without burdening lower-risk ones.
- Self-documenting: the enforcement layer table tells new contributors exactly what protects each invariant.
Costs:
- More enforcement mechanisms to maintain (ESLint config + architecture guard + file-scan tests).
- Shared variable pattern in ESLint config requires updating multiple rule blocks when adding a new vendor SDK restriction.
- File-scan tests use string matching (not AST parsing), which can produce false positives on comments. Mitigated by allowlists.
Risks:
- Override ordering drift: if someone reorders config blocks in the ESLint flat config, the vendors exemption could silently break enforcement. Mitigated by comments explaining ordering requirements.
- File-scan test brittleness: string-matching can catch mentions in comments. Mitigated by ALLOWED_FILES sets.
Implementation notes
- ESLint flat config uses shared restriction patterns extracted as module-level variables for DRY composition. Override ordering is critical: vendor exemption must appear before plane-specific overrides.
- Architecture guard validates boundary and complexity constraints in CI.
- Design guard runs 5 checks (hex colors, inline style semantics, meta-label consistency, transition timing, WebGL budget) scoped to web frontend TypeScript files.
- CI execution order: build packages, format check, lint, architecture guard, design guard, typecheck, test, build.