Visual Atlas
Turn a codebase into a standing, self-contained explanation of its current architecture — and open it. The output is a recursive topic tree in the established app-shell language:
System
├── Domain
│ └── Topic
│ └── Nested topic
└── Root-level topic
System and domain pages orient the reader; topic pages explain one mechanism in depth. The page hierarchy follows the reader's mental model, not the filesystem. A newcomer should grasp what the system is in about 60 seconds, choose a relevant domain, and open only the depth they need.
Tool location (resolved through the installer's ~/.claude/visual-skills symlink — re-run npm run skills:install if the repo moves):
VISUAL_SKILLS_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/visual-skills"
Required language guide. Before writing user-facing text, read
$VISUAL_SKILLS_DIR/skills/shared/plain-language.md. Apply it to every authored field and live
reply.
Unlike visual-spec (pure authoring), the atlas is hybrid: a mechanical scanner inventories the
repo and emits draft JSON; you author the reader-owned hierarchy, write the meaning, and render. The
scanner may suggest extraction candidates, but the scanner never creates, moves, or mutates a page
or the recursive topic tree.
Read the component catalog at $VISUAL_SKILLS_DIR/skills/shared/atlas-components.md, the diagram
catalog at $VISUAL_SKILLS_DIR/skills/shared/diagrams.md, and exact JSON shapes in
$VISUAL_SKILLS_DIR/src/atlas-blocks.ts while authoring.
Editorial contract
- Orientation before depth. A landing page helps someone choose what to open; it does not copy every child explanation.
- One home per fact. Explain a mechanism fully on one canonical page. Parents summarize and link to that home.
- Current truth, not project history. Remove task narration, PR and review-round history, superseded behavior, migration chronology, and cumulative “then we changed…” prose.
- Reader structure is not file structure. A topic may be grounded in one file, one folder, or evidence spread across folders and domains.
- Progressive disclosure without dead ends. Every page works on direct entry and links clearly to its parent, children, siblings, and related pages. Curated reading paths appear on the system or domain landing page that owns them, where they help readers choose a route through the atlas.
The standard — definition of done
A finished atlas always has, regardless of repo size:
atlas.htmlwith: anatlas-tldr("Start here") — what the system does in one line + a 3–5 item primer of what to hold in your head; thedomain-map(all domains + cross-domain edges); and thedomain-index— a grid of domain tiles, each with a realpurposeand (where the page exists) a link. Usually also adiagram-section(the "spine": the runtime loop).- Each domain landing with: a
domain-tldr; purpose, responsibilities, and boundaries; one useful architecture or runtime view; child page cards; owned data; importantseams; and a compact implementation index. When children exist, the domain is orientation-first and a monolithicdepthdump is not required. - Each topic page with: a plain-language
topic-tldr; the applicable flow or algorithm; guarantees and failures; a grounded example or diagram for multi-stage behavior; child cards; and a collapsedimplementation-reference.
Size scales the ceiling, never the floor. A small repo may have just the atlas and one or two domain pages. The system still gets its lead/map/index, each domain gets orientation and seams, and each authored topic gets a direct-entry lead plus evidence appropriate to its shape.
Red flags — you stopped too early
If any of these is true, the atlas is not done — keep going:
atlas.jsonhas noatlas-tldr, nodomain-map, or nodomain-index.- A domain tile has no
purpose(the scanner left it blank and you didn't fill it). - A large domain explains every mechanism inline instead of giving substantial mechanisms their own child pages.
- A child page only repeats its purpose through empty or one-sentence scaffold blocks.
- Every topic uses the same four-block outline without shape-specific decisions, invariants, or evidence.
- A multi-stage mechanism has no grounded worked trace or diagram.
- A domain page has no
seams— the reader can't see how it connects to its neighbors. - A domain page describes a pipeline or flow in prose but shows nothing moving through it.
- Connections / detail prose is still the empty placeholders the scanner emitted.
- The tool printed completeness warnings. Those mean below standard — enrich the JSON and re-render until they are gone.
- You enriched pages but never ran
atlas-check.mjs --stamp— unstamped system, domain, and topic pages fail the drift check in any repo that wires it into pre-commit.
Discover behavior before choosing pages
The page tree is the result of source-grounded discovery, not the starting guess. After the
mechanical scan and before editing atlas.domains.json or page JSON, build a temporary discovery
brief. Keep it as working output for the run; the configured tree and rendered pages remain the
durable artifacts.
Journey traces
Inventory user-facing adapters, runtime entrypoints, routes, jobs, and central orchestrators. Choose the smallest representative set of user or system journeys that exposes the domain's meaningful behavior, then follow each journey from its earliest meaningful preparation through externally visible effects, termination, or recovery. Cross directory and domain boundaries when the calls, data, or state do.
Each journey trace records:
- its trigger, entrypoint, boundary crossings, and central orchestrators;
- ordered phases, state transitions, and admission guards such as leases, budgets, or deadlines;
- asynchronous branches, their purposes, and the barrier or convergence point that joins them;
- durable writes and other externally visible effects;
- failure, cancellation, terminal signaling, recovery, and cleanup behavior;
- repository-relative source evidence for every claim.
Read complete orchestration functions and their material callees; filenames and import counts are leads, not explanations. Search for companion entrypoints and variants that change the journey. A trace that starts at an internal service despite earlier preparation, or ends at the first visible response despite later settlement, is incomplete.
Responsibility ledger
Convert the traces and source inventory into a de-duplicated responsibility ledger. Every row contains the responsibility in reader language, why it matters, its evidence, the journey or invariant that exposed it, and its documentation disposition. Assign each significant row to exactly one of these outcomes:
- summarize it on the domain landing page;
- explain it on a topic or nested topic page;
- link to its canonical home in another domain or cross-cutting topic;
- omit it with a concrete reason, such as generated code or an incidental implementation detail.
"Not selected as a topic" is not an omission reason. Before choosing pages, challenge the ledger against the central orchestrators: account for their meaningful phases, branching and convergence, durable writes, terminal paths, and cleanup. A large domain is not covered merely because one well-understood mechanism received a child page.
Close the brief with a coverage decision:
- make page source groups cover the evidence actually used during discovery;
- add related and reading-path links that preserve important cross-domain journeys;
- confirm every significant ledger row has a visible home, canonical link, or supported omission;
- include the discovery brief in the final handoff so the user can see what was considered and how it was dispositioned.
Choose the page tree before writing prose
Use the completed responsibility ledger to start with reader questions. The domain landing gives a
high-level end-to-end account of its primary journeys, and every significant ledger row has a
visible home or link. Keep a subject on its domain landing when a concise summary is enough. Extract
a child when it has its own flow, rules, failure behavior, worked example, or implementation
evidence. Use a nested child when an internal algorithm deserves independent treatment, such as
domain -> pricing -> discount selection. A root-level topic belongs directly beneath the system
page when it crosses domains and has no natural domain owner.
Prefer at most two topic levels beneath a domain. Deeper authored trees remain valid, but warn so you reconsider whether the branch should become a domain or cross-cutting topic. Do not split merely to satisfy a word count: every page needs one coherent question and useful destination.
Choose an optional shape to guide the page ladder:
mechanism— inputs, stages, outputs, safeguards;algorithm— objective, ordered logic, thresholds, edge cases, worked example;data-model— entities, relationships, ownership, lifecycle, constraints;lifecycle— states or phases, transitions, triggers, recovery;integration— boundary, protocol, request/response flow, failure behavior.
The shape is guidance, not permission to pad irrelevant sections.
Earn every child page
Progressive disclosure moves explanation into a child page; it does not remove the explanation. Before authoring each topic, derive a temporary teaching brief from the discovery evidence:
- the reader question and mental model the page will teach;
- the inputs, outputs, and state transformed;
- the decisions, invariants, thresholds, or transitions that make the behavior non-obvious;
- one grounded trace or representative case, including failure and externally visible outcome;
- the source evidence supporting each part.
Use the shape-specific teaching contracts in skills/shared/atlas-components.md. A page earns its
place when a reader can predict what the system will do in a representative case, not when every
block type exists. Clear every hard integrity problem before rendering or stamping. Treat each
substance advisory as a prompt to add explanation, a worked example, or a diagram—never word-count
padding.
Authored configuration
atlas.domains.json owns both domain module grouping and the conceptual topic tree. These are
different relationships: domains own modules, while topics cite evidence without claiming ownership.
{
"repo": "shop-app",
"srcRoots": ["apps", "packages"],
"topics": [
{
"slug": "one-checkout",
"title": "One checkout",
"purpose": "How one purchase crosses the system.",
"shape": "lifecycle",
"aliases": ["checkout lifecycle"],
"sources": [
{ "label": "Entry and orchestration", "include": ["apps/api/src/**/*checkout*.ts"] }
],
"related": ["order-processing/pricing"]
}
],
"readingPaths": [
{
"title": "Understand one checkout",
"pages": ["one-checkout", "order-processing/pricing"]
}
],
"domains": [
{
"slug": "order-processing",
"name": "Order processing",
"purpose": "Prices, accepts, and fulfills a purchase.",
"globs": ["apps/api/src/orders/**"],
"modules": [],
"topics": [
{
"slug": "pricing",
"title": "Pricing",
"purpose": "How cart state becomes the final quoted total.",
"shape": "mechanism",
"aliases": ["quote", "cart total"],
"sources": [
{
"label": "Quote assembly",
"include": ["apps/api/src/pricing/**/*.ts"],
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
},
{
"label": "Catalog prices",
"include": ["packages/db/src/**/*price*.ts"]
}
],
"topics": [
{
"slug": "discount-selection",
"title": "Discount selection",
"purpose": "How eligible discounts are combined or rejected.",
"shape": "algorithm",
"sources": [
{
"label": "Discount policy",
"include": ["apps/api/src/pricing/**/*discount*.ts"]
}
]
}
]
}
]
}
]
}
Authors control slug, title, purpose, order, source scopes, children, related pages, and reading paths. Slugs are stable page identities; moving or renaming one is explicit.
Each source group has a reader-facing label, one or more include globs, and optional exclude
globs. A source scope can span folders and domains. Scopes may overlap between pages and between
groups; every dependent page includes the shared file in its own fingerprint. All matching files
contribute at file granularity. An empty group is an integrity failure.
Workflow (three modes)
bin/atlas.ts has three operation modes. The artifact set lives in one absolute --out directory,
all committable and re-renderable. Paths mirror the conceptual hierarchy:
.visual/atlas/
atlas.domains.json # the grouping config (human-owned source of truth)
atlas.json atlas.html # the atlas page blocks + rendered output
<atlas diagrams>.excalidraw
domain-<slug>/
domain-<slug>.json domain-<slug>.html
<that domain's diagrams>.excalidraw
<topic-slug>/
<topic-slug>.json <topic-slug>.html
<nested-topic-slug>/
<nested-topic-slug>.json <nested-topic-slug>.html
The renderer derives stable relative links, breadcrumbs, a current-branch sidebar, child page card navigation, parent/sibling links, related pages, owner-scoped reading paths on landing pages, and structured search. Search uses titles, purposes, aliases, source paths, and breadcrumbs—not every prose token.
1. Full scan (the main path)
Scan. From the tool dir so deps resolve:
cd "$VISUAL_SKILLS_DIR" npx tsx bin/atlas.ts --repo <ABSOLUTE_SUBJECT_REPO> --out <ABSOLUTE_OUT_DIR>This walks the repo, creates
atlas.domains.jsonfrom a folder first-guess if absent, reconciles domain drift, resolves topic evidence, emits configured recursive drafts only where absent, and renders. It never clobbers authored prose unless--forceis explicit.Discover behavior, then curate the grouping — usually required, not optional. Build the journey traces and responsibility ledger above before you choose the reader hierarchy. Then open
atlas.domains.json. The first-guess is one domain per top-level dir, which on a layered codebase (arouters/+services/+repositories/split, orapp/+lib/+server/) produces exactly the flat folder grouping this skill forbids — one giant "server" tile is not a domain. For anything beyond a small or already feature-foldered repo, rewrite the domains as feature/bounded-context slices with file-preciseglobs(e.g.["src/server/routers/picks*.ts", "src/server/services/pick*.ts"]) and re-run the scan. Regrouping is deterministic; theglobsare the human lever. After a regroup, delete any orphaneddomain-<slug>/folder for domains you renamed or removed — the scan warns about them (it never deletes files), and a stale one would still render a dead page. You may writeatlas.domains.jsonby hand; the scanner fills in resolved domainmodulesbut never invents or rearrangestopics.Enrich the drafts from the discovery and teaching briefs. Open the actual modules again as needed — don't work from the draft skeleton alone. Replace generated structure and text with reader-owned units and current truth. Ensure every significant responsibility has the landing summary, child page, or canonical link assigned by its disposition. Fill, per the catalog:
$VISUAL_SKILLS_DIR/skills/shared/atlas-components.md- the
atlas-tldrheading + primer; - every tile and child purpose;
- domain purpose, boundaries, ownership, seams, and useful next questions;
- topic summary, shape-specific flow, decisions, guarantees, failure behavior, grounded trace, and source references.
The exact JSON field shape of every block is defined and commented in
$VISUAL_SKILLS_DIR/src/atlas-blocks.ts— read it as you author. Short text fields are inline markdown.- the
Author the diagrams. The scanner drafts an editable
architecturedomain-map and a stub internal-arch; upgrade them (and add per-component flows) using the diagram catalog recipes + color vocabulary — carrymermaidso editable kinds stay editable. Optionally replace the domain-map with a hand-authoredsvgfor a curated layout.$VISUAL_SKILLS_DIR/skills/shared/diagrams.mdRender and review signals. Re-render and open
atlas.html. Close every hard integrity error. Treat readability warnings as editorial prompts: shorten, restructure, remove history, or deliberately extract a child. Treat substance warnings as evidence that a child is still an outline. The linter never auto-splits or pads content.Stamp what you just verified. Every scan/render also emits
atlas-check.mjsinto the out dir (see "Keeping the atlas honest" below). Finish by stamping — this records a per-page hash of the evidence it was verified against:node <ABSOLUTE_OUT_DIR>/atlas-check.mjs --stampOnly stamp pages whose prose you actually wrote or reviewed this run. Stable IDs are slash-joined paths such as
order-processing/pricing/discount-selection.
2. Single domain
Refresh one configured domain subtree after its code changed:
npx tsx bin/atlas.ts --repo <ABSOLUTE_SUBJECT_REPO> --domain <slug> --out <ABSOLUTE_OUT_DIR>
Refresh the journey traces and responsibility ledger before re-enriching, including upstream or downstream boundary evidence when the changed behavior crosses domains. Then stamp only the pages actually reviewed. A child topic has an independent stamp; do not stamp its parent merely because it rendered in the same run. Independent does not mean disjoint: overlapping evidence may legitimately make parent and child stale together.
3. Render-only (reproduce)
Re-render committed JSON with no scan — the recap/spec reproducibility pattern:
npx tsx bin/atlas.ts --blocks <ABSOLUTE_OUT_DIR>/atlas.json --out <ABSOLUTE_OUT_DIR> # one page
npx tsx bin/atlas.ts --all <ABSOLUTE_OUT_DIR> --out <ABSOLUTE_OUT_DIR> # every recursive page
open <ABSOLUTE_OUT_DIR>/atlas.html
Keeping the atlas honest (drift + verification)
Every scan and --all render copies atlas-check.mjs — a self-contained, tool-owned Node
script — into the out dir. Target repos commit it and run it from pre-commit/CI with plain Node
(no visual-skills checkout needed). It checks recursive integrity and freshness:
- Structure and coverage — configured JSON/HTML pages, source groups, local links, page-tree identities, reading paths, search links, domain module coverage, and derived counts agree.
- Grounding — named files, exports, and routes still exist in the page's evidence.
- Independent stamps — each system, domain, and topic page carries
verifiedAgainst. Topic fingerprints cover explicit sources plus child title/purpose summaries; domains cover owned modules plus child summaries; the system covers the authored tree and system summaries. - Advisories — density and historical-language observations print separately and do not make the checker fail by themselves.
Commands (from the subject repo):
node .visual/atlas/atlas-check.mjs # check — wire this into pre-commit/CI
node .visual/atlas/atlas-check.mjs --stamp # re-stamp every page
node .visual/atlas/atlas-check.mjs --stamp <page/id> # re-stamp one page
The maintenance loop this creates: a failing stamp is a review request, not a formality. Re-read only the stale page's evidence against its page, rewrite the smallest coherent current explanation, then re-render and re-stamp. Never stamp a page you haven't just read against the current code—the stamp's only value is that someone actually looked. Grounding and stamps verify structured claims and attention; only that review verifies prose.
That review loop is its own skill: atlas-review (skills/atlas-review/SKILL.md). Use it —
not a full rescan — when an existing atlas fails the check or the user asks to re-verify the
atlas against the code. Stamps record the git commit they were made at, so atlas-review can diff
precisely from the last verified state.
The block model
Each JSON doc is { "kind": "atlas"|"domain"|"topic", …page options…, "blocks": [ … ] }. Page
options drive page chrome; derived navigation is supplied at render time. The block types:
- Atlas page:
atlas-tldr,domain-map,diagram-section,domain-index. - Domain page:
domain-tldr,components,diagram-section,depth,owns,example,seams. - Topic page:
topic-tldr,topic-flow,topic-rules,implementation-reference, plusdiagram-sectionandexamplewhen useful.
The catalog (skills/shared/atlas-components.md) shows what each renders to and when to use it; the
field shapes are in src/atlas-blocks.ts.
Scaling by repo size
The floor is the same; only the ceiling moves.
| Element | Small repo (≤3 domains) | Large repo |
|---|---|---|
atlas-tldr + primer |
required | required |
domain-map |
required | required |
domain-index (tiles w/ purpose) |
required | required |
spine diagram-section |
usually | yes |
| per-domain page | the central 1–2 | all domains |
domain-tldr + child choices |
required | required |
internal-arch diagram-section |
if the domain is large | yes |
| recursive topic pages | when a mechanism merits depth | expected |
owns (data) |
if it owns models | yes |
seams |
required | required |
Don't pad — but a repo with several real domains warrants a page per domain. A "page pending" tile
(no href) is the honest way to name a domain you haven't deep-dived yet.
Readability gate
The linter warns, but does not auto-transform, when prose is likely to overload a reader:
- child page card purpose around 40 words or fewer;
- paragraph at most four sentences or roughly 100 words;
- three or more parallel facts usually become a list or structured group;
- domain landing warning around 1,200 visible prose words;
- topic warning around 2,000 visible prose words;
- warnings for project history or multiple independently explainable mechanisms.
The remedy may be deletion, rewriting, restructuring, or an authored extraction. Do not silence a warning by moving historical prose to another page.
Fallbacks
d2missing: diagrams degrade to visible placeholders (the atlas still produces) — tell the user tobrew install d2for proper rendered diagrams.- d2 vs Excalidraw: diagrams render as static D2 images by default and stay that way unless you
explicitly opt in — the editable Excalidraw upgrade is off by default even after
npm run setup:excalidrawis installed. To promote editable-eligible diagrams to.excalidrawscenes for a run, set"excalidraw": truein the atlas/domain JSON (or pass--excalidrawonrecap,spec, anddoc).--no-excalidraw(or"excalidraw": false) is still accepted as an explicit off, but it is now the default. Excalidraw support is beta and export-only: the atlas pages inline a static snapshot SVG, so editing a.excalidrawsidecar does not change the rendered page and re-rendering overwrites it — to change a diagram, edit itsd2/mermaidsource in the JSON and re-render. - The
domain-mapcan be the scanner's editablearchitecturediagram OR a hand-authoredsvgblock — either satisfies the standard.
Example
cd "$VISUAL_SKILLS_DIR"
# 1. scan
npx tsx bin/atlas.ts --repo /Users/me/Projects/app --out /Users/me/Projects/app/.visual/atlas
# 2-4. curate atlas.domains.json; read the code; enrich the draft JSON per the catalog
# 5. re-render + open, fix warnings
npx tsx bin/atlas.ts --all /Users/me/Projects/app/.visual/atlas --out /Users/me/Projects/app/.visual/atlas
open /Users/me/Projects/app/.visual/atlas/atlas.html
The bundled base atlas/domain reference build:
$VISUAL_SKILLS_DIR/example/atlas-ppgl/ (atlas.{json,html} + domain-<slug>/domain-<slug>.{json,html})