Adapts mattpocock/skills/engineering/improve-codebase-architecture to
this repo. Four files at .claude/skills/improve-codebase-architecture/:
SKILL.md (104 lines):
- Explore -> Present candidates -> Grilling loop process
- "Hard constraints (do not propose violations)" section
enumerating ADRs 006/008/010/012/013/014/015/017/020/021 that
bound the design space
- Repointed at docs/glossary.md (not CONTEXT.md) and
docs/decisions/ (not docs/adr/)
- Exploration shortcuts specific to this repo: pnpm fallow,
pnpm coverage:diff, feature.manifest.ts, pnpm turbo boundaries
- Grilling loop side-effects target the right glossary section
and the next available ADR number (currently 022)
DEEPENING.md (93 lines):
- 4 dependency categories mapped to this repo's reality:
Cat 1 (in-process) -> entities/use-cases/presenters
Cat 2 (local-substitutable) -> our existing real + mock
adapter pattern (every port has both; mocks ARE stand-ins)
Cat 3 (remote but owned) -> cross-feature events via
IEventBus (E0/E1 rules)
Cat 4 (true external) -> Payload, Sentry/OTel, socket.io
(each constrained to its vendor-isolation seam by ADR)
- Seam discipline section recognises DI symbols + manifest entries
as concrete seams alongside .interface.ts files
- Testing strategy: replace not layer (matches ADR-020 L0 + L1)
- Conformance check command list at the end (typecheck, lint,
test --coverage, conformance, fallow:audit, coverage:diff)
INTERFACE-DESIGN.md (66 lines):
- Parallel sub-agent "Design It Twice" pattern preserved
- Every sub-agent brief MUST include glossary terms + ADR
constraints + manifest awareness
- Output items extended with "Manifest + binder impact" and
"ADR conflicts (if any)"
- Comparison axes include conformance impact + coverage delta
- Cross-feature moves flag release-please version-bump
implications (per ADR-021 commit-path targeting)
LANGUAGE.md (79 lines):
- Matt's 7 abstract terms preserved (module, interface,
implementation, depth, seam, adapter, leverage, locality)
- New "Mapping to this repo's identifiers" table — abstract
term -> concrete file shape (e.g. seam -> *.interface.ts +
DI symbol + manifest entry + <gen:*> anchor)
- Rejected framings extended with our reserved meanings
("boundary" stays the ESLint workspace-tag term; "service"
stays the DI port term)
Per user follow-up: vocabulary anchored so that "module" defaults
to "feature" in this repo (since features are our primary unit of
organisation). Abstract refactor sense survives only when the cross-
scale abstraction is the point. Glossary.md updated:
- "Feature" entry adds the "module = feature in refactor sense"
cross-link
- New "Architecture refactor vocabulary" section with 9 terms
(Module, Interface (refactor sense), Implementation, Depth,
Seam, Adapter, Leverage, Locality, Deletion test, Deepening)
— all framed so feature is the primary instance
- Flagged ambiguities entry for "module" rewritten to capture the
three coexisting senses (workspace package / Node ESM / refactor
vocabulary defaulting to feature); new entries for "seam" and
"adapter" to prevent drift with the existing "boundary" / "service"
/ "scope" reservations
Hooks updated:
- session-start.sh skills line lists the new skill
- prompt-context.sh adds a 10th keyword group firing on
refactor / deepening / shallow / architecture / seam / adapter /
interface design / design it twice — inject points at SKILL.md
+ summarises the vocabulary and hard constraints
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.9 KiB
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/<name>/). Narrower scopes (use case, controller, repository) follow the same dependency-category logic. Assumes the vocabulary in 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 helpersentities/errors/**— domain error classesapplication/use-cases/**— pure orchestration (factories take ports as deps)interface-adapters/controllers/**+ their colocatedpresenterfunctions- 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):
<x>.repository.interface.ts— the port (seam)<x>.repository.ts— Payload-backed adapter (real)<x>.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 adaptersIAuthenticationService→ Mock + real adapterIJobQueue(fromcore-shared/jobs/) →InMemoryJobQueue+PayloadJobQueueIEventBus(fromcore-events/) →InMemoryEventBus+PayloadJobsEventBusITracer,ILogger,IMetrics(fromcore-shared/instrumentation/) →Noop*+Otel*+Recording*(the third one lives incore-testingfor assertions)
If the deepening touches a category-2 port, the recommendation shape is: "Merge X into Y, keep the port boundary at the existing <x>.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-outPayloadJobsEventBus(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 onpayloaddirectly. The real adapter (<x>.repository.ts) is the only place Payload is touched. - Sentry / OpenTelemetry exporters — features use
ITracer/ILogger/IMetricsfromcore-shared/instrumentation/. The OTel SDK lives ONLY incore-shared/instrumentation/otel/(ESLint-enforced via rule R52). - Socket.IO — features use
IRealtimeBroadcaster;socket.ioitself 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.IXRepositoryplus<x>.repository.interface.tstogether define what callers depend on. Adapter swaps happen at bind time inbind-production.ts/bind-dev-seed.ts. feature.manifest.tsis the structural-conformance seam. If a deepening moves use cases across features, the manifests of BOTH features change — the conformance ESLint rules +assertFeatureConformanceboot 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/<feature> -- --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.