Project map — PROJECTMAP.md
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
The problem it solves. Without a map, every session rediscovers the same repository: the same
find, the same grep, the same reads of files that turn out not to be the one. It is paid in
full every time, and the context fills with exploration noise instead of work.
The risk it introduces. A stale map is worse than no map: nobody checks it, everybody
believes it. Everything below exists so the map does not lie.
1. Scope and triggers
This skill fires automatically. It is not opt-in.
- Starting work in any repository: read
PROJECTMAP.md before exploring anything. If it
does not exist, create it before the first substantial task — not at the end, not "if there
is time". Creating it is the cheapest exploration you will ever do, because you were going to pay
for it anyway, unindexed.
- It exists but contradicts the repo, or is stale (§5 detects that in one command).
- You catch yourself searching for where something lives. That surprise is the signal: what you
just learned goes into the map, in this turn.
- Structure changes: new directory, moved module, different build/test command, new convention.
Updated in the same turn, never "later".
- You hand work over to another session or a subagent: the map is the cheapest handover there is.
What counts as "substantial": anything beyond answering a question about a file already open. A
one-line fix does not require a map; changing behaviour, adding a feature, debugging or reviewing
does.
Not applicable: see load-expertise (step ② of the same routine: this one answers where
things are in the repo, that one answers under what criteria they get changed — a map row is a
location, never a decision), knowledge-management-standards (documentation for humans: ADRs,
runbooks, wiki, who maintains what — the map is not product documentation and does not replace a
README),
claude-code-skills-standards (authoring skills and CLAUDE.md: the map describes the repo, the
CLAUDE.md sets how work is done in it — if in doubt, the rule goes to CLAUDE.md, the fact goes
to the map), software-architecture-patterns-standards (deciding the architecture; here it is only
described, not judged), code-review-standards (reviewing the change), git-workflow-standards
(history and branches).
2. Default decisions
| Decision |
Default |
Why |
| Name and location |
PROJECTMAP.md at the repo root |
Predictable; any session finds it without searching |
| Size |
As small as it can be while still answering "where is X?" — see §2.1 |
The cap is usefulness, not a line count |
| Versioning in your own repo |
Commit it, if the team wants it |
It pays off across people and sessions |
| Versioning in someone else's repo |
Do NOT commit: .git/info/exclude |
That exclusion is local; .gitignore is versioned and touching it dirties another team's repo |
| Monorepo |
One root map plus one per large package |
A single 800-line map gets read by nobody, including you |
| Generation state |
Header with date and short HEAD SHA |
Without it, staleness cannot be detected |
| Language |
The repository's |
Consistency with code, docs and issues |
2.0 The reader is the model, not a person
Write this file for the agent that will consume it, not for a human onboarding. Everything else
in this section follows from that, and getting it backwards is why most maps are useless:
- Optimise for lookup, not for reading. Stable table shapes, exact identifiers,
path:line
anchors, one fact per row. No narrative, no "as we saw above", no motivational prose. A sentence
that would be cut from a README for being dry is usually the right line here.
- Density beats brevity. The cost is context tokens weighed against tool calls saved — not a
reader's patience. A 600-line map that removes twenty
greps per session is cheap. A 120-line map
that removes none is expensive at any size.
- Ambiguity is the real enemy, not length.
handles auth is noise; POST /session → src/auth/session.ts:BeginSession is a lookup. Prefer the fully qualified name over the readable
paraphrase, always.
- Be greppable. Keep identifiers verbatim as they appear in the code, so a search for the symbol
finds both the map row and the definition. Do not pretty-print, pluralise or translate names.
- A human may read it, and that is fine. But no line earns its place by being nice to read —
only by answering a question that would otherwise cost a tool call.
- It is consumed with one
Read, whole — never grepped, paged with sed or sampled. The size
ceilings in §2.1 exist precisely so a single pass is affordable; searching inside it is the
behaviour the map was built to remove. A matched line arrives without the section that gave it
meaning — which heading it sat under, which minefield qualified it — so the reader guesses or
searches again and pays twice for a worse answer. A map being grepped is a map that failed:
either it is too long for one pass, or its consumer was never given the fragment it needed
(§4.6). Fix the map or fix the routing; do not normalise the grep.
sed/awk never touch it at all. On a governing document that is editing, and edits go
through the single writer that applies the deltas (§4.7) — never through whoever happened to open
it last.
2.1 How deep, really
The old rule of thumb was ~150 lines. That was a human-readability heuristic and it does not
apply (§2.0). The governing constraint is different: every line must save a search, and every
line must be true. Depth is bounded by what can be kept honest, not by what fits on a screen.
- Content repository (each file is a self-contained unit looked up by name — a skill catalogue,
a docs site, a policy set, a notebook collection) → the per-file table is the map: file, what
it decides or contains, size. This is a database index, and indexes list rows. Not optional and
not deferrable: without it the map cannot answer "which file decides X?", every session falls
back to
grep, and the map is doing none of its job. Too large to write by hand is an argument
for generating it (§4.5), never for omitting it.
- Code repository → index the public surface, not the directory tree, and go to symbol level.
Directories and entry points alone answer "where do I go to change something" and leave the
expensive question — "where is the thing that does X?" — to
grep, which is the failure this
whole skill exists to prevent. See §2.3 for what to index and how to keep it true.
- The test is the same either way: would this line save a search? If not it is decoration — cut
it, whatever the total length.
2.3 Code repositories: index the surface, and generate it
The file is not the unit of lookup in code. src/utils/helpers.ts answers nothing; a dumped
tree is the same problem with more tokens. What gets looked up is the surface: the names and
routes through which the system is entered and extended.
Index these, whichever the project has — one row each, with a path:line anchor:
| Surface |
Row shape |
| HTTP/RPC routes |
method + path → handler symbol → file:line |
| CLI commands / subcommands |
command → entry symbol → file:line |
| Background jobs, cron, queue consumers |
trigger/topic → consumer symbol → file:line |
| Events published and consumed |
event name → producer, consumers |
| Public exports of each module/package |
symbol → file:line, one line on what it owns |
| Domain types and their invariants |
type → owning module → where it is validated |
| Persistence |
table/collection → owning module → migration directory |
| Config and feature flags |
key/env var → where it is read → default |
| Extension points |
interface/hook → implementations |
Two rules that make the depth survivable:
- Generate it from the code, never write it by hand. Hand-written symbol detail in an active
codebase is fiction within a sprint, and a confidently wrong map is worse than no map. Extract
from what cannot drift: the router table, the CLI registry, the DI container, exported symbols,
migration filenames, the OpenAPI document,
ctags/LSP output, the framework's own manifest.
Regenerate in CI or a pre-commit hook and fail the build when the map and the code disagree —
that gate is what converts the map from documentation into an invariant.
- Split before it becomes one unreadable file: a root map with the cross-cutting picture, plus
one generated map per package/service. Monorepos get one per workspace, not one of 4,000 rows.
What stays hand-written is exactly what a generator cannot know and what does not churn: why a
boundary exists, which module must not depend on which, the minefields, the invariants, the
"looks wrong but is deliberate" notes. Generated content answers where; hand-written content
answers why and careful. Never mix them in the same section — the generated part is
overwritten, and hand-written notes inside it are lost on the next run.
2.2 Locations and invariants, never derived state
The single most common way a map rots: storing the result of a measurement instead of the way to
obtain it.
- Belongs in the map: where something lives, what a directory is for, which command builds or
tests, what is invariant, what is a minefield. These change when the repository is restructured —
rarely.
- Never belongs in the map: counts, line totals, percentages of progress, "N files pending",
test-pass numbers, sizes that move with the work. These are stale the next time anybody commits,
and a wrong number is worse than an absent one because it gets quoted.
- The rule that resolves it: store the command, not its output.
Pending items: run scripts/pending.sh ages well; Pending items: 64 is a lie by tomorrow.
- Corollary for the reader: a map removes the need to search, never the need to measure.
Locating a file is a map question; "how many are left?" is a command. Do not blame the map for the
second, and do not try to make it answer it.
- The only figures that may appear are the ones in the header (generation date and short SHA), and
they exist precisely so staleness is detectable.
3. What it contains (and in what order)
Order matters: what gets consulted most goes on top.
# Map of <project>
> Generated <YYYY-MM-DD> against `<short-sha>`. If anything here does not match the repo, **the repo
> wins**: fix the line and move on. Maintained per the `project-map` skill.
## I want to change… → go to…
| To… | Go to | Note |
|---|---|---|
| Add an endpoint | `src/api/routes/` | Central registry at `src/api/router.ts:40` |
| Change the DB schema | `migrations/` | Never by hand in `models/`: it is generated |
## Structure
- `src/` — production code. `src/core/` depends on nothing in `src/adapters/`.
- `tests/` — unit next to the code; integration ones here.
## Entry points
- CLI: `src/cli/main.py` → `cmd_*` per subcommand.
- HTTP: `src/api/app.ts`, listens on `:8080`.
## Commands
| What | Command | Verified |
|---|---|---|
| Build | `make build` | 2026-08-05 |
| Tests | `pytest -q` | 2026-08-05 |
## Conventions and invariants
- Tests live next to the module, `_test` suffix.
- No DB access outside `repositories/`.
## Minefields
- `src/legacy/billing.py`: no tests, real billing depends on it. Read `docs/adr/0007` first.
## Out of the map
`node_modules/`, `dist/`, `.venv/`, generated files (`*_pb2.py`).
The "I want to change… → go to…" table is the heart of the file. It is the one that saves the
greps. If you only have time for one section, it is that one. A directory tree without it is
decorative.
For a content repository, add the per-file index (§2.1) as its own section, generated rather
than hand-written wherever possible: derive each row from the file's own front matter, title or
first heading, so it cannot drift into fiction.
4. How to generate it cheaply
Do not read the whole repository: index, do not copy.
- Inventory, not dump.
git ls-files grouped by first and second path level gives the real
shape without listing 10,000 paths. What is not in git (generated, ignored) stays out unless it
is a minefield.
- Read the files that are already maps:
README, CONTRIBUTING, CLAUDE.md/AGENTS.md,
Makefile/justfile, package.json scripts, pyproject.toml, docker-compose.yml, CI under
.github/workflows/. Commands come from there, not from memory.
- Entry points by ecosystem convention:
main, index, app, cmd/, bin/,
[project.scripts], the Dockerfile entrypoint, compose services:.
- Minefields from history: the most-churned files (
git log --format= --name-only | sort | uniq -c | sort -rn | head) show where it hurts. Cross that with missing tests and the minefield
list writes itself.
- Generate the index with a script, not by hand (§2.1, §2.3) — a loop that extracts each file's
own summary line, or each module's exported symbols, is exact, repeatable and free of invention.
In a content repository the source is the file's own front matter or first heading; in a code
repository it is the router table, the CLI registry, exported symbols,
ctags/LSP output or the
OpenAPI document. Commit the generator next to the map: a generated file whose generator is
lost becomes hand-written again on the first edit.
- Gate it: regenerate in CI or a pre-commit hook and fail when the map and the code disagree.
Without that gate the map is documentation and it rots; with it, it is an invariant.
- Delegable: in a large repo an exploration subagent returns the draft and you verify it. That
is precisely the cost the map stops you repeating.
4.6 Slice the map into the agent prompts — the slice is the partition
When work is delegated to a fleet, paste into each agent's prompt exactly the fragment of the map
that its slice needs, and nothing else. Every tier repeats this downwards: an agent that fans out
passes each of its subagents only the part of its own fragment that the subagent needs. The slice
narrows monotonically as you descend.
This is not only context economy. The fragment is how the boundary is communicated: an agent
holding only its own rows has its territory written down, so "do not touch anything outside your
slice" stops depending on it remembering. Handing the whole map to everyone does the opposite — it
shows each agent the files it must not touch, and pays tokens ×N for the privilege.
Two traps that make the difference between this working and it causing an incident:
- Carry the minefields that touch those files, even when they look out of scope. The slice is
cut by relevance, never by section: a warning about a file the agent will edit belongs in its
fragment whichever heading it lives under. An agent that walks into a trap you had documented and
did not forward is your failure, not its.
- The slice inherits the map's honesty, amplified. A wrong line handed to one reader is one
mistake; handed to eight agents it is eight, made simultaneously and confidently. Regenerate the
fragments from the current map on every run — never from a cached copy of a previous one.
- The prompt is the only channel, so agents never open the map themselves. If they may fetch what
the fragment lacks, the fragment stops being a boundary and becomes a suggestion — and the routing
defect that produced the gap is hidden instead of fixed. An agent whose fragment is missing,
incomplete or contradicted by disk reports that and works from the files; the tier above amends
the brief in flight. That is what keeps the boundary absolute while still being survivable: an
agent starved of context has a way out that does not involve reading rows it was never meant to
see, and the gap gets fixed once, above, for every sibling at the same time.
Make the surprise rule (§5) travel too: require each agent to report back when the fragment
failed it — a path that does not exist, a command that does not run, a convention that turned out
different. That turns the fleet into a distributed verification pass over the map: they find the
errors in parallel, and you fix them once, at the top.
4.7 Close the loop: every agent returns a map delta
A fleet rots the map faster than solo work does — eight agents move, rename and create things in
parallel and none of it reaches the index. So the return leg is mandatory, not a courtesy:
- Every agent ends its report with a
map delta section: what it changed that the map names or
should name, what the map got wrong, what it had to discover because the map did not say. Empty is
a valid delta and is stated explicitly — silence is indistinguishable from forgetting.
- Two kinds, and both are wanted: (a) the map was wrong (surprise rule, §5) and (b) the work
changed the repo (same-turn rule, §5). The first fixes the past, the second records the present.
- Each tier aggregates and deduplicates its children's deltas before passing its own upwards.
Eight agents touching one subsystem produce one merged delta, not eight overlapping ones.
- Agents never edit the map. It is a single file: N writers on it violates disjoint ownership and
produces lost updates or conflicts. They emit the delta; the top applies it, in one pass, in the
same turn the fleet lands — which is how the same-turn rule survives delegation instead of being
quietly voided by it.
- Prefer deltas stated as an edit, not as prose: "row
X → path is now Y", "new entry needed:
Z owns …", "minefield stale: the .htaccess warning no longer applies". A delta you have to
interpret is a delta you will apply wrong.
A command you have not run is not written as verified. Either run it, or mark it Declared gap.
5. Keeping it honest
Surprise rule: every time the map fails you — a path that no longer exists, a command that
does not work — you fix that line there and then. It is the only maintenance that survives.
Same-turn rule: if your change moves, creates or renames something the map names, you update
the map in that turn. A map updated "at the end" is not updated.
Mechanical path check (cheap, run it when picking work back up):
grep -oE '`[a-zA-Z0-9_./-]+/[a-zA-Z0-9_./-]*`' PROJECTMAP.md | tr -d '`' |
while read -r p; do [ -e "$p" ] || echo "DEAD PATH: $p"; done
Drift against the code: compare the header SHA with git rev-parse --short HEAD. Many commits
of difference do not invalidate the map — structure moves slowly — but they do mean looking at
git diff --stat <map-sha>..HEAD -- '*/' for new directories.
Working tree vs. HEAD: with many uncommitted changes, say so in the header and state which
figures come from disk and which from HEAD. A map that silently mixes both misleads.
If the map contradicts the repository, the repository wins. Always. The map is an index, not a
source of truth.
6. Prohibitions
- ❌ Starting substantial work in a repo without a map when creating one was possible. The
exploration you are about to do is the map: not writing it down is choosing to pay twice.
- ❌ Copying code content into the map (signatures, function bodies, full schemas). Duplication
guarantees divergence. Cite
file:line, do not transcribe.
- ❌ A hand-written file dump. Not because it is long — because it is unmaintainable and will
lie. Depth in a code repository is legitimate and wanted, but generated from the code and gated
in CI (§2.3). The prohibition is on the hand-written part, not on the detail.
- ❌ Indexing a code repository by path instead of by surface (§2.3): a
tree with comments
answers "where do I go", never "where is the thing that does X" — which is the search you are
paying for.
- ❌ Hand-written notes inside a generated section. They are silently destroyed on the next
regeneration. Keep where (generated) and why/careful (hand-written) in separate sections.
- ❌ Writing what you have not verified. No "the tests are probably run with…". Either check it
or mark it
Declared gap.
- ❌ Storing a measurement instead of the command that produces it (§2.2): counts, totals,
progress percentages. They are stale on the next commit and they get quoted as if they were not.
- ❌ Shipping a content repository's map without the per-file index (§2.1) and calling it done.
That table is the whole point; everything else is preamble.
- ❌ Letting it expire in silence. If you detect it is stale and cannot fix it whole, mark the
affected section as unreliable instead of leaving it looking valid.
- ❌ Putting doctrine in the map (how work is done, what is forbidden): that belongs in
CLAUDE.md. The map says where things are, not how they must be done.
- ❌ Secrets, identifiable internal paths, IPs, production hostnames. Maps tend to end up
versioned; treat one as public code.
- ❌ Creating
PROJECTMAP.md and never looking at it again. A map not read at the start saves
nothing: the habit is reading it before exploring, not after.
- ❌ Committing it in someone else's repository without permission. It goes in
.git/info/exclude, which is local.
7. Long-term sustainability
- The map is disposable and regenerable: if a large refactor invalidates it, regenerate from
scratch (§4). Its history is not worth preserving.
- Growth is not the failure mode; drift is. Do not trim a map because it got long — trim it
because a section stopped being consulted or stopped being true. When it does need splitting, split
by package/service (§2.3), never by truncating detail. The one thing that always goes first is a
hand-maintained full tree: maximum length, minimum answers.
- In repos with rich
AGENTS.md/CLAUDE.md, do not duplicate: link to them.
8. Mandatory web verification
- Against the repository, always: paths exist (§5), commands run, entry points start. If the
map contradicts the repo, the repo wins.
- Against the web, only for external things the map cites: build tool names and versions,
package-manager commands, the canonical location of a framework's config file. Those expire and
are not fixed from memory.
If the web contradicts this document, the web wins — flag the discrepancy.
1---2name: project-map3description: Build and maintain PROJECTMAP.md, the orientation index of whatever repository you are working in, so the same grep/find/read is never paid for twice. Use at the very start of work in ANY repository - before the first substantial task - whenever PROJECTMAP.md is missing, whenever it is stale or contradicted by the repo, whenever you catch yourself searching for where something lives, whenever structure changes (new directory, moved module, different build/test command, new convention), and whenever you hand work over to another session or a subagent. Covers what goes in the map, what must never go in it, how to size it for a code repo versus a content repo, how to generate it cheaply with a script instead of by hand, how to keep it honest, and where to put it so it does not pollute a repository that is not yours.4---56# Project map — `PROJECTMAP.md`78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910> **The problem it solves.** Without a map, every session rediscovers the same repository: the same11> `find`, the same `grep`, the same reads of files that turn out not to be the one. It is paid in12> full every time, and the context fills with exploration noise instead of work.13>14> **The risk it introduces.** A stale map is **worse than no map**: nobody checks it, everybody15> believes it. Everything below exists so the map does not lie.1617## 1. Scope and triggers1819**This skill fires automatically. It is not opt-in.**2021- **Starting work in any repository**: read `PROJECTMAP.md` **before exploring anything**. If it22 does not exist, **create it before the first substantial task** — not at the end, not "if there23 is time". Creating it is the cheapest exploration you will ever do, because you were going to pay24 for it anyway, unindexed.25- It exists but **contradicts the repo**, or is stale (§5 detects that in one command).26- **You catch yourself searching for where something lives.** That surprise is the signal: what you27 just learned goes into the map, in this turn.28- **Structure changes**: new directory, moved module, different build/test command, new convention.29 Updated **in the same turn**, never "later".30- **You hand work over** to another session or a subagent: the map is the cheapest handover there is.3132What counts as "substantial": anything beyond answering a question about a file already open. A33one-line fix does not require a map; changing behaviour, adding a feature, debugging or reviewing34does.3536**Not applicable**: see `load-expertise` (step ② of the same routine: **this one answers *where*37things are in the repo, that one answers *under what criteria* they get changed** — a map row is a38location, never a decision), `knowledge-management-standards` (documentation for humans: ADRs,39runbooks, wiki, who maintains what — the map is **not** product documentation and does not replace a40README),41`claude-code-skills-standards` (authoring skills and `CLAUDE.md`: **the map describes the repo, the42`CLAUDE.md` sets how work is done in it** — if in doubt, the rule goes to `CLAUDE.md`, the fact goes43to the map), `software-architecture-patterns-standards` (deciding the architecture; here it is only44**described**, not judged), `code-review-standards` (reviewing the change), `git-workflow-standards`45(history and branches).4647## 2. Default decisions4849| Decision | Default | Why |50|---|---|---|51| Name and location | `PROJECTMAP.md` at the repo root | Predictable; any session finds it without searching |52| Size | **As small as it can be while still answering "where is X?"** — see §2.1 | The cap is usefulness, not a line count |53| Versioning in your own repo | Commit it, if the team wants it | It pays off across people and sessions |54| Versioning in **someone else's repo** | **Do NOT commit**: `.git/info/exclude` | That exclusion is **local**; `.gitignore` is versioned and touching it dirties another team's repo |55| Monorepo | One root map plus one per large package | A single 800-line map gets read by nobody, including you |56| Generation state | Header with date and **short HEAD SHA** | Without it, staleness cannot be detected |57| Language | The repository's | Consistency with code, docs and issues |5859### 2.0 The reader is the model, not a person6061**Write this file for the agent that will consume it, not for a human onboarding.** Everything else62in this section follows from that, and getting it backwards is why most maps are useless:6364- **Optimise for lookup, not for reading.** Stable table shapes, exact identifiers, `path:line`65 anchors, one fact per row. No narrative, no "as we saw above", no motivational prose. A sentence66 that would be cut from a README for being dry is usually the right line here.67- **Density beats brevity.** The cost is context tokens weighed against tool calls saved — not a68 reader's patience. A 600-line map that removes twenty `grep`s per session is cheap. A 120-line map69 that removes none is expensive at any size.70- **Ambiguity is the real enemy, not length.** `handles auth` is noise; `POST /session →71 src/auth/session.ts:BeginSession` is a lookup. Prefer the fully qualified name over the readable72 paraphrase, always.73- **Be greppable.** Keep identifiers verbatim as they appear in the code, so a search for the symbol74 finds both the map row and the definition. Do not pretty-print, pluralise or translate names.75- A human may read it, and that is fine. But **no line earns its place by being nice to read** —76 only by answering a question that would otherwise cost a tool call.77- **It is consumed with one `Read`, whole — never `grep`ped, paged with `sed` or sampled.** The size78 ceilings in §2.1 exist precisely so a single pass is affordable; searching inside it is the79 behaviour the map was built to remove. A matched line arrives without the section that gave it80 meaning — which heading it sat under, which minefield qualified it — so the reader guesses or81 searches again and pays twice for a worse answer. **A map being grepped is a map that failed**:82 either it is too long for one pass, or its consumer was never given the fragment it needed83 (§4.6). Fix the map or fix the routing; do not normalise the grep.84- **`sed`/`awk` never touch it at all.** On a governing document that is editing, and edits go85 through the single writer that applies the deltas (§4.7) — never through whoever happened to open86 it last.8788### 2.1 How deep, really8990The old rule of thumb was ~150 lines. **That was a human-readability heuristic and it does not91apply** (§2.0). The governing constraint is different: **every line must save a search, and every92line must be true.** Depth is bounded by what can be kept honest, not by what fits on a screen.9394- **Content repository** (each file is a self-contained unit looked up by name — a skill catalogue,95 a docs site, a policy set, a notebook collection) → **the per-file table is the map**: file, what96 it decides or contains, size. This is a database index, and indexes list rows. **Not optional and97 not deferrable**: without it the map cannot answer "which file decides X?", every session falls98 back to `grep`, and the map is doing none of its job. Too large to write by hand is an argument99 for generating it (§4.5), never for omitting it.100- **Code repository** → **index the public surface, not the directory tree, and go to symbol level.**101 Directories and entry points alone answer "where do I go to change something" and leave the102 expensive question — "where is the thing that does X?" — to `grep`, which is the failure this103 whole skill exists to prevent. See §2.3 for what to index and how to keep it true.104- The test is the same either way: **would this line save a search?** If not it is decoration — cut105 it, whatever the total length.106107### 2.3 Code repositories: index the surface, and generate it108109**The file is not the unit of lookup in code.** `src/utils/helpers.ts` answers nothing; a dumped110`tree` is the same problem with more tokens. What gets looked up is the **surface**: the names and111routes through which the system is entered and extended.112113Index these, whichever the project has — one row each, with a `path:line` anchor:114115| Surface | Row shape |116|---|---|117| HTTP/RPC routes | method + path → handler symbol → `file:line` |118| CLI commands / subcommands | command → entry symbol → `file:line` |119| Background jobs, cron, queue consumers | trigger/topic → consumer symbol → `file:line` |120| Events published and consumed | event name → producer, consumers |121| Public exports of each module/package | symbol → `file:line`, one line on what it owns |122| Domain types and their invariants | type → owning module → where it is validated |123| Persistence | table/collection → owning module → migration directory |124| Config and feature flags | key/env var → where it is read → default |125| Extension points | interface/hook → implementations |126127**Two rules that make the depth survivable:**1281291. **Generate it from the code, never write it by hand.** Hand-written symbol detail in an active130 codebase is fiction within a sprint, and a confidently wrong map is worse than no map. Extract131 from what cannot drift: the router table, the CLI registry, the DI container, exported symbols,132 migration filenames, the OpenAPI document, `ctags`/LSP output, the framework's own manifest.133 Regenerate in CI or a pre-commit hook and **fail the build when the map and the code disagree** —134 that gate is what converts the map from documentation into an invariant.1352. **Split before it becomes one unreadable file**: a root map with the cross-cutting picture, plus136 one generated map per package/service. Monorepos get one per workspace, not one of 4,000 rows.137138**What stays hand-written** is exactly what a generator cannot know and what does not churn: why a139boundary exists, which module must not depend on which, the minefields, the invariants, the140"looks wrong but is deliberate" notes. **Generated content answers *where*; hand-written content141answers *why* and *careful*.** Never mix them in the same section — the generated part is142overwritten, and hand-written notes inside it are lost on the next run.143144### 2.2 Locations and invariants, never derived state145146The single most common way a map rots: storing **the result of a measurement** instead of the way to147obtain it.148149- **Belongs in the map**: where something lives, what a directory is for, which command builds or150 tests, what is invariant, what is a minefield. These change when the repository is restructured —151 rarely.152- **Never belongs in the map**: counts, line totals, percentages of progress, "N files pending",153 test-pass numbers, sizes that move with the work. **These are stale the next time anybody commits**,154 and a wrong number is worse than an absent one because it gets quoted.155- The rule that resolves it: **store the command, not its output.** `Pending items: run156 scripts/pending.sh` ages well; `Pending items: 64` is a lie by tomorrow.157- Corollary for the reader: **a map removes the need to *search*, never the need to *measure*.**158 Locating a file is a map question; "how many are left?" is a command. Do not blame the map for the159 second, and do not try to make it answer it.160- The only figures that may appear are the ones in the header (generation date and short SHA), and161 they exist precisely so staleness is detectable.162163## 3. What it contains (and in what order)164165Order matters: what gets consulted most goes on top.166167```markdown168# Map of <project>169170> Generated <YYYY-MM-DD> against `<short-sha>`. If anything here does not match the repo, **the repo171> wins**: fix the line and move on. Maintained per the `project-map` skill.172173## I want to change… → go to…174| To… | Go to | Note |175|---|---|---|176| Add an endpoint | `src/api/routes/` | Central registry at `src/api/router.ts:40` |177| Change the DB schema | `migrations/` | Never by hand in `models/`: it is generated |178179## Structure180- `src/` — production code. `src/core/` depends on nothing in `src/adapters/`.181- `tests/` — unit next to the code; integration ones here.182183## Entry points184- CLI: `src/cli/main.py` → `cmd_*` per subcommand.185- HTTP: `src/api/app.ts`, listens on `:8080`.186187## Commands188| What | Command | Verified |189|---|---|---|190| Build | `make build` | 2026-08-05 |191| Tests | `pytest -q` | 2026-08-05 |192193## Conventions and invariants194- Tests live next to the module, `_test` suffix.195- No DB access outside `repositories/`.196197## Minefields198- `src/legacy/billing.py`: no tests, real billing depends on it. Read `docs/adr/0007` first.199200## Out of the map201`node_modules/`, `dist/`, `.venv/`, generated files (`*_pb2.py`).202```203204**The "I want to change… → go to…" table is the heart of the file.** It is the one that saves the205greps. If you only have time for one section, it is that one. A directory tree without it is206decorative.207208For a content repository, add the **per-file index** (§2.1) as its own section, generated rather209than hand-written wherever possible: derive each row from the file's own front matter, title or210first heading, so it cannot drift into fiction.211212## 4. How to generate it cheaply213214Do not read the whole repository: **index, do not copy**.2152161. **Inventory, not dump.** `git ls-files` grouped by first and second path level gives the real217 shape without listing 10,000 paths. What is not in git (generated, ignored) stays out unless it218 is a minefield.2192. **Read the files that are already maps**: `README`, `CONTRIBUTING`, `CLAUDE.md`/`AGENTS.md`,220 `Makefile`/`justfile`, `package.json` scripts, `pyproject.toml`, `docker-compose.yml`, CI under221 `.github/workflows/`. Commands come from there, not from memory.2223. **Entry points by ecosystem convention**: `main`, `index`, `app`, `cmd/`, `bin/`,223 `[project.scripts]`, the Dockerfile `entrypoint`, compose `services:`.2244. **Minefields from history**: the most-churned files (`git log --format= --name-only | sort |225 uniq -c | sort -rn | head`) show where it hurts. Cross that with missing tests and the minefield226 list writes itself.2275. **Generate the index with a script, not by hand** (§2.1, §2.3) — a loop that extracts each file's228 own summary line, or each module's exported symbols, is exact, repeatable and free of invention.229 In a content repository the source is the file's own front matter or first heading; in a code230 repository it is the router table, the CLI registry, exported symbols, `ctags`/LSP output or the231 OpenAPI document. **Commit the generator next to the map**: a generated file whose generator is232 lost becomes hand-written again on the first edit.2336. **Gate it**: regenerate in CI or a pre-commit hook and fail when the map and the code disagree.234 Without that gate the map is documentation and it rots; with it, it is an invariant.2357. **Delegable**: in a large repo an exploration subagent returns the draft and you verify it. That236 is precisely the cost the map stops you repeating.237238### 4.6 Slice the map into the agent prompts — the slice *is* the partition239240When work is delegated to a fleet, **paste into each agent's prompt exactly the fragment of the map241that its slice needs, and nothing else**. Every tier repeats this downwards: an agent that fans out242passes each of its subagents only the part of *its own* fragment that the subagent needs. The slice243narrows monotonically as you descend.244245This is not only context economy. **The fragment is how the boundary is communicated**: an agent246holding only its own rows has its territory written down, so "do not touch anything outside your247slice" stops depending on it remembering. Handing the whole map to everyone does the opposite — it248shows each agent the files it must not touch, and pays tokens ×N for the privilege.249250Two traps that make the difference between this working and it causing an incident:251252- **Carry the minefields that touch those files, even when they look out of scope.** The slice is253 cut by *relevance*, never by section: a warning about a file the agent will edit belongs in its254 fragment whichever heading it lives under. An agent that walks into a trap you had documented and255 did not forward is your failure, not its.256- **The slice inherits the map's honesty, amplified.** A wrong line handed to one reader is one257 mistake; handed to eight agents it is eight, made simultaneously and confidently. Regenerate the258 fragments from the current map on every run — never from a cached copy of a previous one.259- **The prompt is the only channel, so agents never open the map themselves.** If they may fetch what260 the fragment lacks, the fragment stops being a boundary and becomes a suggestion — and the routing261 defect that produced the gap is hidden instead of fixed. **An agent whose fragment is missing,262 incomplete or contradicted by disk reports that and works from the files**; the tier above amends263 the brief in flight. That is what keeps the boundary absolute while still being survivable: an264 agent starved of context has a way out that does not involve reading rows it was never meant to265 see, and the gap gets fixed once, above, for every sibling at the same time.266267**Make the surprise rule (§5) travel too**: require each agent to report back when the fragment268failed it — a path that does not exist, a command that does not run, a convention that turned out269different. That turns the fleet into a distributed verification pass over the map: they find the270errors in parallel, and you fix them once, at the top.271272### 4.7 Close the loop: every agent returns a map delta273274**A fleet rots the map faster than solo work does** — eight agents move, rename and create things in275parallel and none of it reaches the index. So the return leg is mandatory, not a courtesy:276277- **Every agent ends its report with a `map delta` section**: what it changed that the map names or278 should name, what the map got wrong, what it had to discover because the map did not say. Empty is279 a valid delta and is stated explicitly — silence is indistinguishable from forgetting.280- **Two kinds, and both are wanted**: *(a) the map was wrong* (surprise rule, §5) and *(b) the work281 changed the repo* (same-turn rule, §5). The first fixes the past, the second records the present.282- **Each tier aggregates and deduplicates its children's deltas before passing its own upwards.**283 Eight agents touching one subsystem produce one merged delta, not eight overlapping ones.284- **Agents never edit the map.** It is a single file: N writers on it violates disjoint ownership and285 produces lost updates or conflicts. They emit the delta; **the top applies it, in one pass, in the286 same turn the fleet lands** — which is how the same-turn rule survives delegation instead of being287 quietly voided by it.288- Prefer deltas stated as an edit, not as prose: *"row `X` → path is now `Y`"*, *"new entry needed:289 `Z` owns …"*, *"minefield stale: the `.htaccess` warning no longer applies"*. A delta you have to290 interpret is a delta you will apply wrong.291292**A command you have not run is not written as verified.** Either run it, or mark it `Declared gap`.293294## 5. Keeping it honest295296- **Surprise rule**: every time the map fails you — a path that no longer exists, a command that297 does not work — **you fix that line there and then**. It is the only maintenance that survives.298- **Same-turn rule**: if your change moves, creates or renames something the map names, you update299 the map **in that turn**. A map updated "at the end" is not updated.300- **Mechanical path check** (cheap, run it when picking work back up):301302 ```bash303 grep -oE '`[a-zA-Z0-9_./-]+/[a-zA-Z0-9_./-]*`' PROJECTMAP.md | tr -d '`' |304 while read -r p; do [ -e "$p" ] || echo "DEAD PATH: $p"; done305 ```306- **Drift against the code**: compare the header SHA with `git rev-parse --short HEAD`. Many commits307 of difference do not invalidate the map — structure moves slowly — but they do mean looking at308 `git diff --stat <map-sha>..HEAD -- '*/'` for new directories.309- **Working tree vs. HEAD**: with many uncommitted changes, say so in the header and state which310 figures come from disk and which from `HEAD`. A map that silently mixes both misleads.311- **If the map contradicts the repository, the repository wins.** Always. The map is an index, not a312 source of truth.313314## 6. Prohibitions315316- ❌ **Starting substantial work in a repo without a map when creating one was possible.** The317 exploration you are about to do *is* the map: not writing it down is choosing to pay twice.318- ❌ **Copying code content into the map** (signatures, function bodies, full schemas). Duplication319 guarantees divergence. Cite `file:line`, do not transcribe.320- ❌ **A hand-written file dump.** Not because it is long — because it is unmaintainable and will321 lie. Depth in a code repository is legitimate and wanted, but **generated from the code and gated322 in CI** (§2.3). The prohibition is on the hand-written part, not on the detail.323- ❌ **Indexing a code repository by path instead of by surface** (§2.3): a `tree` with comments324 answers "where do I go", never "where is the thing that does X" — which is the search you are325 paying for.326- ❌ **Hand-written notes inside a generated section.** They are silently destroyed on the next327 regeneration. Keep *where* (generated) and *why/careful* (hand-written) in separate sections.328- ❌ **Writing what you have not verified.** No "the tests are probably run with…". Either check it329 or mark it `Declared gap`.330- ❌ **Storing a measurement instead of the command that produces it** (§2.2): counts, totals,331 progress percentages. They are stale on the next commit and they get quoted as if they were not.332- ❌ **Shipping a content repository's map without the per-file index** (§2.1) and calling it done.333 That table is the whole point; everything else is preamble.334- ❌ **Letting it expire in silence.** If you detect it is stale and cannot fix it whole, **mark the335 affected section as unreliable** instead of leaving it looking valid.336- ❌ **Putting doctrine in the map** (how work is done, what is forbidden): that belongs in337 `CLAUDE.md`. The map says **where** things are, not **how** they must be done.338- ❌ **Secrets, identifiable internal paths, IPs, production hostnames.** Maps tend to end up339 versioned; treat one as public code.340- ❌ **Creating `PROJECTMAP.md` and never looking at it again.** A map not read at the start saves341 nothing: the habit is reading it **before** exploring, not after.342- ❌ **Committing it in someone else's repository without permission.** It goes in343 `.git/info/exclude`, which is local.344345## 7. Long-term sustainability346347- The map is **disposable and regenerable**: if a large refactor invalidates it, regenerate from348 scratch (§4). Its history is not worth preserving.349- **Growth is not the failure mode; drift is.** Do not trim a map because it got long — trim it350 because a section stopped being consulted or stopped being true. When it does need splitting, split351 by package/service (§2.3), never by truncating detail. The one thing that always goes first is a352 hand-maintained full tree: maximum length, minimum answers.353- In repos with rich `AGENTS.md`/`CLAUDE.md`, **do not duplicate**: link to them.354355## 8. Mandatory web verification3563571. **Against the repository, always**: paths exist (§5), commands run, entry points start. **If the358 map contradicts the repo, the repo wins.**3592. **Against the web**, only for external things the map cites: build tool names and versions,360 package-manager commands, the canonical location of a framework's config file. Those expire and361 are not fixed from memory.362363If the web contradicts this document, **the web wins** — flag the discrepancy.