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

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.mdmodule, 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):

  • <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 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 <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-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 (<x>.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 <x>.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/<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.