Skip to main content

SOLID boundaries eslint spike — eslint-plugin-boundaries on canonical modules

Tier: Internal
Status: Frozen (2026-06-04) — Subplan A4 deliverable
Audience: module authors, CI maintainers
Related: solid-decoupling-audit.md §4 Tier A, solid-audit-charter.md, RFC-0002, LINTING.md


1. Why this spike exists

Trigger: SOLID audit Tier A recommended piloting eslint-plugin-boundaries on services/api/src/modules/** (RFC-0002 footnote; solid-decoupling-audit.md §4). Subplan A1 removed the only known P0 sibling import (CRP→MRP for projection IDs); this spike asks whether mechanical lint can prevent recurrence without blocking the repo on false positives.

Decision deadline: Gates Subplan B5 (promote warn → error + blocking CI). This spike does not enable blocking CI.


2. What we have today

ArtifactRole
scripts/audit/solid-inventory.tsReport-only sibling-import + fat-file scanner
npm run audit:solid-inventoryRegenerates solid-audit-inventory.md
@arch-boundary conventionDocuments accepted in-process coupling (charter §6)
A1 platform/breweryProjectionIds.tsShared ID helpers; zero sibling imports in modules/** (2026-06-04)

Intentional cross-layer imports (out of this spike's scope):

  • services/api/src/services/ai/tools/**modules/*/services/* (horizontal AI advisor reads module services)
  • services/api/src/app.ts → module register*Module() entrypoints
  • modules/brewery/**services/api/src/domain/** (vertical domain; not a sibling module)

3. Prototype configuration

File: eslint.config.mjs — block scoped to services/api/src/modules/**/*.{ts,tsx}.

Elements:

TypePathPurpose
platformservices/api/src/platform/**Shared cross-module helpers (A1)
domainservices/api/src/domain/**Brewery vertical domain (allowed from modules)
pluginsservices/api/src/plugins/**Fastify plugins (allowed from modules)
canonical-moduleservices/api/src/modules/*/**One element per module folder; capture: ["moduleCode"]

Rule: boundaries/element-types at warn — canonical modules disallow imports where dependency.moduleCode !== from.moduleCode.

Dependencies added: eslint-plugin-boundaries@^5, eslint-import-resolver-typescript@^4 (root devDependencies).


4. Spike measurements (2026-06-04)

CheckResult
npx eslint services/api/src/modules on clean tree0 warnings, 0 errors
Synthetic probe (crp importing ../../mrp/...)1 warning (boundaries/element-types) — rule fires
False positives on legitimate domain/, platform/, plugins/ imports0 observed
Full-repo npm run lint (ci-parity lint job)Green after adding scripts/audit/solid-inventory.ts to allowDefaultProject

5. Six skeptical tests

5.1 Test A — Novelty bias

Question: Are we adopting boundaries lint because it is trendy?

Evidence: RFC-0001 §8.2 and RFC-0002 footnote already committed to boundaries lint post-β-layout. The spike implements a previously planned enforcement path, not a new architectural direction.

Verdict: Passes — not novelty-driven.

5.2 Test B — Replacement value

Question: Does eslint-plugin-boundaries add signal beyond solid-inventory.ts?

Evidence: Inventory runs in CI only if wired separately; ESLint runs on every web-lint / npm run lint touch of services/api/src/**. ESLint gives editor-time feedback; inventory catches fat files and non-import heuristics ESLint does not.

Verdict: Passes — complementary, not redundant for sibling imports.

5.3 Test C — Cost of adoption

Question: Is the config + resolver cost acceptable?

Evidence: One flat-config block (~60 LoC), two devDependencies, ~8s incremental lint on modules/**. No changes to runtime or contracts.

Verdict: Passes — low cost for scoped rule.

5.4 Test D — False-positive budget

Question: Does the rule fire incorrectly on the current tree?

Evidence: 0 warnings on 741-file inventory scan surface under modules/**. AI tools and app.ts registration are outside the scoped files glob by design.

Verdict: Passes — false-positive count 0 (threshold for blocking CI: spike plan used >5 as fail).

5.5 Test E — Expressiveness gap

Question: Can we express “AI tools may import module services” without breaking module rules?

Evidence: Horizontal services/ai/tools/** is not in the canonical-module element set; those imports are unaffected. Extending lint to forbid modules → horizontal services would be a separate rule set (out of A4 scope).

Verdict: Passes for sibling-module enforcement; horizontal direction not covered (documented deferral).

5.6 Test F — Operational fit

Question: Does this fit ci-parity / GHA without a new @umbraculum/ci-parity release?

Evidence: Prototype piggybacks on existing lint job (npm run lint → full repo ESLint). Optional non-blocking inventory workflow added separately (report-only).

Verdict: Passes — no ci-parity manifest job id required for prototype.


6. Verdict

Blocking CI adoption (error-level, merge-blocking)

SOUND — with conditions:

  1. Keep scope at services/api/src/modules/** only (not services/ai/tools, not apps/).
  2. Ship warn for one release cycle on master (current prototype); promote to error in Subplan B5.
  3. Pair with npm run audit:solid-inventory on services/api/** PRs as report-only (non-blocking) drift signal.

Warn-only / report-only adoption (immediate)

SOUND — land as-is on master.


7. Open questions (none blocking B5)

  • Should modules/brewerydomain/** remain allowed indefinitely, or move behind a port when B3 lands?
  • When module count exceeds ~8, revisit single canonical-module capture vs per-module element types for clearer messages.

ActionOwnerWhen
Keep prototype at warn on mastermerged A4now
Subplan B5: warn → error + inventory drift check (optional blocking)Tier B waveafter Tier A push
Re-evaluate if new sibling import bypasses relative paths (package-scope imports)inventory scriptongoing

9. Sign-off

FieldValue
Date2026-06-04
SubplanA4
VerdictSOUND (warn shipped A4; error-level CI blocking landed B5 2026-06-04)
Successorsolid-impl-B5-boundaries-ci.plan.mdlanded 2026-06-04 (boundaries/element-types promoted to error)