
Ai Codebase Deep Modules
- 73 installs
- 3 repo stars
- Updated June 29, 2026
- tristanmanchester/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
ai-codebase-deep-modules is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- ai-codebase-deep-modules
- AI & Agent Building
- AI-coding skill
Ai Codebase Deep Modules by the numbers
- 73 all-time installs (skills.sh)
- Ranked #5,587 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tristanmanchester/agent-skills --skill ai-codebase-deep-modulesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 73 |
|---|---|
| repo stars | ★ 3 |
| Last updated | June 29, 2026 |
| Repository | tristanmanchester/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
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:
1. The codebase matters more than the prompt. AI struggles when feedback is slow, structure is unclear, and dependencies are tangled. 2. 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. 3. Prefer deep modules. Lots of implementation behind a small, well-designed public interface. 4. 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.
1. 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
2. Prefer a single “verify” entrypoint:
make verify,just verify,npm run verify,./scripts/verify.sh
3. 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
4. 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.
1. List the product domains/features (aim for 3–10):
- e.g.
auth,billing,thumbnail-editor,video-editor,cms-forms
2. For each domain, identify:
- entrypoints (routes/controllers/handlers)
- data boundaries (models/schemas)
- external dependencies (APIs, DB, queues)
3. 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:
1. Define the public interface (small surface area):
- functions/classes/commands exposed
- public types/data contracts
- error/edge-case semantics
2. Define what is explicitly internal:
- helper functions, adapters, DB queries, parsing, etc.
3. Decide the dependency direction:
- Prefer:
domain → shared primitives - Avoid:
domain ↔ domaincross-imports
4. 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.
1. Write/identify contract tests for each module’s public API:
- behavioural checks
- key error cases
- side effects (DB writes, events emitted) via fakes/spies
2. Keep tests close to the interface:
- treat internals as replaceable
3. 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:
1. Create the new module folder and public interface (empty implementation). 2. Add contract tests (they will fail). 3. Add a thin adapter that wraps existing code (tests pass). 4. Move internals gradually behind the interface:
- keep exports stable
- delete old entrypoints only once usage is migrated
5. 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:
1. Current state summary (1–2 paragraphs) 2. Fast feedback loop (exact commands) 3. Module Map (table) 4. Proposed deep modules (list + responsibilities) 5. Interface specs (per module) 6. Filesystem changes (move plan) 7. Boundary enforcement (rules + tooling) 8. Testing strategy (contract tests first) 9. 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: 1. Identify verify loop (typecheck + unit tests) and how to run it per package. 2. Produce a module map (3–7 modules). 3. Propose deep modules with a clear public interface (index.ts, types.ts). 4. Recommend boundary enforcement via ESLint no-restricted-imports. 5. 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: 1. Identify dependency cycle and why it exists (shared types? shared DB code?). 2. Extract a deep module interface boundary:
authexportsgetCurrentUser(),requireAuth()billingdepends on those interfaces only (no deep imports)
3. Move shared primitives into shared/ or platform/ module. 4. 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:
- references/module-templates.md
- references/prompts.md
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
AI‑Friendly Refactor Plan (Deep Modules)
1. Current state summary
- Repo type:
- Language/tooling:
- Current pain:
2. Fast feedback loop
- Local:
...- CI:
...- Notes on speed / reliability:
3. Module Map
| Module | Responsibilities | Entrypoints | Key types | Current coupling risks |
|---|---|---|---|---|
| auth | ... | ... | ... | ... |
4. Proposed deep modules
auth: ...billing: ...video-editor: ...
5. Interface specs
Module: auth
Public API
login(...) -> ...requireAuth(...) -> ...
Invariants
- ...
Examples
- ...
6. Filesystem changes (move plan)
- Create:
- Move:
- Delete:
- Temporary shims:
7. Boundary enforcement
- Rule set:
- Tooling:
- Where configured:
8. Testing strategy
- Contract tests to add first:
- Integration tests to keep/trim:
- Missing coverage risks:
9. Incremental migration steps
1. ... 2. ... 3. ...
10. Rollback plan
- ...
Boundary enforcement patterns
The goal is simple: prevent cross-domain imports that make the codebase hard to navigate and refactor.
Start light (conventions), then add tooling as soon as drift appears.
Shared principles
1. One public entrypoint per module
- TS/JS:
src/<module>/index.ts - Python:
src/<module>/__init__.py
2. No external imports from internal folders
src/<module>/internal/**is off-limits
3. Explicit dependency direction
- Agree a small set of “platform/shared” modules
- Domain modules may depend on platform/shared, not on each other (unless explicitly allowed)
TypeScript / JavaScript
Option A — ESLint no-restricted-imports (pragmatic default)
Example idea (adjust paths to your repo):
- Allow:
import { login } from "@/auth" - Disallow:
import { hash } from "@/auth/internal/password"
Config sketch:
// .eslintrc.js
module.exports = {
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
// Block importing internals from anywhere
"**/internal/*",
"**/internal/**",
// Optional: block cross-feature imports (example)
// "@/auth/**" may only be imported via "@/auth"
],
},
],
},
};Add a path alias so the “public entrypoint only” rule is ergonomic:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}Then keep module entrypoints short and stable:
src/auth/index.tssrc/billing/index.ts
Option B — Packages with explicit exports
If you already use workspaces (packages/*), make each module a package and expose only the public surface.
Example:
packages/auth/
package.json # "exports" points to dist/index.js
src/index.ts # public API
src/internal/ # private implementationConsumers import @acme/auth only.
Option C — Architecture tests (when you need stronger guarantees)
Write a test that fails on forbidden imports (many libraries exist; also easy to script with grep/AST parsing).
Keep it simple:
- fail on
../auth/internal - fail on
src/auth/imports that aren’tsrc/auth/index
Python
Convention + Import Linter
Use a tool like import-linter (or a simple AST check) to enforce rules:
billingmay importplatformbut notauth.internal- only
billing/__init__.pyis imported from outside
If you don’t want extra tooling yet:
- keep
internal/as a convention - enforce via PR review and a lightweight CI grep check
Go
Use internal/ packages to make illegal imports impossible.
If you still have cross-domain coupling, it usually comes from:
- shared types in the wrong place
- shared DB access logic
- shared configuration
Fix by extracting a platform package or by inverting dependencies via interfaces.
JVM (Java/Kotlin)
Two solid options:
- ArchUnit tests: assert package dependency rules
- JPMS / Gradle conventions: split domains into modules
Even without tooling:
- keep “public API” classes in the top package
- keep internal classes package-private and under
.internal
Dealing with necessary cross-domain calls
Sometimes modules must interact. Prefer one of these:
1. Service interface inversion
billingdefinesUserLookupinterfaceauthimplements it- wire together in a composition root
2. Events
authemitsUserLoggedInbillingsubscribes- avoids direct imports
3. Shared primitives module
- move only truly generic types/utilities into
platform/(orshared/) - keep it small; avoid turning it into a junk drawer
Module templates for deep (greybox) modules
These templates are examples, not mandates. The key idea is always the same:
- A module has a small public interface
- Everything else is internal
- External code imports from the public interface only
- Tests lock behaviour at the boundary
TypeScript / JavaScript
Recommended structure (single repo)
src/
auth/
index.ts # public exports
types.ts # public types
internal/ # implementation (not imported from outside)
token.ts
password.ts
db.ts
__tests__/ # boundary/contract tests
auth.contract.test.tsPublic interface pattern
src/auth/index.ts:
export type { User, Session, AuthError } from "./types";
export { login, logout, requireAuth, getCurrentUser } from "./internal/public-api";src/auth/types.ts:
export type User = { id: string; email: string };
export type Session = { userId: string; token: string };
export type AuthError =
| { type: "invalid-credentials" }
| { type: "locked" }
| { type: "unknown"; message: string };Key rules:
index.tsshould be short and obvious.- Prefer exporting functions over exporting internal classes.
- Keep error semantics explicit (union types / result objects).
Result object pattern (optional)
For fragile boundaries, avoid throwing across module boundaries:
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };Python
Recommended structure
src/
billing/
__init__.py # public exports
types.py # public types (dataclasses / TypedDict)
internal/
invoices.py
stripe_adapter.py
db.py
tests/
test_billing_contract.pysrc/billing/__init__.py should expose the stable API:
from .types import Invoice, BillingError
from .internal.public_api import create_invoice, get_invoice
__all__ = ["Invoice", "BillingError", "create_invoice", "get_invoice"]Enforcement options:
- Keep
internal/as a convention, and enforce with import-linter. - In larger systems, package each module as its own distribution with explicit exports.
Go
Go gives you a built-in boundary mechanism:
billing/
billing.go # public package API
types.go
internal/
db/
stripe/
billing_test.go # contract tests at package levelAnything under internal/ cannot be imported by other packages outside the parent tree.
Java / Kotlin
Use packages + visibility:
src/main/java/com/acme/auth/
AuthService.java // public API
AuthTypes.java
internal/
JwtTokens.java
PasswordHasher.java
src/test/java/com/acme/auth/
AuthContractTest.javaPrefer:
publicfor interface surface- package-private for internals (no modifier)
- enforcement via build tooling / architecture tests (ArchUnit)
“Deep module” smell checks
A module is too shallow when:
- it is mostly re-exports of other modules
- it has many “helper” files but no cohesive interface
- consumers import lots of internals to “get work done”
A module is deep enough when:
- consumers can do their work via 3–12 exported functions/types
- most code lives behind the interface
- tests at the boundary make internal changes safe
Copy‑paste prompts for architecture work
These prompts are designed to be pasted into any agent chat.
THE EXACT PROMPT — Build a module map
Read this repository and produce a “Module Map” that matches how the product behaves.
- Group code into 3–10 domains/features/services.
- For each module: responsibilities, entrypoints, key data types, and current coupling risks.
- Identify cross-import cycles and “junk drawer” directories (utils/shared/helpers).
- Output a table and a short narrative summary.
THE EXACT PROMPT — Specify a deep module interface
For the module {module-name}, design a small public interface:
- List the 3–12 exported functions/types.
- Define inputs/outputs and error semantics.
- Provide 2 examples (happy path + one edge case).
- Define what is explicitly internal.
Keep the interface boring and stable.
THE EXACT PROMPT — Plan an incremental refactor (strangler)
Create an incremental refactor plan to introduce deep modules without a rewrite:
1) Create module folders + public entrypoints.
2) Add contract tests at the boundaries.
3) Wrap existing code behind the new interfaces until tests pass.
4) Move internals behind internal/ gradually.5) Add boundary enforcement (lint/arch tests).
Include checkpoints and rollback strategy.
THE EXACT PROMPT — Boundary enforcement options
Given this repo’s language/tooling, propose the lightest boundary enforcement that prevents:
- imports from **/internal/**- cross-domain imports that bypass module entrypoints
Include exact config snippets for lint rules and/or architecture tests.
THE EXACT PROMPT — Review this plan for realism
Review this architecture plan as a senior engineer.
- What is risky or ambiguous?
- What hidden dependencies might break?
- Are the modules too shallow or too broad?
- Where should we add contract tests first?
Propose a revised plan that is safer and more incremental.
Testing and feedback loops for greybox modules
A deep module becomes a greybox when you can trust its behaviour without reading its internals.
That trust comes from:
- a fast feedback loop (run locally + in CI)
- contract tests at the boundary
The “fast loop” checklist
Aim for a default verify command that:
- runs in minutes (ideally < 2–5 for unit-level signal)
- fails loudly with actionable messages
- is deterministic
If your repo has multiple packages/services:
- add
verify:fast(unit + typecheck) andverify:full(integration/e2e) - make the agent run
verify:fastfrequently while refactoring
Contract tests (module boundary tests)
Contract tests focus on what the module promises, not how it does it.
For each exported function/type:
- 1–3 happy path tests
- at least 1 meaningful error/edge case
- side effects verified via fakes/spies
Examples of boundary contract assertions:
- return value shape is stable
- errors are typed/structured and documented
- database writes happen exactly once
- events are emitted with the expected payload
A practical sequence for risky refactors
1. Freeze the surface
- Write down the public API you want.
- Add a thin wrapper over existing code to match that API.
2. Add contract tests
- If there are no tests, start with “golden master” tests:
- given input X, output matches snapshot Y
- useful for stabilising behaviour before improving it
3. Refactor internals
- move helpers into
internal/ - replace implementations gradually
4. Tighten tests over time
- replace snapshots with explicit assertions
- add internal tests only where justified
What to test where
- Boundary / contract tests (high value)
- live next to the module
- protect consumers
- enable internal rewrites
- Internal unit tests (selective)
- for complex algorithms
- for performance-sensitive code
- Integration tests (targeted)
- verify real DB/API wiring
- keep small; they’re slower and flakier
Making tests AI-friendly
Tests should:
- be runnable with one command
- use clear naming (“should return invalid-credentials when …”)
- include minimal fixture complexity
- avoid non-determinism (time, random, network)
When failures happen, error output should tell the agent:
- what broke
- where to look
- what invariant was violated
#!/usr/bin/env python3
"""
Scaffold a "deep module" folder with a small public interface.
This is optional helper tooling for the ai-codebase-deep-modules skill.
It aims to create the *shape* of a module quickly so you can iterate on the
public interface and contract tests first.
Usage examples:
python scripts/scaffold_deep_module.py --name auth --lang ts
python scripts/scaffold_deep_module.py --name billing --lang py --base-dir src
python scripts/scaffold_deep_module.py --name video-editor --lang ts --base-dir packages
python scripts/scaffold_deep_module.py --name video-editor --lang py # becomes video_editor/
Notes:
- This script does not enforce boundaries; use lint/arch rules for that.
- It is intentionally minimal and dependency-free.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import sys
def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists():
# Don't clobber existing work.
return
path.write_text(content, encoding="utf-8")
def scaffold_ts(module_dir: Path, name: str) -> list[Path]:
created: list[Path] = []
index_ts = module_dir / "index.ts"
types_ts = module_dir / "types.ts"
public_api_ts = module_dir / "internal" / "public-api.ts"
test_ts = module_dir / "__tests__" / f"{name}.contract.test.ts"
_write(
types_ts,
f"""// Public types for the `{name}` module.
export type {to_pascal(name)}Error =
| {{ type: "not-implemented" }};
""",
)
created.append(types_ts)
_write(
public_api_ts,
f"""import type {{ {to_pascal(name)}Error }} from "../types";
export type Result<T, E> =
| {{ ok: true; value: T }}
| {{ ok: false; error: E }};
export function notImplementedYet(): Result<never, {to_pascal(name)}Error> {{
return {{ ok: false, error: {{ type: "not-implemented" }} }};
}}
""",
)
created.append(public_api_ts)
_write(
index_ts,
f"""// Public entrypoint for the `{name}` module.
// External code should import from here only.
export type {{ {to_pascal(name)}Error }} from "./types";
export type {{ Result }} from "./internal/public-api";
export {{ notImplementedYet }} from "./internal/public-api";
""",
)
created.append(index_ts)
_write(
test_ts,
f"""import {{ notImplementedYet }} from "../index";
test("notImplementedYet returns a structured error", () => {{
const result = notImplementedYet();
expect(result.ok).toBe(false);
if (!result.ok) {{
expect(result.error.type).toBe("not-implemented");
}}
}});
""",
)
created.append(test_ts)
return created
def scaffold_py(module_dir: Path, package_name: str, display_name: str) -> list[Path]:
created: list[Path] = []
init_py = module_dir / "__init__.py"
types_py = module_dir / "types.py"
public_api_py = module_dir / "internal" / "public_api.py"
test_py = module_dir / "tests" / f"test_{package_name}_contract.py"
type_name = to_pascal(display_name) + "Error"
_write(
types_py,
f"""\"\"\"Public types for the `{display_name}` module.\"\"\"
from __future__ import annotations
from typing import Literal, TypedDict, Union
class NotImplementedErrorType(TypedDict):
type: Literal["not-implemented"]
{type_name} = Union[NotImplementedErrorType]
""",
)
created.append(types_py)
_write(
public_api_py,
f"""from __future__ import annotations
from typing import Literal, TypedDict
from ..types import {type_name}
class Result(TypedDict):
ok: Literal[False]
error: {type_name}
def not_implemented_yet() -> Result:
return {{"ok": False, "error": {{"type": "not-implemented"}}}}
""",
)
created.append(public_api_py)
_write(
init_py,
f"""\"\"\"Public entrypoint for the `{display_name}` module.\"\"\"
from .types import {type_name}
from .internal.public_api import not_implemented_yet
__all__ = [
"{type_name}",
"not_implemented_yet",
]
""",
)
created.append(init_py)
_write(
test_py,
f"""from {package_name} import not_implemented_yet
def test_not_implemented_yet_returns_structured_error():
result = not_implemented_yet()
assert result["ok"] is False
assert result["error"]["type"] == "not-implemented"
""",
)
created.append(test_py)
return created
def to_pascal(s: str) -> str:
return "".join(part.capitalize() for part in s.replace("_", "-").split("-") if part)
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--name", required=True, help="Module name (kebab-case recommended).")
parser.add_argument("--lang", choices=["ts", "py"], required=True, help="Language template to use.")
parser.add_argument("--base-dir", default="src", help="Base directory inside repo (default: src).")
parser.add_argument("--root", default=".", help="Repo root (default: current directory).")
args = parser.parse_args(argv)
root = Path(args.root).resolve()
base_dir = root / args.base_dir
if args.lang == "py":
# Python packages must be valid identifiers; normalise kebab-case to snake_case.
package_name = args.name.replace("-", "_")
module_dir = base_dir / package_name
created = scaffold_py(module_dir, package_name=package_name, display_name=args.name)
else:
module_dir = base_dir / args.name
created = scaffold_ts(module_dir, args.name)
print(f"Scaffolded module at: {module_dir}")
for p in created:
if p.exists():
print(f" - {p.relative_to(root)}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))