# Deepening How to deepen a cluster of shallow modules in this repo, given its dependencies. **In this repo "module" defaults to "feature"** — most deepenings operate on or within a feature (`packages//`). Narrower scopes (use case, controller, repository) follow the same dependency-category logic. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**. ## Dependency categories When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam. ### 1. In-process Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed. **Where this lives in our repo:** - `entities/models/**` — Zod schemas + types + pure helpers - `entities/errors/**` — domain error classes - `application/use-cases/**` — pure orchestration (factories take ports as deps) - `interface-adapters/controllers/**` + their colocated `presenter` functions - Pure helpers in `core-shared/conformance/`, `core-shared/instrumentation/` (the non-OTel parts) For category 1 modules: **merge, then test the result through its interface**. No mocks. The test surface IS the new interface. ### 2. Local-substitutable Dependencies that have local test stand-ins. **In this repo every infrastructure port already has both a real implementation and a mock side-by-side** — the mock IS the test stand-in. **Pattern (from ADR-012):** - `.repository.interface.ts` — the port (seam) - `.repository.ts` — Payload-backed adapter (real) - `.repository.mock.ts` — in-memory adapter (test stand-in) The deepened module is tested with the `.mock.ts` adapter injected directly via the factory function. No container rebinding (ADR-012). **Common category-2 ports in this repo:** - `IUsersRepository`, `IArticlesRepository`, `IMediaRepository`, etc. → Mock + Payload adapters - `IAuthenticationService` → Mock + real adapter - `IJobQueue` (from `core-shared/jobs/`) → `InMemoryJobQueue` + `PayloadJobQueue` - `IEventBus` (from `core-events/`) → `InMemoryEventBus` + `PayloadJobsEventBus` - `ITracer`, `ILogger`, `IMetrics` (from `core-shared/instrumentation/`) → `Noop*` + `Otel*` + `Recording*` (the third one lives in `core-testing` for assertions) If the deepening touches a category-2 port, the recommendation shape is: _"Merge X into Y, keep the port boundary at the existing `.repository.interface.ts`; both adapters survive unchanged."_ ### 3. Remote but owned Our own services across a network boundary. **In this repo, cross-feature communication is already this pattern via the event bus (ADR-015).** The **port** is `IEventBus.publish(descriptor, payload)` + `IEventBus.subscribe(descriptor, consumerFeature, handler)`. Adapters: - `InMemoryEventBus` (test + dev-seed) — synchronous fan-out - `PayloadJobsEventBus` (production) — Payload tasks fan-out durably across the network if features are split into separate deploys If a deepening proposal would introduce a NEW cross-feature seam, the answer is almost always "use the event bus" — don't invent a new transport. Rule E0 forbids in-feature use of the bus (in-feature reactions are direct use-case calls); E1 keeps consumer handlers private. If the deepening proposal would EXPOSE one feature's internals to another, that's a boundary violation — reject and suggest events instead. ### 4. True external Third-party services we don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter. **Where this lives in our repo:** - Payload CMS itself — features depend on `IXRepository`, not on `payload` directly. The real adapter (`.repository.ts`) is the only place Payload is touched. - Sentry / OpenTelemetry exporters — features use `ITracer`/`ILogger`/`IMetrics` from `core-shared/instrumentation/`. The OTel SDK lives ONLY in `core-shared/instrumentation/otel/` (ESLint-enforced via rule R52). - Socket.IO — features use `IRealtimeBroadcaster`; `socket.io` itself lives only in `@repo/core-realtime` (rule R2). If a deepening proposal would import a vendor SDK from a feature package, that's an ADR-014/ADR-016/ADR-017 violation — reject and route through the existing port. ## Seam discipline - **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). Single-adapter ports in this repo are usually a smell — check if the mock is missing or if the port itself is unnecessary. - **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them. - **The DI symbol is the seam contract.** `*_SYMBOLS.IXRepository` plus `.repository.interface.ts` together define what callers depend on. Adapter swaps happen at bind time in `bind-production.ts` / `bind-dev-seed.ts`. - **`feature.manifest.ts` is the structural-conformance seam.** If a deepening moves use cases across features, the manifests of BOTH features change — the conformance ESLint rules + `assertFeatureConformance` boot check enforce that the move is reflected in declarations, not just code. ## Testing strategy: replace, don't layer - **Old unit tests on shallow modules become waste** once tests at the deepened module's interface exist — delete them. Our coverage thresholds (ADR-020) reward this: collapsing N shallow modules + their N test files into one deep module + one test file at its interface keeps coverage 100% without test bloat. - **Write new tests at the deepened module's interface.** The **interface is the test surface**. - **Tests assert on observable outcomes through the interface**, not internal state. - **Tests should survive internal refactors** — if a test has to change when the implementation changes, it's testing past the interface. - **L1 diff coverage (`pnpm coverage:diff`) will surface uncovered lines after the refactor** — every deepened module needs its new tests to cover the merged behaviour before the refactor PR is mergeable. ## Conformance check (run before claiming the deepening is complete) After deepening, the following must all stay green: ``` pnpm typecheck # TS brand-slot enforcement pnpm lint # ESLint conformance + boundaries pnpm test --filter @repo/ -- --coverage # per-layer L0 thresholds pnpm conformance # cross-feature event closure pnpm fallow:audit # whole-codebase audit + dead-export sweep pnpm coverage:diff -- --base origin/main # cover-the-diff (ADR-020 L1) ``` If `pnpm dev` was running, it should still boot — `assertFeatureConformance` will fail loudly on brand-slot or manifest drift if the deepening forgot a binding update.