| name | archlet |
| description | Interactive, provenance-backed architecture maps in .archlet/. Two modes: MAP — analyze the whole codebase into .archlet/data.js and seed a starter overlay from a recent change; DIFF — given a PR, commit, range, branch, or working-tree changes, overlay what they touch. Use when the user asks to map / diagram / visualize a codebase's architecture, refresh the map, or show what a PR / commit / change touches. |
archlet
You produce files under .archlet/ that the viewer renders. Pick the mode from the request:
- MAP — "map / visualize the architecture", "refresh the map", no specific change given.
- DIFF — a change is referenced (PR, commit, range
A..B, branch, or "my changes / staged").
Needs an existing .archlet/data.js — run MAP first if absent.
Nodes form a tree of any depth via parent: top-level nodes (source roots / apps) contain modules,
which can nest further when a module is big enough to warrant it. Keep it shallow — add a level only when it
earns its keep; the viewer expands/collapses at every level. Always finish by launching the viewer (see end).
Mode 1 — MAP → .archlet/data.js
The single source of truth the viewer reads:
window.ARCH = {
config: { project, layers: { <key>: { label, border, ink, fill? }, ... } },
nodes: [{ id, name, g, parent?, brief, role, rt, path, methods? }],
edges: [{ s, t, k, l?, src, method }],
methodEdges: [{ sNode, sMethod, tNode, tMethod, src }],
};
- node —
id internal unique slug (never shown; namespace it, e.g. coding-tools vs foundation-tools) ·
name short heading on the card · g a layer key · parent the containing node's id, nested as deep as the
structure needs (omit for top-level nodes) ·
brief ≤ ~6-word tagline · role one sentence · rt key file(s) · path dir relative to repo root ·
methods (optional, leaf nodes) [{ name, src:"file:line", calls? }] — the top ~3–6 functions by call count.
- edge.k — a short lowercase kind YOU choose (static
import, runtime wire, app→engine…); keep the
vocabulary small so the viewer can style by kind. method: mechanical (tool) | manual (grepped).
Containment is implicit via parent — never emit containment edges.
- edge.l (optional) — what flows over a wire edge (
messages.create, POST /webhook); set it only where
it adds signal, never a count (3×).
- methodEdge — a call from one node's method to another's.
sMethod/tMethod must exist in those nodes'
methods and src is the call site, else the edge is dropped.
Procedure:
- Scan structure — find source roots (
apps/*, packages/*, src); they become the top-level nodes. Read docs.
- Choose the layer taxonomy (the key judgment) — a few layers, calm palette, e.g.
frontends / control-plane / runtime / shared-pkg / external. Put them in
config.layers.
- Extract the mechanical floor — required, via
npx:
- Routes + import/call graph (codegraph):
npx -y @colbymchenry/codegraph@latest init -i, then
npx -y @colbymchenry/codegraph@latest query "" --kind route --limit 99999 --json for the route
inventory. The import + call graph lives in .codegraph/codegraph.db (query it directly).
- Import graph (madge), JS/TS roots only:
npx -y madge@latest --json --ts-config <root>/tsconfig.json <root>/src,
then collapse file→file edges to module→module. Non-JS/TS roots: use that language's tool
(go mod graph, pydeps) or codegraph's import graph instead.
Build on these — only drop to Grep/Read for what a static tool can't see (most wire/cross-process edges).
- Build — use a workflow, one agent per major module. Split the repo into its handful of large/important
modules (in a monorepo, each app/package; in a single package, the main subsystems) and give each its own
agent. Each agent picks core modules (routes/controller/plugin/tool/store/service, or doc-named), emits
intra-module import edges (
mechanical), and stitches wire edges out of its module by matching client
call sites (fetch/SDK/db/proxy/queue) to a target or external node, each with a real file:line (manual).
Wire edges are NOT in the import graph and are the most valuable.
- Methods + methodEdges — from codegraph's call graph (
.codegraph/codegraph.db; don't skip — this is
what makes nodes legible). Derive mechanically, don't invent names:
node.methods — top functions defined under each leaf node's path, ranked by call in-degree:
SELECT n.name, n.file_path||':'||n.start_line AS src, (SELECT COUNT(*) FROM edges e WHERE e.kind='calls' AND e.target=n.id) AS calls FROM nodes n WHERE n.kind IN ('method','function') AND n.file_path LIKE '<node.path>/%' ORDER BY calls DESC LIMIT 6;
methodEdges — calls edges crossing node boundaries, mapped to { sNode, sMethod, tNode, tMethod, src };
keep only if both method names survived into their nodes' methods. Bucket each side's file_path to its
node by longest path prefix.
- Write, then validate — write
.archlet/data.js, run npx archlet validate (checks shape +
referential integrity), fix what it reports, re-run until clean. Spot-check wire edges against their
file:line — drop, don't fabricate.
- Seed a starter diff — by default. A fresh map is static; overlaying a real recent change shows the DIFF
feature working and where activity is concentrated. Pick one coherent change (prefer a recently-merged PR via
gh pr list --state merged --limit 15 --json number,title,additions,changedFiles,url; else the largest
meaningful commit from git log), discounting lockfile / generated / dist noise. Build it as in Mode 2,
register it in the manifest but leave default null so the clean map opens first. Skip silently if history is
too thin (shallow clone, lone initial commit, only trivial changes); never fabricate one. Mention it on handoff.
Launch the viewer — npx archlet view is a blocking server that auto-opens the browser; start it in the
background and tell the user it's at http://localhost:4173 (stop the process to shut it down).
Mode 2 — DIFF → .archlet/diffs/<name>.js
Overlay the files a change touches onto the existing map. Read .archlet/data.js first for node paths.
1. Get the changed files (each: path, additions, deletions):
| source | command |
|---|
| PR | gh pr view <n> --repo <owner/name> --json files,title,additions,deletions,changedFiles,url,number |
| commit | git show --numstat --format= <sha> |
| range / branch | git diff --numstat <A>..<B> (use <base>...<head> for branch-vs-merge-base) |
| working tree / staged | git diff --numstat · git diff --numstat --staged |
--numstat lines are <add>\t<del>\t<path> (- = binary).
2. Map files → nodes. For each path, find the node whose path is the longest prefix; else fall back to
its top-level root/app node; else report it unmapped. Aggregate per node { files, add, del, paths }. Compute
contains = ancestor node ids of every changed node (walk parent).
3. Write the overlay .archlet/diffs/<name>.js:
window.ARCH = window.ARCH || {};
window.ARCH.pr = {
label: "<human label>",
title: "<optional>", url: "<optional>", number: <n>,
add, del, files,
nodes: { "<nodeId>": { files, add, del, paths:[...] }, ... },
contains: [ "<ancestorNodeId>", ... ],
links: {
add: [ { s, t }, ... ],
cut: [ { s, t }, ... ],
},
};
links.add renders as a green + arrow, links.cut as a dashed ✕ — between the nodes' visible ancestors. Only
fill links when the change actually adds/removes a node→node connection (a new/removed import, fetch, db/proxy
call); omit for internal-only changes. <name>: pr-<n>, else commit-<sha7> / wip / staged.
4. Register it in .archlet/diffs/manifest.js (create if missing; keep existing entries):
window.DIFFS = {
default: "<name>",
available: [ { name, label, number?, add, del, files }, ... ],
};
Run npx archlet validate, fix any issues, then launch the viewer in the background (as above) at
http://localhost:4173. The viewer has a Diffs menu; switch via the menu, ?pr=<n>, or ?diff=<name>.
Rules
- Provenance or it didn't happen — every wire edge needs a real
file:line; never invent edges.
- You decide the taxonomy (small);
data.js is the single source of truth and a draft the user can hand-edit.
- Always end by opening the map in the background, whichever mode you ran — don't leave a command to paste.
- Monorepo gotchas: CommonJS/NestJS DI deps aren't plain imports; apps importing another app's built
dist;
dynamic plugin/route registration (invisible to static tools — read the registrar).