Visual Recap
Turn a git target (PR, commit, branch, or working tree) into a single self-contained HTML review narrative — and open it.
The deliverable is a guided narrative, never a bare pile of diffs. A reviewer (the author self-reviewing, or a cold teammate with zero context) must grasp what changed, why, and where the risk is in ~10 seconds, then be walked through the change in a deliberate order. The raw output of the gather step is raw material, not the deliverable — you are not done until it has been enriched and re-rendered.
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.
The standard — definition of done
A finished recap always has, regardless of change size:
- An authored title —
Recap — <human label>, notRecap — commit <sha>. - A lead
overviewblock, placed FIRST —headline+ TL;DRfacets(what / why / size) +risk(level + note). This populates the TL;DR card and the topbar risk chip — the 10-second orientation. This is required even for a one-file fix. - Every non-trivial diff annotated — a bold one-line takeaway + 2–5 bullets. Omit the description only for a genuinely trivial one-or-two-line change.
- A deliberate order, and — once there is more than a couple of diffs — diffs grouped into
a narrative (e.g. core change → supporting wiring → tests/config), each group with a
description. - At least one authored diagram when behavior or structure is clearer shown than read.
- For a behavior-changing PR, one contrast
example: the same input under old vs new code, mined from the PR's tests (ready-made input/output pairs) — or grounded in a documented reproduction when no test covers the case, said plainly insource— placed inside the group beside the diff it explains. The lint only counts diffs, so it nudges by size; you decide whether this PR is one where an example earns its place.
Size scales the ceiling, it never lowers the floor. A small change gets fewer overview points and maybe no diagram — but it still gets the headline, the what/why/size facets, the risk chip, and per-diff annotations. See "Scaling by size" below.
Red flags — you stopped too early
If any of these is true, the recap is not done — keep going:
blocks.jsonis empty, or is still the gathered blocks with nooverviewand nodescriptions.- The title is still
Recap — commit <sha>/Recap — <branch>. - There is no
overviewblock, or itsfacets/riskare unset. - Diff cards have no
description. - The PR changes behavior and no
exampleshows a concrete before/after. (The zero-example warning is a heuristic nudge off diff count, not a verdict — on a pure refactor or a docs-only PR it is fine to leave it standing.) - You ran only
--out(the one-shot bare render) and opened that. That output is raw material. - The tool printed warnings about a missing overview, incomplete TL;DR, unannotated diffs, an
ungrouped pile of diffs, or a wall-of-text description. Those warnings mean below standard —
fix
blocks.jsonand re-render until they are gone.
Workflow (one required path)
Identify the target repo (default: cwd), what to recap, and the output folder (
--outis a directory, e.g.<target-repo>/.visual/recaps/<short-label>, absolute path). Pick the target flag:- a pull request →
--pr <number>(needs theghCLI) - a commit/SHA/tag →
--commit <ref> - a branch →
--branch <name>(optionally--base <ref>) - uncommitted working changes → no target flag
- a pull request →
Gather the raw blocks into
blocks.json— this is raw material, not the deliverable:cd "$VISUAL_SKILLS_DIR" npx tsx bin/recap.ts --repo <ABSOLUTE_TARGET_REPO> <target flag> --emit-blocks <ABSOLUTE_OUT_DIR>/blocks.jsonRead the diff AND the changed code. Open the changed files in the target repo to understand what the change does and why — do not work from the diff text alone.
Author the lead
overviewblock, placed FIRST (step detail below). Always fillheadline,facets(what/why/size), andrisk.Annotate every non-trivial diff as a bold takeaway + bullets (step detail below).
Order and group the diffs into a narrative (step detail below).
Author the diagram(s) for the change (see "Which diagram(s) to add").
Render and open. This writes
recap.html(and re-writesblocks.json) back into the folder, so the doc folder stays self-contained and re-renders in place:npx tsx bin/recap.ts --blocks <ABSOLUTE_OUT_DIR>/blocks.json --title "Recap — <label>" --out <ABSOLUTE_OUT_DIR> open <ABSOLUTE_OUT_DIR>/recap.htmlRead the tool's warnings and close them. The renderer lints the blocks and prints warnings for anything below standard (missing overview, incomplete TL;DR/risk, unannotated diffs, an ungrouped pile, wall-of-text). Edit
blocks.jsonand re-run step 8 until the render is clean.
Scaling by size
The floor is the same for everyone; only the ceiling moves.
| Element | Small (1–2 files) | Medium | Large (many files, schema/API) |
|---|---|---|---|
overview headline + facets + risk |
required | required | required |
overview points |
1–3 | 3–5 | 4–6, keyword-linked |
per-diff description (bullets) |
required (unless truly trivial) | required | required |
group blocks |
optional (often one group, or none) | yes | yes, ordered as a narrative |
| lead diagram | optional | usually | yes |
| per-diff diagram | rare | when it clarifies | where logic is clearer shown |
A one-file fix is still a finished review: an overview with a one-line what/why/size + risk, and a
bulleted annotation on the diff. It is never just the bare diff.
Step detail
4 — Author the lead overview block (FIRST)
A scannable lead the reader groks in seconds:
{ "type": "overview", "id": "overview",
"headline": "Add PayPal capture to the checkout flow",
"points": [
{ "text": "new `capture` mutation on the [order router](#diff-0)" },
{ "text": "checkout calls it after buyer [approval](#diff-1)" }
],
"facets": { "what": "Capture funds when the buyer approves", "why": "Orders were authorized but never charged", "size": "8 files, ~154 runtime lines" },
"risk": { "level": "low", "note": "additive; no schema changes" },
"diagram": { "type": "diagram", "id": "ov-flow", "title": "Capture flow", "kind": "sequence",
"d2": "shape: sequence_diagram\nclient -> api: capture\napi -> paypal: capture",
"mermaid": "sequenceDiagram\n client->>api: capture\n api->>paypal: capture" } }
headline: the main change in ONE line.points: SHORT items (count scales with size — see the table). Link a keyword inline with markdown to its detail section, e.g.new \capture` mutation on the order router— do NOT rely on a barehref` that turns the whole bullet into one link (a keyword link reads far better).facets/risk: always set these — they populate the TL;DR card (What / Why / Risk / Size) and the topbar risk chip.facets:{ "what": "…one line…", "why": "…one line…", "size": "…e.g. 8 files, ~154 runtime lines…" }risk:{ "level": "low" | "med" | "high", "note": "…why, e.g. additive, no schema changes…" }
diagram(optional, scales with size): the single most illuminating illustration (often thewhere-it-fitsgraph or the key behavioral diagram). Carry itsmermaidto stay editable; don't point ahrefat a diagram hidden in a non-default tab.
For a very small change you may keep the points list short and drop the diagram, but the
headline, facets, and risk are still required.
5 — Annotate each diff (bullets, not a paragraph)
Set a diff block's description to markdown that leads with a one-line bold takeaway, then 2–5
- bullets of the specific changes, with inline code for identifiers. A multi-sentence
paragraph (≈300+ chars of prose) is a FAILURE — break it into bullets. Omit the description only
for a genuinely trivial one-or-two-line change. The code hunks are collapsed by default under a
"View changes" toggle, so the description is the primary read — make it carry the meaning.
Author every description in this shape (note the literal \n and - bullets in the JSON string):
"description": "**Two new queries — the data foundation.**\n\n- `findStandings` joins members → picks → results, so a member who didn't pick still yields a row\n- ordered by earnings DESC, then a stable tiebreak\n- `countStandings` counts active members — the pagination denominator\n\nConsumed by [the service](#diff-service)."
❌ Do NOT write it as one run-on string: "Two new queries. findStandings joins members to picks to results so a member who didn't pick still yields a row, ordered by earnings desc… and countStandings counts active members which is the denominator…" — that is the wall of text this format exists to
prevent.
Cross-link related diffs by id, e.g. [the router](#diff-router) (each block renders with
id="<its id>", so #diff-router jumps to that diff).
Illustrate a diff when it helps (distinct from the top-level change diagram — this one lives
inside a single diff's card). When a single diff implements logic that's clearer shown than read —
a new state machine, a non-obvious control/data flow, a sequence across collaborators — attach a
small catalog diagram as that diff's diagram:
"diagram": { "type": "diagram", "id": "diff-3-diag", "title": "Capture flow", "kind": "sequence",
"d2": "shape: sequence_diagram\napi -> paypal: capture(id)\npaypal -> api: ok",
"mermaid": "sequenceDiagram\n api->>paypal: capture(id)\n paypal-->>api: ok" }
- Restraint: most diffs need none — attach one only when it adds understanding the code alone doesn't.
- Keep it editable: copy the catalog recipe including its
mermaidsibling for any editable-eligible kind (flowchart/architecture/sequence/class), so the diagram renders as an editable Excalidraw scene; a d2-only diagram forfeits that (ERD stays d2-only by design). - Multiple views: if one diff genuinely needs more than one diagram, set
diagramto atabsblock of diagrams instead (default to a single diagram). A diagram inside a non-default tab is hidden until clicked, so don't make it a#cross-linktarget. - A
groupof diffs can likewise lead with adiagram(or atabs) block to illustrate the whole grouping.
6 — Order and group the diffs
Order the diffs so reading top-to-bottom is a narrative. Once there is more than a couple of diffs,
wrap them in group blocks, ordered by importance — e.g. The core change → Supporting wiring →
Tests & config. Every group MUST include a description (markdown, one or two scannable lines
on what the group covers and why it matters):
{ "type": "group", "id": "grp-core", "title": "The core change — query, shape, expose",
"description": "The SQL query, the service that guards and shapes it, and the tRPC procedure that exposes it.",
"blocks": [ …diff blocks… ] }
(One level deep — groups may not contain groups.) Place the groups after the lead overview, the
where-it-fits diagram, and the diagram(s).
Within each group, order the diffs most-important-first (the core logic change before its supporting wiring; styles, tests, config, and lockfiles last). The gather step already applies this ordering heuristically, but when you regroup, preserve importance order inside each group — never lead a group with a stylesheet or test file.
For a 1–2 file change, grouping is optional (a single group, or none) — but the overview and the
per-diff annotations are not.
Fallbacks
--prfails / nogh: the CLI prints "PR scope needs the gh CLI". Resolve the PR's merge or head commit SHA (e.g. viagh, the GitHub UI, orgit log) and re-run with--commit <sha>.d2missing: diagrams degrade to visible placeholders (the recap 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
opt in. Pass
--excalidrawto promote editable-eligible diagrams to.excalidrawscenes (requires thenpm run setup:excalidrawtoolchain); without it — or with--no-excalidraw— you get the static D2 floor (no sidecars).
Which diagram(s) to add
Consult the shared diagram catalog for the full selection guide and tested recipes:
$VISUAL_SKILLS_DIR/skills/shared/diagrams.md
Prefer the fewest diagrams that explain the change — often ONE. The catalog's Behavior and
Journey lenses cover most recaps (a sequence for a new runtime path; a state machine for a
lifecycle change). When 2–3 lenses each add distinct value (e.g. a sequence AND a data-flow),
present them in a tabs block instead of forcing one.
The mechanical where-it-fits import graph is rough orientation, not a deliverable. It is a
de-noised dependency graph the gather step produces automatically; it rarely tells a story. For any
change with a clear architectural home, replace it with a curated Feature home diagram (see the
catalog) — the changed pieces in their layer/stack (router → service → repository → datastore), with
reused existing modules called out and library/test noise dropped. Keep the same id
(where-it-fits) so it lands in the structural slot. Only for a small, single-file change with no
meaningful layering is a structural diagram unnecessary — then drop it rather than ship the raw graph.
Use the catalog's recipes verbatim (they are compile-tested), substituting real identifiers.
Apply the catalog's color vocabulary — always mark the changed subject, and tag actors /
external systems / datastores by role so the diagram reads at a glance.
Grouping multiple diagrams into tabs
A tabs block presents complementary views as a CSS-only switcher (no JS):
{ "type": "tabs", "id": "views", "title": "How it works", "tabs": [
{ "label": "Sequence", "block": { "type": "diagram", "id": "seq", "title": "captureOrder flow", "kind": "sequence",
"d2": "shape: sequence_diagram\nclient -> api: captureOrder(id)\napi -> paypal: capture(id)" } },
{ "label": "States", "block": { "type": "diagram", "id": "states", "title": "Order states", "kind": "flowchart",
"d2": "direction: right\nPENDING -> PAID: capture\nPENDING -> CANCELLED: cancel" } }
] }
Each tab holds ONE block (one level deep — a tab may not contain a group or another tabs). Place
the tabs near the top, after the lead overview and the where-it-fits diagram.
Tabs use a no-JS CSS switcher, so an inactive tab's panel is hidden until clicked. Keep
#-cross-link targets (e.g. a diff the lead links to) OUT of tabs — a jump to a block in an
inactive tab lands on a hidden panel. Put alternate views in tabs; keep linkable content in groups
or top-level blocks.
Example
cd "$VISUAL_SKILLS_DIR"
# 1–2. gather raw blocks (raw material, not the deliverable)
npx tsx bin/recap.ts --repo /Users/me/Projects/ppgl --commit 3559f61 \
--emit-blocks /Users/me/Projects/ppgl/.visual/recaps/pr-183/blocks.json
# 3–7. read the code; edit blocks.json — add the overview, annotate diffs, group, diagram
# 8. render + open
npx tsx bin/recap.ts --blocks /Users/me/Projects/ppgl/.visual/recaps/pr-183/blocks.json \
--title "Recap — estimated purse" --out /Users/me/Projects/ppgl/.visual/recaps/pr-183
open /Users/me/Projects/ppgl/.visual/recaps/pr-183/recap.html
# 9. fix any warnings the render printed, then re-render