# Repo Cartograph

> Surveys an unfamiliar repo and writes what it found to docs/agent/repo-map.md — workspace kind, every project with its stack, boundary rules, gate commands, changed-set command. Run it when asked what a repo contains, which projects or which stack it has, how it is tested or built; when landing in a repo not yet mapped; when the map is stale; or before any skill that reads it.

- Skill: `alpayc/repo-cartograph` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add alpayc/repo-cartograph`
- Raw SKILL.md: https://api.skillmd.com/api/skills/alpayc/repo-cartograph/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: AlpayC (https://skillmd.com/u/alpayc)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/alpayc/repo-cartograph

---


# Repo cartograph

The same questions — what is this project, how do I test it, what may it import, what did my change affect — get asked on every run. The map answers them once. Everything downstream reads `docs/agent/repo-map.md`; nothing else re-detects.

The map is written the same way regardless of how the repo is organised. Only the **detection** differs, and that is what step 1 settles.

## 1 — Identify the workspace kind

Detect before assuming. A repo can also be several kinds at once — a monorepo containing an independently built subfolder is common.

| Signal | Kind |
|---|---|
| `nx.json` | NX |
| `turbo.json` | Turborepo |
| `pnpm-workspace.yaml` | pnpm workspaces |
| `workspaces` in root `package.json` | npm / yarn workspaces |
| `lerna.json` | Lerna |
| `rush.json` | Rush |
| One `package.json`, no workspace field | Single package |
| `pom.xml` / `build.gradle*` / `*.sln` / `go.work` / `Cargo.toml` with `[workspace]` | Backend build system |
| No shared root; separate clones | Polyrepo — map each repo separately, link them via **Seams** |

Read [`WORKSPACE-KINDS.md`](WORKSPACE-KINDS.md) for the per-kind commands: how to enumerate projects, read the dependency graph, and compute the changed set. It also carries the fallback ladder for repos whose tooling offers none of that.

Record the kind, its version, and the enumeration command in the map. When two kinds coexist, record both and say which part of the tree each governs.

## 2 — Enumerate the projects

Ask the tooling first — its config is the source of truth and it cannot go stale. Where there is no tooling to ask, derive projects from the directory structure: a project is a directory with its own manifest (`package.json`, `pom.xml`, `csproj`, `go.mod`).

Record for each: name, type (app / lib / e2e / service), path, and any tags or grouping metadata the tooling carries.

## 3 — Detect the stack per project

Per project, never one answer for the whole repo — even a single-package frontend has a backend sibling with a different stack. Resolve these slots from evidence, writing `unknown` where evidence is absent, because a wrong entry costs more than a blank one:

| Slot | Where the evidence lives |
|---|---|
| Framework | dependencies in the project's manifest, build config, executor or script names |
| Language variant | `tsconfig` strictness, file extensions in the source root |
| Test runner | `jest.config.*`, `vite.config.*`, `karma.conf.*`, `pytest.ini`, the test script |
| E2E | sibling e2e project, `playwright.config.*`, `cypress.config.*` |
| State / data layer | **two real source files first**, then the manifest. A repo with no store dependency still has a convention — a per-feature hook, a service, a plain context — and reading the manifest first returns "none", which is the wrong answer written confidently |
| Styling | SCSS, Tailwind config, CSS modules, styled-components in the source root |
| HTTP layer | generated client, framework HTTP wrapper, `fetch` wrapper — name the file that owns it |

State and HTTP are the slots downstream skills get wrong most often, and they fail in both directions: a dependency being installed does not mean this project uses it, and no dependency does not mean there is no convention. Confirm both against real files, and where the answer is a convention rather than a library, write the convention down with the files that prove it.

**Fan out across projects.** Detection is pure reading and each project is independent, so past roughly five projects, dispatch a subagent per project — or per batch of three or four in a large workspace — and have each return only the filled slot table plus the evidence path for each. A fifteen-app workspace goes from a long serial crawl to one round, and the subagent contexts absorb the file reading instead of yours.

Where the returned slots disagree about something workspace-wide — two projects reporting different base branches, say — resolve it yourself against the config rather than trusting the majority.

## 4 — Extract the gate commands

Per project, the exact commands that constitute green: typecheck, lint, test, build, e2e. Take them from the project's own scripts or targets, not from memory of what the tool usually calls things. Record which gates **do not exist** — a project with no test script is a fact `green-gate` needs in order to report a skip rather than a pass.

Also record how to compute the changed set for this repo (from `WORKSPACE-KINDS.md`) and the default base branch.

## 5 — Extract the boundary rules

Whatever enforces architecture here, in descending order of strength:

- Tag-based rules (`@nx/enforce-module-boundaries` and equivalents) — record the full tag-to-tag matrix
- Import restrictions in ESLint (`no-restricted-imports`, `import/no-restricted-paths`)
- A dependency-cruiser or ArchUnit-style ruleset
- Package visibility: `private`, `exports` fields, module systems, package-level access modifiers
- **Convention only** — nothing machine-enforced. Record the convention *and* the fact that nothing checks it, because an agent will otherwise assume lint would have caught a violation.

## 6 — Locate the seams

- **Across repos** — where the other repo lives on disk or in git, how the API contract is expressed (OpenAPI path, GraphQL schema, hand-maintained types), and which project owns the generated client. Hand the detail to `api-contract-sync`.
- **Within the repo** — for micro-frontends or modular monoliths: which project composes the others, who owns which routes, what deploys independently.

## 7 — Write the map

`docs/agent/repo-map.md`:

```markdown
# Repo map
Generated: <ISO date> · Repo: <name> · Commit: <sha>

## Workspace
Kind: <nx | turbo | workspaces | lerna | single | polyrepo | mixed> <version>
Enumerate: `<cmd>` · Graph: `<cmd or "derived from manifests">`
Base branch: <name> · Changed set: `<cmd or the fallback used>`

## Projects
| Project | Type | Framework | Test | Tags | Path |

## Project detail
### <project>
Stack: <the slots from step 3>
Depends on: <projects> · Consumed by: <projects>
Dev server: <command> (port <n>) · Route base: <path>
Gates: typecheck `<cmd>` · lint `<cmd>` · test `<cmd>` · build `<cmd>` · e2e `<cmd or none>`

## Boundaries
Enforced by: <mechanism, or "convention only — nothing checks this">
<matrix or rule list>

## Seams
Contract: <where, what format, who owns the client>
Composition: <which project composes what>

## Unknowns
<Every blank slot, with what evidence would fill it>
```

The **Unknowns** section is load-bearing. A downstream skill hitting a blank knows to investigate; hitting a plausible-looking wrong value, it does not.

## Done when

Every project the enumeration returns appears in the map, the workspace kind and changed-set command are recorded and have been run once, every slot is either filled from named evidence or listed under Unknowns, and every gate command in the map has been executed once to confirm it exists and exits.

## Staleness

The map carries the commit it was generated at. Before relying on it, re-run the enumeration: a project the map lacks means the map is stale, and refreshing it is cheaper than working around the gap.

