Navigability for AI Agents
An agent with a full context window still fails if the repo is a maze. Navigability is how quickly a cold session can locate the single authoritative place for a concern — and how hard it is to accidentally create a second one. Humans navigate by tribal knowledge; agents navigate by names, imports, and docs. Optimize for the latter.
What "navigable" means operationally
A repo is navigable when a fresh Claude Code session can, in under a few tool calls:
- Answer "where does billing proration live?" with one path (not three candidates).
- Answer "where do I add a new HTTP endpoint?" with a convention, not a guess.
- Avoid inventing a parallel helper because the real one was named
utils2or buried in an unrelated module.
| Signal | Navigable | Hostile |
|---|---|---|
| Feature code | src/invoices/ owns invoice HTTP + domain | Logic split across helpers/, services/, utils/invoice_stuff.ts |
| Cross-cutting | src/lib/money.ts is the only money helper | formatCents reimplemented in 4 feature folders |
| Entry points | src/api/routes.ts or framework convention documented in CLAUDE.md | Routes registered in 6 files with no index |
| Docs | ARCHITECTURE.md maps domains → directories | README is setup-only; architecture is oral |
See Repository Structure for layout patterns; this page is about the agent-facing properties of whatever layout you chose.
Naming that agents can grep
| Rule | Do | Don't |
|---|---|---|
| Name by domain + role | InvoiceProration, invoice_proration.ts | helper, manager, processor, stuff |
| One concept, one stem | Proration everywhere | Proration / ProRate / BillingAdjust |
| Match route to module | GET /invoices → invoices/ | GET /invoices implemented under billing/legacy/ |
| Avoid false friends | Don't name a DTO InvoiceService | Agents will put business logic in anything called *Service |
Put the naming rules in CLAUDE.md as greppable never-dos:
- Money math lives only in src/lib/money.ts — never reimplement cents conversion.
- New HTTP handlers go under src/api/<resource>/ — never add routes in src/lib/.
- Do not create files named utils.ts, helpers.ts, or common.ts — name the domain.Module boundaries agents respect
Agents follow import graphs more faithfully than prose architecture diagrams. Make the illegal import structurally awkward:
| Boundary rule | Enforcement that works with agents |
|---|---|
| Domain must not import API | Lint/import rule + CLAUDE.md never-do |
| No feature imports another feature's internals | Public barrel only; deep imports flagged in review |
Shared code earns its place in lib/ | "Promoted only when used by 2+ domains" — written down |
Without enforcement, agents will "just import" across layers to finish the task. With a lint rule, the agent hits a red gate and usually corrects course. See Architecture Erosion.
Index files and "where does X live" conventions
Barrel / index files help when they are curated public APIs, not dumping grounds:
| Pattern | Use when | Agent failure if misused |
|---|---|---|
src/domain/invoices/index.ts exports public types/functions | Clear module API | Barrel re-exports everything → agents import internals "through" the barrel anyway |
docs/map.md or ARCHITECTURE.md table: Domain → Path | Multi-package monorepos | Stale map → agent trusts the wrong path |
CLAUDE.md "Where things live" section (≤15 lines) | Every non-trivial repo | Missing section → agent searches and invents |
Example ARCHITECTURE.md fragment agents actually use:
## Where things live
| Concern | Path | Notes |
|---|---|---|
| HTTP routes | `src/api/<resource>/` | Register in `src/api/routes.ts` |
| Invoice domain | `src/domain/invoices/` | No Stripe imports here |
| Stripe adapter | `src/adapters/stripe/` | Only place that imports `stripe` |
| Money | `src/lib/money.ts` | Integer cents only |Preventing duplicated logic
Duplication is the #1 navigability killer under agents: they find a place that almost fits and copy it.
| Prevention | How |
|---|---|
| Single authoritative module | Document it; link from CLAUDE.md |
| Search-before-create rule | Prompt + CLAUDE.md: "rg for existing helpers before adding new ones" |
| Characterization of shared behavior | Tests in the shared module so copies drift loudly |
| Review grep | On PRs, search for near-duplicate function names |
Prompt fragment:
Before adding any shared helper, search the repo for existing implementations
(rg -i proration, cents, money). If one exists, extend it. If two exist,
STOP and report duplication — do not create a third.Anti-patterns agents exploit
| Anti-pattern | What the agent does | Countermeasure |
|---|---|---|
utils.ts / helpers.ts grab bags | Dumps new functions there or creates utils2.ts | Ban the names; require domain-named modules |
| Parallel "v2" packages | Creates invoices-v2/ beside invoices/ | Never-do + review reject; migrate or extend in place |
| Copy-paste adjacent feature | Clones credits/ to make debit/ with drift | Spec "extend shared X"; provide the path to extend |
God services/ layer | Puts everything in InvoiceService class | Prefer functions in domain modules; limit class size in CLAUDE.md |
| Undocumented side entrances | Adds a one-off script that bypasses domain rules | Scripts must call domain API; note in navigability map |
| Comments as architecture | Relies on stale "see Foo" comments | Executable structure + short map; delete lying comments |
| Generated + hand-edited mix | Edits generated clients in place | CLAUDE.md: never edit **/generated/** |
Navigability audit (run quarterly or after a messy epic)
- [ ] Fresh session: "Where does <core domain rule> live?" — one answer?
- [ ] `ARCHITECTURE.md` / map matches reality (spot-check 5 rows)
- [ ] No `utils.ts` / `helpers.ts` at repo root of src
- [ ] Import lint (or equivalent) still fails on a deliberate layer violation
- [ ] Duplicate-stem search: `rg "function formatCents"` → ≤1 definition
- [ ] CLAUDE.md "Where things live" ≤15 lines and accurate
- [ ] Last 10 agent PRs: count of drive-by new modules — trending down?Making navigability stick
- Write the map when the structure is still small (New Project).
- Encode never-dos in
CLAUDE.md— agents load them every session. - Enforce with gates where possible (import rules beat prose).
- Reject navigability regressions in review as hard as logic bugs — a second
money.tsis a permanent tax on every future agent run. - After recovery from a messy agent PR, update the map if the agent got lost for a structural reason — see Recovery.
Related
- Repository Structure — concrete layout options this page assumes.
- CLAUDE.md and AGENTS.md — where to put "where things live" and never-dos.
- Docs as Memory — keeping maps from going stale.
- Architecture Erosion — failure mode when navigability collapses.
- What Not to Delegate — boundary decisions stay human.