Skip to main content

SOLID / decoupling audit — system-wide (umbraculum-dev)

Tier: Internal
Status: Signed off 2026-06-04 — SOUND (codify as architectural pillar with pragmatic scope)
Audience: project lead, module authors, AI agents
Related: solid-audit-charter.md, solid-audit-inventory.md, architectural-audit-template.md, application-surfaces-vs-platform-backbone.md, DATA-ACCESS-BOUNDARIES.md, RFC-0001, RFC-0002


1. Why this audit exists

Trigger: Milestone plan to assess whether SOLID heuristics, mapped to Umbraculum-native boundaries, should become a documented architectural pillar with agent enforcement — without mandating Java-style IoC everywhere.

Scope: apps/, services/api/, packages/ (740+ TS/TSX files scanned).

Method: Automated inventory (scripts/audit/solid-inventory.ts) + manual slice review + six skeptical tests from architectural-audit-template.md.


2. SOLID mapped to repo-native vocabulary

LetterPrincipleUmbraculum expression
SSingle ResponsibilityOne URL-segment owner (registerWebModule); one Prisma schema per module; routes thin / services own logic; contracts wire-only
OOpen/ClosedregisterModule() extension points; versioned *V1 DTOs; new modules via registration
LLiskov SubstitutionZod response schemas; ValidatedSchema<T> in ai-tool-sdk
IInterface SegregationSeparate *-contracts; clients use api-client; UI layering in CODING-STANDARDS
DDependency InversionApps → HTTP + contracts; modules → platform SDK; not every service class needs an interface

Guiding sentence: Follow dependency direction and single ownership; extend via registration and contracts; when coupling is intentional, say why in code (@arch-boundary).


3. Slice-by-slice manual audit

3.1 API canonical modules (pim, mrp, crp, automation)

ModuleSOLIDNotes
pimPassPassPassPassPassReference β-layout: thin routes → ProductsService
automationPassPassPassPassPassAdapters folder for external seams
mrpPartialPartialPassPartialPartialMrpBreweryProjectionService reads brewery.* schema (documented @arch-boundary)
crpPartialPartialPassPartialFail (P0)CRP→MRP breweryProjectionIds import — Tier A fix

Findings:

  • PIM is the target shape for canonical modules: route file ~130 LoC, service owns Prisma.
  • MRP/CRP α projections intentionally cross-read brewery + automation tables until a BreweryScheduleProjection port (Tier B).
  • No other sibling module imports detected besides CRP→MRP (P0).

3.2 Brewery vertical

AreaSDNotes
modules/brewery/routes/PartialPartialRoutes migrated (RFC-0002); services still in horizontal services/api/src/services/ (P2 transitional)
waterCalcFail (P1)Partial~1.4k LoC fat route — Tier A: extract WaterCalcService
recipesService / brewSessionsServiceFail (P1)Partial~1.2–1.3k LoC god services — Tier B split
domain/waterCalc/*PassPassCohesive pure functions; large files acceptable if logically single domain

3.3 Platform routes/services

FileSeverityNotes
routes/auth.tsP2~530 LoC; Tier A: extract AuthService
routes/ai.tsP2Usage aggregation inline; defer service extraction
routes/integrationsTilt.tsP2Mixed service + raw Prisma
services/ai/tools/*P2Platform AI binds to concrete module services (acceptable until tool registry ports)

3.4 Packages

PackageAssessment
*-contractsStrong — ESLint forbids hand-rolled type guards (RFC-0003)
module-sdkAccepted — process singleton registries are intentional composition root (OCP)
beerjsonP3 — large barrel; packaging split optional
brewery-corePass — shared domain math, no server imports

Apps → server: No services/api or @prisma imports in apps (inventory clean after allowlist fix). Client packages (ui, navigation, i18n, etc.) correctly consumed.

3.5 Apps (web / native)

FindingSeverity
apps/web/app/[locale]/(brewery)/recipes/[id]/edit/ (formerly flat apps/web/app/recipes/[id]/edit/page.tsx ~3.8k LoC)Resolved — Tier B decompose landed (Waves 11–17 + S closure); tree consolidated under (brewery)/recipes/ 2026-06
apps/native/src/modules/*Pass — thin module slices
apps/web/src/*Pass — re-exports only

3.6 Cross-module edges

EdgeSeverityRecommendation
CRP imports MRP breweryProjectionIdsP0Unify in platform/breweryProjectionIds.ts (Tier A)
Duplicate brewery-brew-session-step- prefixP1Same shared module (Tier A)
MRP/CRP brewery schema readsP2 (accepted)@arch-boundary + README § Known couplings until projection port (Tier A/B)

4. Tier A / B / C desirability matrix

ActionRationalePrinciples
Unify breweryProjectionIds in platform/Removes duplicate prefix constants; fixes P0 CRP→MRP importO, D
Remove CRP → MRP direct importOnly P0 sibling-module violation foundD
Extract AuthService from routes/auth.tsMatches PIM patternS, D
Extract WaterCalcService from waterCalc.ts routesAligns brewery with β-layoutS
Add @arch-boundary on MRP/CRP brewery projection servicesDocuments intentional cross-schema read for αS, D
Pilot eslint-plugin-boundaries on modules/*Mechanical D enforcement (RFC-0002 footnote)D

Inventory / charter tooling (this milestone — documentation only):

ActionStatus
scripts/audit/solid-inventory.ts + npm run audit:solid-inventoryDelivered
Charter + audit + inventory snapshotDelivered
CODING-STANDARDS § Architectural couplingDelivered (append-only)
Toolset rule 03-layering-and-coupling-discipline.mdcDelivered in umbraculum-toolset

Tier B — desirable; follow-up epics

ActionRationale
Split recipesService / brewSessionsServiceHigh-churn brewery core; reduce merge conflicts
Decompose web recipe edit pageSRP for UI; native parity
BreweryScheduleProjection port for MRP/CRPAnti-corruption layer for cross-schema reads
Thin ai.ts, integrationsTilt.ts, brewSessions.ts routesPIM consistency
Pilot eslint-plugin-boundaries on modules/*Mechanical D enforcement (RFC-0002 footnote)

Tier C — not desirable now (document why)

PatternWhy keepDocumentation
new XxxService(app.prisma) without interfacesFastify + vi.mock sufficient; IoC ceremonyRule + audit §4
Interface per Prisma serviceNo runtime polymorphismWont-fix
Full anti-corruption for brewery ↔ MRP/CRP nowα scope; cost >> benefit until ROADMAP H1 2027@arch-boundary
Split beerjson barrelStable; hygiene onlyP3 backlog
module-sdk singleton registriesBoot-time composition rootAccepted OCP pattern
Large cohesive domain/* filesLogical SRP ≠ file sizeReview logic only

5. What we have today (strengths)

  • Contracts split — wire DTOs isolated per module (*-contracts).
  • Data-access boundaries — clients never touch Prisma.
  • Module registrationregisterModule() / registerWebModule() extension points.
  • PIM reference layout — reproducible thin-route pattern.
  • Validation at boundary — Zod + Fastify type provider (RFC-0003).

6. Six skeptical tests

6.1 Test A — Novelty bias

Question: Are we recommending SOLID because it is trendy?

Evidence: Umbraculum already encodes S/D/I via RFC-0001/0002 and boundary docs without the acronym. Recommendation maps SOLID to existing patterns, not a new framework.

Verdict: Passes — not novelty-driven.

6.2 Test B — Cost-estimate honesty

Question: Is the cost estimate credible?

Evidence: Tier A (documentation + inventory tooling) ≈ 2–3 days. Tier A code refactors ≈ 3–5 days additional — explicitly deferred to a follow-up implementation epic, not this audit milestone.

Verdict: Passes — scoped honestly.

6.3 Test C — Intermediate options

OptionAssessment
Full SOLID + DI containersRejected — overkill
Docs-only (no agent rule)Weaker agent signal; chosen: docs + scoped rule
Map SOLID to native boundaries + @arch-boundaryChosen — middle ground
CI LoC gatesRejected — too noisy; report-only inventory
Wait for RFC-0002 completionPartial — audit proceeds with P2 transitional labels

Verdict: Passes — middle ground selected.

6.4 Test D — Timing soundness

Question: Why now?

Evidence: Mid-modularization (RFC-0002) is when boundary violations compound — P0 CRP→MRP import, god services, fat routes. Delaying agent guidance until post-β-layout means agents replicate transitional anti-patterns.

Verdict: Passes — timing justified.

6.5 Test E — Cost of being wrong

ScenarioDirectIndirectRecovery
Wrong: adopt SOLID pillarRule + doc churnAgent confusion if over-specifiedNarrow rule scope; amend charter
Wrong: deferP0 imports recurAgents copy fat-route patternRe-run audit; higher refactor cost
Right: adopt (scoped)Tier A refactorsNone significant
Right: defer docs-onlyNoneContinued drift in modulesAudit inventory still useful

Verdict: Passes — asymmetric cost favors scoped codification.

6.6 Test F — Falsifiability

Would have failed SOUND if:

  1. Inventory found >3 P0 sibling module imports — found 1 (CRP→MRP); not fixed in this milestone.
  2. Apps imported Prisma/server paths — none found.
  3. PIM module violated route/service split — passes.
  4. Tier A refactors would break typecheck if done carelessly — spike recommended before implementation epic.

Verdict: Passes for audit + governance scope; implementation falsifiers remain for the follow-up epic.


7. Verdict

SOUND — Codify SOLID as repo-native coupling discipline (charter + CODING-STANDARDS + toolset rule 03-layering-and-coupling-discipline.mdc). No new RFC required — RFC-0001/0002 already commit module boundaries; this audit adds vocabulary, scoring, @arch-boundary, and inventory tooling.

Structural caveat: DIP is satisfied at package/layer boundary, not per service class. LSP applies primarily at Zod wire contracts, not TypeScript class hierarchies.


  1. Done (audit milestone): Charter, inventory script, inventory snapshot, governance docs/rule.
  2. Done (implementation epic, 2026-06-04): Tier A + B — projection IDs, AuthService, WaterCalcService, boundaries spike + CI, god-service splits, recipe-edit shell refactor (web + native), projection port, thin routes.
  3. Optional backlog: Recipe-edit section component splits (RecipeEditPageContent, RecipeEditScreenContent); apps/** boundaries lint (future epic); water/brew-session god pages per B2 follow-ups.

9. Sign-off

FieldValue
Date2026-06-04
VerdictSOUND
Successor artifactssolid-audit-charter.md, CODING-STANDARDS § Architectural coupling, toolset 03-layering-and-coupling-discipline.mdc
RFCNot required (existing RFC-0001/0002 sufficient)

Audit body frozen 2026-06-04. Inventory rows regenerate via script; do not edit solid-audit-inventory.md by hand.


10. Post–Wave 17 addendum (2026-06)

Status: Mechanical Waves 11–17 + Tier A/B epic complete on master (bfd6214).

Rows in §3 referencing ~1.2k LoC recipesService / brewSessionsService, ~3.8k LoC recipe edit page.tsx, and ~2k LoC water pages are superseded. Current state:

  • God services decomposed to facades + focused services under services/api/src/services/.
  • God pages reduced to thin shells (~9 LoC) + hooks/components.
  • Inventory automation: P2=0, P3=0; P1 = generated OpenAPI only.
  • D enforcement: B5 (API modules), WS5 (apps), WS6 (client-safe imports) all at error — verified via T2-PR lint job.
  • Agent gate: AGENTS.md § SOLID and dependency direction (D).

Canonical program status: solid-post-wave17-closure.md.

Do not schedule Wave 18+ unless inventory regresses (see closure doc §4).