Cartograph
Extract a structural map of a codebase: surfaces, features, entities, relationships, operations, flows, compartments, and tech stack. Four orthogonal axes — surfaces are where you go (pages/entry points), features are what you can do (standalone capabilities), entities are what the app works with (data), and compartments are how the code is organized (logical file groupings that bridge product concepts to the underlying codebase). The tech stack provides a comprehensive inventory of all technologies, frameworks, and libraries the project uses.
Workflow
Intent Detection
The default mode is Full Scan — produce a full structural map of the codebase plus code health. Run this unless the user's message clearly asks for one of the invariant-specific modes below.
Two narrower modes override the default:
- Add Invariant — message contains "add invariant", "new invariant", or "add this invariant: '...'" → Add Invariant Flow.
- Standalone Verify — message contains "verify", "check invariants", "run invariants", or similar → Standalone Verify Flow.
Full Scan
A scan has four steps:
- Discover — read the codebase to build the context every extract task needs.
- Extract — produce the structural map.
- Analyze — assess the assembled map for code health and invariants.
- Assemble — merge everything into .
Within Extract and Analyze, fan out via subagents on anything whose inputs are ready — don't run them serially in the orchestrator, wall time matters. The natural parallel batches are:
{ Surfaces, Entities }
→ { Features, Operations }
→ { Flows, Compartments, File Tree Weights }
→ { Compartment Dependencies }
→ { Co-location, DRYness, Dead Code, Invariants }
Every task below names its inputs and what it returns. All return shapes conform to
references/json-schema.md
.
1. Discover
Run this yourself before fanning out — it's fast and every Extract task needs it.
- Read for project name and dependencies (framework detection).
- Glob for key structural files:
- Schema: , ,
- Routes/Pages:
app/**/page.{tsx,ts,jsx,js}
, ,
- Server actions: grep for
- Components:
components/**/*.{tsx,jsx}
- Lib/services: ,
- Read the directory tree to understand the overall shape.
- Detect the tech stack. For each technology in use, record name, version, category, source (where you found it), and confidence (high = explicit dependency + matching config; medium = dependency only; low = inferred from patterns). The full catalog of detection signals — config files, file patterns, import patterns — lives in
references/tech-stack-detection.md
. The category list lives in references/json-schema.md
.
- Collect the full non-generated file inventory.
Returns the discover bundle: file inventory + tech stack array. Pass this to every Extract and Analyze task.
2. Extract
Surfaces
Needs: discover.
Surfaces are entry points — self-contained user-facing experiences. Each app is fundamentally a collection of surfaces.
- Walk the route tree () and identify each distinct user-facing experience.
- Group related routes into surfaces (e.g., + = one "Creation Studio" surface).
- Look for admin-only areas, standalone tools, dashboards, and onboarding flows.
- For each surface determine: entrypoint (main page file and route), actor (user/admin/system), description (what this surface does as a standalone experience).
Returns: surfaces array (without
,
,
, or
— those get back-filled by later tasks and Assemble).
Entities + Relationships
Needs: discover.
Entities — read schema/type definitions and extract domain objects:
- DB models (high confidence) — Prisma models, TypeORM entities, Mongoose schemas.
- TypeScript types/interfaces (medium confidence) — types used as API payloads, form data, state.
- Enums (high confidence) — enum definitions representing domain concepts.
- Derived types (medium confidence) — transformed versions like .
For each entity: id, name, kind, description, source location, key fields (3–8 most important), confidence.
Relationships — map connections between entities:
- Foreign keys and references in schema → , , .
- Nested includes/joins → confirm relationships.
- Type compositions → .
- Looser references → .
Returns: two arrays —
and
.
Features
Needs: discover, surfaces, entities.
Features are standalone capabilities embedded within surfaces — what you can do in the app, as distinct from where you go. A like button, a prompt wizard, a credit purchase, an age gate are all features. They compose into surfaces; they aren't themselves pages.
If you can't describe a feature without naming a specific page, it's probably part of a surface, not a feature.
The six kinds — tool, interaction, transaction, gate, infrastructure, workflow — and the code patterns that signal each are catalogued in
references/feature-kinds.md
. Use that as your scanning checklist.
Separate implementations are separate features. The same conceptual capability often exists as independent implementations in different surfaces — a user-facing "Prompt Wizard" modal in chat and an admin "Prompt Remix Wizard" in the post-management area. Always create separate feature entries; name them distinctly. After extracting from one surface, scan the others' component trees for similar patterns and grep for shared service imports — this is the easiest class of feature to miss.
For each feature record: name, description, kind,
,
, implementations (2–5 most important files, not every file).
Returns: features array (without
— Compartment Dependencies fills that in).
Operations
Needs: discover, entities.
For each entry point (route handler, server action, API endpoint):
- Which entity it targets.
- Operation type: , , , , or .
- Descriptive name (e.g., "Publish Post", "Generate Preview").
- Side effects on other entities.
- Implementation location (file + function).
Returns: operations array.
Flows
Needs: discover, surfaces, entities, features, operations.
- Start from UI pages — what can a user do on each page?
- Trace: UI action → handler → service → DB.
- Name each flow by its user-visible goal.
- Identify trigger and actor (user/admin/system).
- List steps in order, linking to operations and entities.
Returns: flows array.
Compartments
Needs: discover, surfaces, features, entities, operations.
Compartments are logical groupings of related files that form cohesive units of functionality. They bridge the product-side view (surfaces, features) with the underlying code structure, so a developer can navigate from "what does this feature do?" to "where does that code live?".
- Scan the file tree, using surfaces, features, entities, and operations as context.
- Group files using multiple signals:
- Folder structure — files in the same directory often belong together.
- Import graph — files that heavily import each other are likely in the same compartment.
- Feature alignment — files belonging to a feature should cluster into compartments mapping to those features.
- Domain proximity — files dealing with the same entity or business concept belong together.
- Naming conventions — files with related names (e.g., , ) suggest a compartment.
- Shared infrastructure — files used by 3+ features may warrant their own compartment, or may appear in multiple compartments.
- Compartments are nestable — sub-compartments can be nested to any depth. A typical web app has 2–3 levels.
- Files are non-exclusive — a file can appear in multiple compartments (e.g., in both "Database Access" and "Shared Infrastructure").
- Every non-generated file must appear in at least one compartment. Config files, build tooling, etc. go into a "Project Infrastructure" compartment. Exclude , , , .
- For each compartment: name and description (name after what it does, not folder names — "Image Generation Pipeline" not "app/chat/actions"), tags (from the vocabulary in
references/json-schema.md
, plus custom as needed), files (with role: component, hook, action, api, lib, type, config, style, test, other), parentId (null for top-level), featureIds, surfaceIds.
Guidelines that keep compartments useful:
- Don't create compartments with only 1 file unless it's a genuinely standalone module — merge small groupings into their parent.
- Keep top-level compartments to 8–15 for a typical web app. Sub-compartments can be more.
- Prefer meaningful groupings over 1:1 folder mapping. If a folder mixes unrelated files, split them. If related files span folders, group them.
Returns: compartments array (without
— Compartment Dependencies fills that in).
File Tree Weights
Needs: discover, features.
For every non-generated file, estimate what proportion of the file's purpose serves each feature.
- Take the full file list from the discover bundle.
- For each file, read it (or sample very large files) and estimate proportions.
- Files that don't belong to any product feature get as their sole feature weight.
- Files serving multiple features get proportional weights (e.g., a shared hook → 50/50).
- All weights for a file must sum to 1.0.
Estimation guidance:
- Look at imports, function names, component names, and the overall purpose of the file.
- A file 100% dedicated to one feature →
[{featureId: "that-feature", weight: 1.0}]
.
- A shared utility used by multiple features → split proportionally.
- Config files, generic type definitions, build config, middleware → .
- Prefer fewer features per file with higher weights over many features with tiny weights.
Returns: array of
{file, featureWeights: [{featureId, weight}]}
entries — one per file.
Compartment Dependencies
Needs: compartments (plus surfaces and features for back-filling).
- Walk the imports of every file in every compartment.
- Map each imported file to the compartment(s) it belongs to.
- Record these as edges on each compartment (inter-compartment only, no self-references).
- Populate on features — for each feature, determine which compartments implement it.
- Populate on surfaces — for each surface, determine which compartments serve it.
Returns: updated compartments array (with
populated), plus a
map and a
map.
3. Analyze
Runs on the assembled extract.
How analysis tasks report. Each analysis task emits a metric record with a score, a thresholds object, a one-line
, and a
array. The findings
are the explanation for the score — every file or item that pulled the score below 100 must appear as a finding, with a concrete recommendation. The summary must include exact counts ("8 of 103 evaluated files are misplaced"), not vague prose ("most files are correctly placed").
A sub-100 score with no findings is unactionable — treat it as a bug in your output, not a valid result.
Each task's specific finding shape lives in
references/json-schema.md
under the metric's name.
Co-location
Read project instructions (
,
, or equivalent) and extract explicit co-location conventions. If none are found, fall back to:
- Files used by a single surface should live inside that surface's directory.
- Files shared by multiple surfaces but representing one capability belong in .
- Root , , and are reserved for truly global code used by 3+ surfaces/features.
- is exempt and considered correctly placed.
Evaluate every non-generated, non-infrastructure file:
- Trace which files import it.
- Determine which surfaces/features actually consume it.
- Compare its current location to where the rules say it should live.
- Assign a binary / verdict.
For each failing file, emit a finding with
(co-locate inside a surface or feature directory) or
(move up into
because it serves multiple surfaces). Include
,
,
,
, and
.
Score:
(passing files / total evaluated files) * 100
.
Returns: one metric object with
.
DRYness
- Use features and compartments as the starting map.
- Look for candidate duplication before reading files:
- Features with the same and overlapping across different surfaces.
- Files in different surfaces with similar names or import patterns.
- Compartments with similar descriptions, tags, or overlapping .
- Hooks/actions/clients that wrap the same external API or workflow.
- Read the candidates to confirm real overlap — weigh both functional overlap (same product problem solved twice) and structural similarity (same technical pattern repeated with light variation).
- For each confirmed duplication: identify what's genuinely shared, what must stay implementation-specific, and where shared logic should live (respecting co-location rules).
Each finding includes
,
,
,
,
, and
.
Score:
K = 200 / totalNonInfrastructureFiles
;
score = max(0, 100 - (findingCount * K))
.
Returns: one metric object with
and
.
Dead Code
- Build an import map for every non-generated file (resolve relative imports and path aliases).
- Use the always-live entry-point list from
references/dead-code-detection.md
— those never count as dead even with zero importers.
- Walk the codebase looking for the five finding kinds documented in
references/dead-code-detection.md
:
- — non-entry-point with zero importers.
- — non-test file imported only by tests (informational; doesn't affect the score).
- — surface with no inbound navigation references.
- — feature with empty or all-orphaned surfaces.
- — DB model or DTO with no operation, feature, or query reference.
Each finding includes
,
,
,
,
,
,
.
Score:
totalEvaluated = filesEvaluated + surfacesEvaluated + featuresEvaluated + entitiesEvaluated
;
deadItems = deadFiles + orphanedSurfaces + orphanedFeatures + deadEntities
;
score = ((totalEvaluated - deadItems) / totalEvaluated) * 100
. Exclude test-only files from both numerator and denominator.
Returns: one metric object with
.
Invariants
Runs only if
exists at the repo root. Otherwise return
and Assemble omits the
key entirely.
- Read .
- For every invariant, run the Verifying an invariant procedure below.
- Compute summary counts (total, passing, failing, skipped).
- Set to the current ISO 8601 timestamp and to
"cartograph-invariants.md"
.
Returns: the
object matching the schema in
references/json-schema.md
, or
.
4. Assemble
Run this yourself. Merge everything into
:
- Populate , , , and on each surface — from operations, flows, and the Compartment Dependencies output.
- Populate on each feature — from the Compartment Dependencies output. Set (the empty array is the back-compat shape; is the primary code mapping).
- Include the array from Discover as-is.
- Include the File Tree Weights array as-is under .
- Add a top-level object:
analyzedAt = ISO timestamp
, metrics = [coLocationMetric, drynessMetric, deadCodeMetric]
.
- If the Invariants analysis returned a non-null result, include . If , omit the key.
- Write at the repo root, creating the directory if needed. The final shape lives in
references/json-schema.md
.
- Tell the user: "Start the Cartograph UI from your project root with
npm --prefix skills/cartograph/app install
once, then npm --prefix skills/cartograph/app start
." If invariants were verified, also print the invariant summary (same format as Standalone Verify).
Add Invariant Flow
When the user wants to add a new invariant:
- Extract the user's assertion text (the natural-language claim after "add invariant:" or similar phrasing).
- Read the codebase to understand the assertion:
- Identify relevant files, functions, and patterns related to the assertion.
- Determine which surfaces and features are involved (if a previous exists, reference its IDs for and ).
- Map out the verification approach.
- Expand the one-liner into a full invariant definition following
references/invariant-definitions-format.md
:
- Write the YAML frontmatter: generate a unique kebab-case ; set (critical for money/security/data integrity, high for core product logic, low for conventions); add relevant ; optionally add /.
- Write all body sections: Assertion, Verification steps, Pass criteria, Known scope, Verification prompt.
- Append the invariant to at the repo root. Create the file with a heading if it doesn't exist.
- Run an initial verification using the Verifying an invariant procedure below.
- Report the result:
- Passing: "Invariant added and verified. Definition saved to ."
- Failing: "Invariant added but does NOT currently hold — definition saved anyway. Violations: [details]. Fix the code to make it pass, or edit the definition if the assertion needs adjusting."
If the assertion is too vague to determine verification steps, ask a clarifying question before writing the definition.
Standalone Verify Flow
When the user wants to verify existing invariants without a full scan:
-
Read
from the repo root. If the file doesn't exist: respond "No invariant definitions found. Add one with:
/cartograph add this invariant: '...'
".
-
Run the Verifying an invariant procedure below on every invariant in the file.
-
Print a pass/fail summary to the console:
Invariant Results (N checked)
──────────────────────────────────
✓ CRITICAL Invariant name
Summary of passing result
✗ HIGH Invariant name
Violation in file:line
Brief description of violation
N of M invariants passing.
-
If
exists, update
only the
key (leave all other data untouched). Write the
object following the schema in
references/json-schema.md
.
-
If
doesn't exist, create the
directory if needed and write a minimal JSON with only
and
keys.
Verifying an invariant
The shared procedure used by Add Invariant Flow, Standalone Verify Flow, and the Invariants analysis task in Full Scan.
- Parse the invariant: extract frontmatter fields and body sections (see
references/invariant-definitions-format.md
).
- If , emit a result and stop.
- Follow the Verification steps section as a guide; read files listed in Known scope plus anything the steps reference.
- Evaluate whether the Pass criteria hold:
- Passing: record checked files, an empty violations array, and set to .
- Failing: record specific violations with file paths, line numbers, what was expected, what was found, and a suggestion. Generate a self-contained that an AI agent can use to fix the specific violations — include the invariant name, violation details, affected paths/lines, and what needs to change.
- Set on every result (passing or failing) to the Verification prompt from the invariant definition.
The result shape lives in
references/json-schema.md
under the
key.
Important
- Read-only on the analyzed codebase — never modify it. Only and (on user request) are written.
- Prefer inclusion with lower confidence over omission when unsure.
- Plain-language descriptions — a PM should be able to read them.
- Relative paths — all file paths relative to repo root.
- Large repos — analyze by feature/route directory and merge.