# Archlet

> 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.

- Skill: `superdesigndev/archlet` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add superdesigndev/archlet`
- Raw SKILL.md: https://api.skillmd.com/api/skills/superdesigndev/archlet/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: superdesigndev (https://skillmd.com/u/superdesigndev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/superdesigndev/archlet

---


# 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:

```js
/* GENERATED by the archlet skill. */
window.ARCH = {
  config: { project, layers: { <key>: { label, border, ink, fill? }, ... } },   // project = repo name (viewer title); layers = the taxonomy YOU choose
  nodes: [{ id, name, g, parent?, brief, role, rt, path, methods? }],   // g = a layer key
  edges: [{ s, t, k, l?, src, method }],                               // src = "file:line"
  methodEdges: [{ sNode, sMethod, tNode, tMethod, src }],              // method→method calls (step 5)
};
```

- **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:
1. **Scan structure** — find source roots (`apps/*`, `packages/*`, `src`); they become the top-level nodes. Read docs.
2. **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`.
3. **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**).
4. **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.
5. **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.
6. **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.
7. **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 `path`s.

**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`:
```js
window.ARCH = window.ARCH || {};
window.ARCH.pr = {
  label: "<human label>",          // "PR #565" · "commit a1b2c3d" · "working tree"
  title: "<optional>", url: "<optional>", number: <n>,  // number only for PRs (enables ?pr=<n>)
  add, del, files,
  nodes: { "<nodeId>": { files, add, del, paths:[...] }, ... },
  contains: [ "<ancestorNodeId>", ... ],
  links: {                          // OPTIONAL — connectivity the change alters
    add: [ { s, t }, ... ],         // a dependency/wire INTRODUCED
    cut: [ { s, t }, ... ],         // one REMOVED
  },
};
```
`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):
```js
window.DIFFS = {
  default: "<name>",
  available: [ { name, label, number?, add, del, files }, ... ],   // newest first
};
```

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).

