> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reflections.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-0022: Lint policy and enforcement layer strategy

> Mechanically enforce architectural invariants through layered, defense-in-depth tooling that prevents bypass without sacrificing developer feedback speed.

<Info>**Status:** Accepted **Date:** 2026-02-10 **Deciders:** Reflections Maintainers</Info>

## 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.

### 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.

## Related ADRs

* [ADR-0001: Monorepo and package boundaries](/decisions/adr-0001)
* [ADR-0006: DB query surface segregation](/decisions/adr-0006)
* [ADR-0012: CI/CD quality and release gates](/decisions/adr-0012)
* [ADR-0016: AI vendor and model strategy](/decisions/adr-0016)
