Skip to main content
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:
LayerMechanismFeedback timeBypass vectorMitigation
L1: ESLintno-restricted-imports, no-restricted-syntaxIDE + pre-commiteslint-disable commentsReason guard test (L3)
L2: Architecture guardsimplicity-guard.mjsCI onlyPR merge without CIBranch protection rules
L2b: Design guarddesign-guard.mjsCI onlyPR merge without CIBranch protection rules
L3: File-scan testsVitest tests scanning source filesCI + local testModifying the test file itselfVisible in PR diff
Layer allocation per invariant:
InvariantL1L2L3Rationale
Admin query boundaryYesYesYesData integrity risk; highest blast radius
Vendor SDK isolationYesYes-Architectural coupling; L1+L2 sufficient
Deep importsYesYes-Package hygiene; self-correcting on rebuild
process.env restrictionYes-YesSecrets protection; file-scan catches bypass
eslint-disable governance--YesMeta-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.

Alternative 3: Uniform three layers for every invariant

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.