From 949472a7916c2496041a3a10aaa2e16472ac69f4 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Thu, 23 Apr 2026 11:25:36 +0100 Subject: [PATCH] Added DDD-awareness to improve-codebase-architecture --- README.md | 2 +- improve-codebase-architecture/DEEPENING.md | 32 ++++++++ .../INTERFACE-DESIGN.md | 42 ++++++++++ improve-codebase-architecture/REFERENCE.md | 78 ------------------ improve-codebase-architecture/SKILL.md | 79 ++++++++----------- ubiquitous-language/SKILL.md | 1 + 6 files changed, 108 insertions(+), 126 deletions(-) create mode 100644 improve-codebase-architecture/DEEPENING.md create mode 100644 improve-codebase-architecture/INTERFACE-DESIGN.md delete mode 100644 improve-codebase-architecture/REFERENCE.md diff --git a/README.md b/README.md index 11d99fb..29f84ed 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ These skills help you write, refactor, and fix code. npx skills@latest add mattpocock/skills/triage-issue ``` -- **improve-codebase-architecture** — Explore a codebase for architectural improvement opportunities, focusing on deepening shallow modules and improving testability. +- **improve-codebase-architecture** — Find deepening opportunities in a codebase, informed by the domain language in `CONTEXT.md` and the decisions in `docs/adr/`. ``` npx skills@latest add mattpocock/skills/improve-codebase-architecture diff --git a/improve-codebase-architecture/DEEPENING.md b/improve-codebase-architecture/DEEPENING.md new file mode 100644 index 0000000..29f43d1 --- /dev/null +++ b/improve-codebase-architecture/DEEPENING.md @@ -0,0 +1,32 @@ +# Deepening + +How to deepen a cluster of shallow modules safely, given its dependencies. + +## Dependency categories + +When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested. + +### 1. In-process + +Pure computation, in-memory state, no I/O. Always deepenable — just merge the modules and test directly. + +### 2. Local-substitutable + +Dependencies that have local test stand-ins (e.g. PGLite for Postgres, in-memory filesystem). Deepenable if the test substitute exists. The deepened module is tested with the local stand-in running in the test suite. + +### 3. Remote but owned (Ports & Adapters) + +Your own services across a network boundary (microservices, internal APIs). Define a port (interface) at the module boundary. The deep module owns the logic; the transport is injected. Tests use an in-memory adapter. Production uses the real HTTP/gRPC/queue adapter. + +Recommendation shape: *"Define a shared interface (port), implement an HTTP adapter for production and an in-memory adapter for testing, so the logic can be tested as one deep module even though it's deployed across a network boundary."* + +### 4. True external (Mock) + +Third-party services (Stripe, Twilio, etc.) you don't control. Mock at the boundary. The deepened module takes the external dependency as an injected port, and tests provide a mock implementation. + +## Testing strategy: replace, don't layer + +- Old unit tests on shallow modules are waste once boundary tests exist — delete them. +- Write new tests at the deepened module's interface boundary. +- Tests assert on observable outcomes through the public interface, not internal state. +- Tests should survive internal refactors — they describe behavior, not implementation. diff --git a/improve-codebase-architecture/INTERFACE-DESIGN.md b/improve-codebase-architecture/INTERFACE-DESIGN.md new file mode 100644 index 0000000..112bcc4 --- /dev/null +++ b/improve-codebase-architecture/INTERFACE-DESIGN.md @@ -0,0 +1,42 @@ +# Interface Design + +When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best. + +## Process + +### 1. Frame the problem space + +Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: + +- The constraints any new interface would need to satisfy +- The dependencies it would need to rely on (see [DEEPENING.md](DEEPENING.md)) +- A rough illustrative code sketch to ground the constraints — this is not a proposal, just a way to make the constraints concrete + +Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel. + +### 2. Spawn sub-agents + +Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module. + +Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what's being hidden). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint: + +- Agent 1: "Minimize the interface — aim for 1-3 entry points max" +- Agent 2: "Maximize flexibility — support many use cases and extension" +- Agent 3: "Optimize for the most common caller — make the default case trivial" +- Agent 4 (if applicable): "Design around the ports & adapters pattern for cross-boundary dependencies" + +Include CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the project's domain language. + +Each sub-agent outputs: + +1. Interface signature (types, methods, params) +2. Usage example showing how callers use it +3. What complexity it hides internally +4. Dependency strategy (see [DEEPENING.md](DEEPENING.md)) +5. Trade-offs + +### 3. Present and compare + +Present designs sequentially so the user can absorb each one, then compare them in prose. + +After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu. diff --git a/improve-codebase-architecture/REFERENCE.md b/improve-codebase-architecture/REFERENCE.md deleted file mode 100644 index f5ce904..0000000 --- a/improve-codebase-architecture/REFERENCE.md +++ /dev/null @@ -1,78 +0,0 @@ -# Reference - -## Dependency Categories - -When assessing a candidate for deepening, classify its dependencies: - -### 1. In-process - -Pure computation, in-memory state, no I/O. Always deepenable — just merge the modules and test directly. - -### 2. Local-substitutable - -Dependencies that have local test stand-ins (e.g., PGLite for Postgres, in-memory filesystem). Deepenable if the test substitute exists. The deepened module is tested with the local stand-in running in the test suite. - -### 3. Remote but owned (Ports & Adapters) - -Your own services across a network boundary (microservices, internal APIs). Define a port (interface) at the module boundary. The deep module owns the logic; the transport is injected. Tests use an in-memory adapter. Production uses the real HTTP/gRPC/queue adapter. - -Recommendation shape: "Define a shared interface (port), implement an HTTP adapter for production and an in-memory adapter for testing, so the logic can be tested as one deep module even though it's deployed across a network boundary." - -### 4. True external (Mock) - -Third-party services (Stripe, Twilio, etc.) you don't control. Mock at the boundary. The deepened module takes the external dependency as an injected port, and tests provide a mock implementation. - -## Testing Strategy - -The core principle: **replace, don't layer.** - -- Old unit tests on shallow modules are waste once boundary tests exist — delete them -- Write new tests at the deepened module's interface boundary -- Tests assert on observable outcomes through the public interface, not internal state -- Tests should survive internal refactors — they describe behavior, not implementation - -## Issue Template - - - -## Problem - -Describe the architectural friction: - -- Which modules are shallow and tightly coupled -- What integration risk exists in the seams between them -- Why this makes the codebase harder to navigate and maintain - -## Proposed Interface - -The chosen interface design: - -- Interface signature (types, methods, params) -- Usage example showing how callers use it -- What complexity it hides internally - -## Dependency Strategy - -Which category applies and how dependencies are handled: - -- **In-process**: merged directly -- **Local-substitutable**: tested with [specific stand-in] -- **Ports & adapters**: port definition, production adapter, test adapter -- **Mock**: mock boundary for external services - -## Testing Strategy - -- **New boundary tests to write**: describe the behaviors to verify at the interface -- **Old tests to delete**: list the shallow module tests that become redundant -- **Test environment needs**: any local stand-ins or adapters required - -## Implementation Recommendations - -Durable architectural guidance that is NOT coupled to current file paths: - -- What the module should own (responsibilities) -- What it should hide (implementation details) -- What it should expose (the interface contract) -- How callers should migrate to the new interface - - diff --git a/improve-codebase-architecture/SKILL.md b/improve-codebase-architecture/SKILL.md index 87d858b..575dc79 100644 --- a/improve-codebase-architecture/SKILL.md +++ b/improve-codebase-architecture/SKILL.md @@ -1,19 +1,30 @@ --- name: improve-codebase-architecture -description: Explore a codebase to find opportunities for architectural improvement, focusing on making the codebase more testable by deepening shallow modules. Use when user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more AI-navigable. +description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable. --- # Improve Codebase Architecture -Explore a codebase like an AI would, surface architectural friction, discover opportunities for improving testability, and propose module-deepening refactors as GitHub issue RFCs. +Surface architectural friction and propose deepening opportunities — refactors that consolidate shallow modules into deeper ones with smaller interfaces. The aim is testability and AI-navigability. -A **deep module** (John Ousterhout, "A Philosophy of Software Design") has a small interface hiding a large implementation. Deep modules are more testable, more AI-navigable, and let you test at the boundary instead of inside. +A **deep module** (Ousterhout, "A Philosophy of Software Design") has a small interface hiding a large implementation. Deep modules are easier to test at the boundary and easier for both humans and AI to navigate. + +This skill is _informed_ by the project's domain model — `CONTEXT.md` and any `docs/adr/`. The domain language gives names to good module boundaries; ADRs record decisions the skill should not re-litigate. + +See [CONTEXT-FORMAT.md](../domain-model/CONTEXT-FORMAT.md) and [ADR-FORMAT.md](../domain-model/ADR-FORMAT.md) for the file formats. ## Process -### 1. Explore the codebase +### 1. Explore -Use the Agent tool with subagent_type=Explore to navigate the codebase naturally. Do NOT follow rigid heuristics — explore organically and note where you experience friction: +Read existing documentation first: + +- `CONTEXT.md` (or `CONTEXT-MAP.md` + each `CONTEXT.md` in a multi-context repo) +- Relevant ADRs in `docs/adr/` (and any context-scoped `docs/adr/` directories) + +If any of these files don't exist, proceed silently — don't flag their absence or suggest creating them upfront. + +Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction: - Where does understanding one concept require bouncing between many small files? - Where are modules so shallow that the interface is nearly as complex as the implementation? @@ -25,52 +36,26 @@ The friction you encounter IS the signal. ### 2. Present candidates -Present a numbered list of deepening opportunities. For each candidate, show: +Present a numbered list of deepening opportunities. For each candidate: -- **Cluster**: Which modules/concepts are involved -- **Why they're coupled**: Shared types, call patterns, co-ownership of a concept -- **Dependency category**: See [REFERENCE.md](REFERENCE.md) for the four categories -- **Test impact**: What existing tests would be replaced by boundary tests +- **Cluster**: which modules/concepts are involved +- **Why they're coupled**: shared types, call patterns, co-ownership of a concept +- **Dependency category**: see [DEEPENING.md](DEEPENING.md) +- **Test impact**: what existing tests would be replaced by boundary tests + +**Use CONTEXT.md vocabulary when describing candidates.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" rather than "the FooBarHandler." + +**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction you noticed is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids. Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?" -### 3. User picks a candidate +### 3. Grilling loop -### 4. Frame the problem space +Once the user picks a candidate, drop into a grilling conversation. Walk the design tree with them — constraints, dependencies, the shape of the deepened module, what gets hidden, what tests survive. -Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: +Side effects happen inline as decisions crystallize: -- The constraints any new interface would need to satisfy -- The dependencies it would need to rely on -- A rough illustrative code sketch to make the constraints concrete — this is not a proposal, just a way to ground the constraints - -Show this to the user, then immediately proceed to Step 5. The user reads and thinks about the problem while the sub-agents work in parallel. - -### 5. Design multiple interfaces - -Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module. - -Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category, what's being hidden). This brief is independent of the user-facing explanation in Step 4. Give each agent a different design constraint: - -- Agent 1: "Minimize the interface — aim for 1-3 entry points max" -- Agent 2: "Maximize flexibility — support many use cases and extension" -- Agent 3: "Optimize for the most common caller — make the default case trivial" -- Agent 4 (if applicable): "Design around the ports & adapters pattern for cross-boundary dependencies" - -Each sub-agent outputs: - -1. Interface signature (types, methods, params) -2. Usage example showing how callers use it -3. What complexity it hides internally -4. Dependency strategy (how deps are handled — see [REFERENCE.md](REFERENCE.md)) -5. Trade-offs - -Present designs sequentially, then compare them in prose. - -After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not just a menu. - -### 6. User picks an interface (or accepts recommendation) - -### 7. Create GitHub issue - -Create a refactor RFC as a GitHub issue using `gh issue create`. Use the template in [REFERENCE.md](REFERENCE.md). Do NOT ask the user to review before creating — just create it and share the URL. +- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/domain-model` (see [CONTEXT-FORMAT.md](../domain-model/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist. +- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there. +- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../domain-model/ADR-FORMAT.md). +- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md). diff --git a/ubiquitous-language/SKILL.md b/ubiquitous-language/SKILL.md index 4a1d17f..35b649d 100644 --- a/ubiquitous-language/SKILL.md +++ b/ubiquitous-language/SKILL.md @@ -1,6 +1,7 @@ --- name: ubiquitous-language description: Extract a DDD-style ubiquitous language glossary from the current conversation, flagging ambiguities and proposing canonical terms. Saves to UBIQUITOUS_LANGUAGE.md. Use when user wants to define domain terms, build a glossary, harden terminology, create a ubiquitous language, or mentions "domain model" or "DDD". +disable-model-invocation: true --- # Ubiquitous Language