# Archdoc

> Generate or refresh an architecture walkthrough doc for a subsystem or feature branch. A top-down text walkthrough with verified clickable file:line links, mechanically extracted public API, invariants, and a debugging map. Use when asked to map, document, or explain the architecture of a subsystem, feature, or branch, or to refresh an existing archdoc after changes.

- Skill: `astashov/archdoc` (Agent Skill)
- Install (CLI): `npx skillmds@latest add astashov/archdoc`
- Raw SKILL.md: https://api.skillmd.com/api/skills/astashov/archdoc/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: astashov (https://skillmd.com/u/astashov)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/astashov/archdoc

---


# archdoc, architecture walkthrough docs

Produce a markdown doc the reader uses instead of reading the implementation: data structures, interfaces, function signatures, and how data flows through them, from entry point down to details. They review diffs of this doc to track architecture changes, and ask questions about it inline. Answers get folded back into the text.

Where the docs live and what a snapshot contains is per-repo configuration. Run `grasp archdoc config` first. When it reports no `grasp.json`, ask the user whether to create one with `grasp archdoc init` instead of guessing a location.

## Ground rules

- **Text first.** Plain language ("we load X, pass these props; under the hood it calls Y to get Z"), verbatim signatures in code spans inside the Public API list, simple ASCII sketches only. No flow diagrams.
- **Every reference is a link whose text names the thing.** Write `[startTimer](...)`, never a link whose text is a line number. When no symbol sits at the line, name the behaviour: `the fall-through`, `the 250ms tick`.
- **Write links as plain relative paths while drafting.** `[startTimer](../../src/models/timer.ts#L1511)`, resolved from the doc's own directory. `grasp archdoc relink` converts them at the end. Never hand-write a `vscode://` URL.
- **Link a range when the thing is a block, a single line when it is a place.** `#L1220-L1232` arrives selected, so the reader sees the whole union, switch or guard instead of its first line and a guess about where it ends. Use a range for a type body, a `switch` or `if` chain the sentence is about, or a multi-line guard. Use a single line for a function you are naming, a call site, or a constant. Do not range a whole function body, because the diff route already shows that.
- **Fences carry no links and no line numbers**, since markdown renders no links inside a code fence. Never write `// :N`. A fence shows shape and the list around it owns navigation.
- **Individual function bodies are not the content.** What matters: types, signatures, who calls whom, where decisions and state live, and what must stay true.
- When the repo states its own writing rules, in a `CLAUDE.md` or a style doc, follow them for prose. This skill governs structure and links, not voice.

## Verify every anchor before you link it

Exploring agents report stale or wrong line numbers routinely. One agent described a 730-line shape of a file that was 322 lines after refactors; another time the branch gained a method between two verification passes in the same session. So:

1. Never link a line number that came from an agent report or from memory.
2. Before writing, grep-verify every anchor you will link: `grep -n "<distinctive signature text>" <file>`. Batch many patterns per call with `|`.
3. Read small key files, under about 400 lines, directly instead of trusting summaries of them.
4. A link that lands on the wrong line tells the reader the doc has drifted from the code, which only holds when every link was correct when the doc was written.

## Extract the public API mechanically, never by hand

For the Public API section, run this over the feature's files. It is written for TypeScript, so adapt the closing heuristics for other languages.

```sh
awk '
FNR==1 { mode=""; inblock=0; print "\n==== " FILENAME }
/^export / && !inblock { inblock=1; mode = ($0 ~ /^export (interface|type) /) ? "iface" : "fn" }
inblock { print FNR": "$0 }
inblock && mode=="iface" && /^export type .*;[ ]*$/ { inblock=0 }
inblock && mode=="iface" && /^}/ { inblock=0 }
inblock && mode=="fn" && /[{;][ ]*$/ { inblock=0 }
' <files...>
```

## Process

**1. Scope.** For a branch: `git diff --stat $(git merge-base <default-branch> HEAD)..HEAD`, plus `git log --oneline` for the commit narrative. For a subsystem: identify its file surface first.

**2. Explore top-down.** Follow the chain from entry point downward, collecting verbatim types and signatures with file:line and two to four sentences of narrative per hop. Commit messages are good hop hints. When a hypothesis turns out wrong, record what exists instead.

**3. Verify.** Grep every anchor, read the small key files, run the API extraction.

**4. Write** in this structure. Sections are named and referred to by name, never by number.

```
# <Title> (branch `<name>` - if branch-scoped)
*base `<merge-base sha>` - head `<sha or the word working>` - <date>*

**The feature in one paragraph.** What it is from the user's point of view, then
the one-sentence architectural shape.

ASCII layer stack (entry point at top, leaves at bottom, one annotation per
layer). This one fence stays as art, since nothing in it needs clicking.

The rule that organizes everything: **<the design's one organizing principle>**

## Public API
Per file, stack order, as a LIST, never a fenced signature block, because a
fence kills the links. One bullet per symbol: linked name, a colon, the
signature in a code span, then an indented line saying what it owns.

  ### [src/models/timer.ts](../../src/models/timer.ts)

  - [`getActiveTimer`](../../src/models/timer.ts#L1240): `(state) => Timer | undefined`
    The single read model. Four presenters moved onto it.
  - [`Timer`](../../src/models/timer.ts#L1220-L1232): the union every presenter reads
    A type body is a block, so it gets a range and arrives selected.

End with "Host integration points": pre-existing functions and props the feature
hooks into. This section is the canonical home of every signature, and narrative
points here.

## How it runs
One block per user-visible scenario, not per file: "tap play", "countdown
expires", "wake from a 10-minute sleep". Plain indentation inside a fence, with
no bullets, no box-drawing and no line numbers. Call tree for who calls whom,
pseudocode for a function whose branches are the point. Every symbol named here
must be linked in the Public API section.

## <Named sections>. The cascade
One section per hop, entry point first: what this layer owns, what it hands down
(the seam or contract verbatim when small), what it calls below and why. Deep
links on every claim. Prose carries the why only, and never restates an edge of
a call tree.

## Things that must stay true
Numbered invariants a diff must be judged against, each with its enforcing
location linked and one sentence on what breaks when it is violated. Tie known
past bugs to the invariant they violated.

## Where to look when something breaks
Symptom to starting file or function, as a list. Bold the symptom, link the
destination.
```

**5. Lint.** Run `grasp archdoc lint <path>` and fix every error. It checks each anchor against the file as of the doc's `head` sha, so a dead path or a line past end-of-file means the doc makes a claim the code no longer supports.

**Documenting work that is not committed yet.** Stamp `head` with the word `working` instead of a sha: `*base `<sha of HEAD>` - head `working` - <date>*`. Anchors then resolve against the files on disk, so a doc about a new file that exists in no commit lints clean, and `git diff <base>` classifies your uncommitted edits for routing. The doc stays reviewable while the branch is open, and lint prints one `working-stamp` warning to say the anchors move with your next edit. Any file that does not exist at `base` routes to the file view rather than a diff, whether it is untracked, staged, or the new path of a rename, because the diff's left pane would have nothing to show. Once the work is committed, replace `working` with the new sha and run `grasp archdoc relink <path> --write` again. That is the whole re-stamp.

**6. Relink.** Run `grasp archdoc relink <path>` to see the split, then again with `--write`. It rewrites every relative link into one of two routes the graspcode extension handles, choosing by whether the target changed between the stamped commits:

- Inside a function that `base..head` touched, the link becomes `archdoc/diff` and opens a two-pane diff at that line. Both sides are real files in sparse worktrees, so go-to-definition works inside the diff.
- Untouched code becomes `archdoc/open`, the plain file at `head`. Host integration points and pre-existing invariants read better as files than as a diff of a file that did not change.

It decides with `git diff -W`, so a function's declaration counts as changed when its body changed, while a pre-existing function in the same file does not. Directory links and links to other docs stay relative, and a `#L1220-L1232` range carries through as `line=1220-1232`.

This is why the header stamp needs both shas, and why anchors keep working in an old doc: `head` pins every link to the commit the doc describes, so links do not rot as the code moves. Re-run it after any refresh that changes the stamp.

**7. Report** the path, plus anything surprising the mapping surfaced, such as drift, asymmetries or weak spots.

## Refresh mode, when the doc already exists

1. Grep-verify the doc's existing anchors against the working tree, and list which moved or broke.
2. Re-run the API extraction and diff it against the Public API section. New, removed and changed exports drive which narrative sections need updating.
3. Update the header stamp's `base` and `head`, then re-run `grasp archdoc relink <path> --write` so every link points at the new commits.
4. Fold in any clarifications the reviewer asked for since. A question in the grasp inbox marks a place the doc failed to explain, and the answer belongs in the text.
5. Keep edits minimal. The reader reviews the doc's diff, so a 40-line rewrite for a 3-line change hides what changed.

## Snapshots

The diff route reads from sparse git worktrees under the configured snapshots directory. They are created on demand, so nothing needs doing up front. To reclaim disk, `grasp archdoc snapshot --prune` removes every snapshot no live doc still references.

