| 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;
- — edges crossing node boundaries, mapped to ;
keep only if both method names survived into their nodes' . Bucket each side's to its
node by longest prefix.
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).