Files
Danijel Martinek c85f96c62e feat(skills): improve-codebase-architecture skill adapted for template-vertical
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>
2026-05-13 17:30:59 +02:00

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

Modulein 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.ts declares 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: InMemoryEventBusPayloadJobsEventBus.
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 interface keyword 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 (feature may depend on core + tooling only). 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