ai-codebase-deep-modules
AI Codebase Deep Modules
Turn “a web of shallow, cross-importing files” into a codebase that is easy for AI (and humans) to navigate, change, and test.
This skill is built around four ideas:
- The codebase matters more than the prompt. AI struggles when feedback is slow, structure is unclear, and dependencies are tangled.
- Match the filesystem to the mental model. Group code the way you think about it (features/domains/services), not as a grab-bag of utilities.
- Prefer deep modules. Lots of implementation behind a small, well-designed public interface.
- Treat deep modules as greyboxes. Lock behaviour with tests at the boundary; internal code becomes replaceable.
When to use this skill
Use this skill when the user wants any of the following:
- Refactor an existing repo to be more navigable and safer for AI-assisted coding
- Introduce/strengthen module boundaries, reduce coupling, or eliminate “spaghetti imports”
- Restructure the repo by feature/domain (a “map you hold in your head” reflected on disk)
- Define service/module interfaces, public APIs, and “only import from here” rules
- Build fast feedback loops (tests, typecheck, lint) so AI can verify changes quickly
- Plan a refactor with incremental steps, acceptance criteria, and tests
Do not use this skill for:
- One-off debugging of an isolated error (use normal debugging / code review)
- Purely stylistic refactors with no boundary or testing implications
- Writing greenfield code where the user already has a clear modular architecture (unless they want a module template)
Inputs this skill expects (minimal)
If available, ask for or infer:
- Language/runtime (TS/JS, Python, Go, Java/Kotlin, etc.)
- How to run the fastest meaningful check (unit tests, typecheck, lint, build)
- The top 3–7 “chunks” of product behaviour (domains/features/services)
- Any hard constraints (monorepo tooling, existing packages, deployment boundaries)
If the user hasn’t provided this, do not stall. Make best-effort guesses by inspecting:
package.json,pyproject.toml,go.mod,pom.xml,build.gradle,Makefile,justfilesrc/,app/,packages/,services/,modules/- existing test folders and CI configs
Workflow
Step 0 — Establish the feedback loop (non-negotiable)
Goal: ensure there is a fast “did it work?” loop before and during refactors.
- Identify the quickest command that provides signal:
- Typecheck:
tsc -p tsconfig.json - Unit tests:
npm test,pytest -q,go test ./... - Lint:
eslint .,ruff check,golangci-lint run
- Typecheck:
- Prefer a single “verify” entrypoint:
make verify,just verify,npm run verify,./scripts/verify.sh
- If tests are missing, propose the smallest viable starting point:
- Smoke tests for core flows
- Contract tests for the boundaries you’re about to introduce
- If the loop is slow, propose speed-ups before large refactors:
- Run only impacted packages
- Split unit vs integration tests
- Cache dependencies in CI
Deliverable: a short “Feedback loop” section with the exact commands and expected outputs.
Step 1 — Reconstruct the mental map of the codebase
Goal: identify the natural groupings that already exist in the product.
- List the product domains/features (aim for 3–10):
- e.g.
auth,billing,thumbnail-editor,video-editor,cms-forms
- e.g.
- For each domain, identify:
- entrypoints (routes/controllers/handlers)
- data boundaries (models/schemas)
- external dependencies (APIs, DB, queues)
- Capture the current pain:
- “Where do people get lost?”
- “What breaks when we change X?”
- “Where are imports crossing domains?”
Deliverable: a Module Map (table) with: domain, responsibilities, key files, current coupling risks.
Step 2 — Design deep modules (few, chunky, stable interfaces)
Goal: reduce the number of things the agent must keep in working memory.
For each domain/module candidate:
- Define the public interface (small surface area):
- functions/classes/commands exposed
- public types/data contracts
- error/edge-case semantics
- Define what is explicitly internal:
- helper functions, adapters, DB queries, parsing, etc.
- Decide the dependency direction:
- Prefer:
domain → shared primitives - Avoid:
domain ↔ domaincross-imports
- Prefer:
- Keep the interface boring and predictable:
- stable names
- minimal parameters
- explicit return types / result objects
Deliverable: an Interface Spec for each deep module:
- Public API (signatures)
- Invariants (pre/post conditions)
- Examples (happy path + one edge case)
See: references/module-templates.md
Step 3 — Align the filesystem to the map (progressive disclosure)
Goal: make it obvious where to look.
Default rule: outside code imports only from a module’s public entrypoint.
Recommended structure (adapt per language):
src/<module>/index.*(public exports)types.*(public types)internal/(implementation details; not imported from outside)__tests__/ortests/(contract tests for the public API)
If the repo uses packages, prefer packages/<module>/ with explicit exports.
Deliverable: a “Move plan” listing:
- directories to create
- files to move
- import paths to update
- temporary compatibility shims (if needed)
Step 4 — Make modules greyboxes with boundary tests
Goal: you shouldn’t need to understand internals to trust behaviour.
- Write/identify contract tests for each module’s public API:
- behavioural checks
- key error cases
- side effects (DB writes, events emitted) via fakes/spies
- Keep tests close to the interface:
- treat internals as replaceable
- Only add internal unit tests where:
- performance-critical logic needs tight coverage
- tricky algorithms deserve direct tests
Deliverable: test plan + initial contract test skeletons.
See: references/testing-and-feedback.md
Step 5 — Enforce boundaries (so the architecture stays true)
Goal: prevent the codebase from drifting back into a web.
Pick the lightest viable enforcement:
- Conventions + code review (baseline)
- Lint rules (TS/JS:
no-restricted-imports, ESLint boundary plugins) - Architecture tests (assert “module A cannot import module B”)
- Language-level boundaries (Go
internal/, Rustpub(crate), Java modules)
Deliverable: an “Enforcement” section with the exact rules and where to configure them.
See: references/boundary-enforcement.md
Step 6 — Refactor incrementally (strangler pattern)
Goal: avoid giant-bang rewrites.
Suggested sequence:
- Create the new module folder and public interface (empty implementation).
- Add contract tests (they will fail).
- Add a thin adapter that wraps existing code (tests pass).
- Move internals gradually behind the interface:
- keep exports stable
- delete old entrypoints only once usage is migrated
- Repeat module-by-module.
Deliverable: a stepwise refactor plan with checkpoints and rollback options.
Output format (what to produce)
When this skill is activated, produce a structured plan using this outline:
- Current state summary (1–2 paragraphs)
- Fast feedback loop (exact commands)
- Module Map (table)
- Proposed deep modules (list + responsibilities)
- Interface specs (per module)
- Filesystem changes (move plan)
- Boundary enforcement (rules + tooling)
- Testing strategy (contract tests first)
- Incremental migration steps (with checkpoints)
Optional: copy the template from assets/architecture-plan-template.md.
Examples
Example 1 — Broad request
User says: “Make our TypeScript monorepo more AI-friendly. It’s hard to find things and tests are slow.”
Actions:
- Identify
verifyloop (typecheck + unit tests) and how to run it per package. - Produce a module map (3–7 modules).
- Propose deep modules with a clear public interface (
index.ts,types.ts). - Recommend boundary enforcement via ESLint
no-restricted-imports. - Add contract tests for each module.
Result: a concrete refactor plan and initial skeletons that can be executed incrementally.
Example 2 — Specific boundary problem
User says: “Auth imports billing and billing imports auth. We keep breaking things.”
Actions:
- Identify dependency cycle and why it exists (shared types? shared DB code?).
- Extract a deep module interface boundary:
authexportsgetCurrentUser(),requireAuth()billingdepends on those interfaces only (no deep imports)
- Move shared primitives into
shared/orplatform/module. - Add an architecture rule to prevent the cycle returning.
Result: cycle removed, boundaries enforced, behaviour locked by tests.
Troubleshooting
Skill feels too “high level”
Use the template and references to get concrete:
Refactor is risky / unknown behaviour
Prioritise greybox contract tests first:
- freeze behaviour at the public interface
- only then move internals
Boundaries are hard to enforce in TS/JS
Start with lint rules and path conventions; add architecture tests if needed. See: references/boundary-enforcement.md