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>
11 KiB
Language
Shared vocabulary for every suggestion this skill makes. Use these terms exactly — don't substitute "component," "service" (we use that narrowly for DI ports), "API," or "boundary" (overloaded with our workspace-tag enforcement). Consistent language is the whole point.
This vocabulary is foundational for the skill's reasoning. The project's domain vocabulary lives in docs/glossary.md — terms like use case, manifest, slice, binder, brand, conformance band, coverage layer. Both vocabularies are in scope when proposing deepenings; see the "Mapping to this repo's identifiers" section below for how the abstract terms here land on concrete file shapes.
Terms
Module — in this repo, "module" defaults to "feature" (packages/<name>/). The abstract definition (anything with an interface + implementation) still applies at narrower scales — a use case, controller, repository/service port, or binder can also be a module — but whenever the refactor scope is "the whole thing", say feature. Reach for "module" only when the abstraction across scales actually matters (e.g., comparing how a use case's depth differs from its containing feature's depth).
Avoid: unit, component, service (we use "service" for DI ports specifically).
Interface Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, performance characteristics, manifest declarations, and DI symbol contract. Avoid: API, signature (too narrow — those refer only to the type-level surface).
Implementation What's inside a module — its body of code. Distinct from Adapter: a thing can be a small adapter with a large implementation (a Payload-backed repository) or a large adapter with a small implementation (an in-memory mock). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
Depth Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is deep when a large amount of behaviour sits behind a small interface. A module is shallow when the interface is nearly as complex as the implementation.
Seam (from Michael Feathers) A place where you can alter behaviour without editing in that place. The location at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it. Avoid: boundary (this repo uses "boundary" specifically for ESLint workspace-tag rules — keep it for that meaning).
Adapter A concrete thing that satisfies an interface at a seam. Describes role (what slot it fills), not substance (what's inside). In this repo every port typically has at least two adapters (real + mock); some have three (real + mock + recording).
Leverage What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests.
Locality What maintainers get from depth. Change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere.
Principles
- Depth is a property of the interface, not the implementation. A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface.
- The deletion test. Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep.
- The interface is the test surface. Callers and tests cross the same seam. If you want to test past the interface, the module is probably the wrong shape.
- One adapter means a hypothetical seam. Two adapters means a real one. Don't introduce a seam unless something actually varies across it. In this repo, the typical justification is "one real adapter + one mock for tests" — that's two.
- The manifest is a structural seam. A feature's
feature.manifest.tsdeclares its use cases / events / jobs / channels / required cores / coverage bands. Refactors that move behaviour between features MUST move manifest entries too; the conformance gates enforce this.
Relationships
- A Module has exactly one Interface (the surface it presents to callers and tests).
- Depth is a property of a Module, measured against its Interface.
- A Seam is where a Module's Interface lives.
- An Adapter sits at a Seam and satisfies the Interface.
- Depth produces Leverage for callers and Locality for maintainers.
Mapping to this repo's identifiers
Abstract → concrete translation table. When proposing a deepening, name things using the right column.
| Abstract term | Where it lands in this repo |
|---|---|
| Module | Primarily a feature (packages/<name>/) — that's the canonical refactor scope. Also: a use case (*.use-case.ts), controller (*.controller.ts), repository port + adapters (*.repository.{interface,mock,}.ts), service port + adapters (*.service.{interface,mock,}.ts), binder (bind-production.ts / bind-dev-seed.ts), manifest (feature.manifest.ts), or a core package (packages/core-<name>/) — when the refactor operates at those narrower scales. When in doubt, say "feature". |
| Interface | The exported types from a module file: IXUseCase = ReturnType<typeof xUseCase>, IXController, the <x>.repository.interface.ts shape, the manifest's declared keys, the Zod input/output schemas, the DI symbol contract. |
| Implementation | The factory body, the adapter class body, what the binder wires. |
| Seam | <x>.repository.interface.ts, <x>.service.interface.ts, the DI symbol (*_SYMBOLS.IXRepository), the manifest entry, a // <gen:*> anchor, the protocol types in core-shared/di/bind-protocols.ts. |
| Adapter | <x>.repository.ts (Payload real) ↔ <x>.repository.mock.ts (in-memory). For instrumentation: Noop* ↔ Otel* ↔ Recording* (test). For bus: InMemoryEventBus ↔ PayloadJobsEventBus. |
| Test stand-in | The .mock.ts adapter (constructed directly + injected into the factory). No container rebinding (ADR-012). |
Rejected framings
- Depth as ratio of implementation-lines to interface-lines (Ousterhout's original metric): rewards padding the implementation. We use depth-as-leverage instead.
- "Interface" as the TypeScript
interfacekeyword or a class's public methods: too narrow — interface here includes every fact a caller must know, including manifest entries and DI symbols. - "Boundary" as a synonym for seam: this repo uses "boundary" specifically for ESLint workspace-tag rules (
featuremay depend oncore+toolingonly). Keep that meaning intact; say seam or interface when discussing features. - "Service" as a generic term: in this repo, service = a DI-injected port for non-collection capabilities (
IAuthenticationService,IMailerService). Not a generic stand-in for "feature" or "the module doing the work." - "Module" as the canonical noun: avoid in everyday discourse — say feature (or use case / controller / package when narrower). "Module" is the abstract refactor vocabulary's word for the same thing, useful only when the abstraction across scales is the point.
Cross-references
docs/glossary.md— project domain vocabulary (use case, manifest, slice, brand, coverage band, etc.)docs/decisions/— 21 ADRs that constrain the design space- SKILL.md — the skill's process + hard constraints
- DEEPENING.md — dependency categories + seam discipline
- INTERFACE-DESIGN.md — parallel sub-agent design exploration