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-disablecomments. - 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()andrequire()bypass ESLint’s staticno-restricted-importsrule.
Decision
Adopt a three-layer enforcement architecture with asymmetric allocation based on blast radius:
Layer allocation per invariant:
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-disablebypass 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.
- 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.
- No IDE feedback (errors only visible after push).
- No pre-commit enforcement.
- Slower developer feedback loop.
Alternative 3: Uniform three layers for every invariant
Pros:- Maximum defense in depth.
- Simple mental model.
- 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.
- 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.
- 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.
- 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.

