ascii-explain
Traces a request, flow, or pipeline as an ASCII/box-drawing diagram in the terminal — real file:line anchors, real payloads, branch points marked, failure edges shown. Use when the user asks to "expla
SKILL.md.agents/skills/ascii-explain/SKILL.md
GitHubascii-explain
Draw the flow, in the terminal, from the actual code. One fenced block the user can
read top to bottom, with file:line anchors they can click.
The diagram's entire value is accuracy. Read the code first. A confident diagram with an invented line number is worse than prose, because it looks verified.
When this is the right tool
Reach for it when the answer is a sequence with branches: a request path, a lifecycle, a pipeline, two versions diverging through shared code. Skip it when a sentence covers it, or when the shape is a table (comparisons, config matrices) or a list (options, findings).
Adjacent skills: explain-with-html for a rich standalone artifact,
explain-simply for plain-language re-explanation with no code trace.
Before drawing
- Trace the real path end to end. Every hop: entry point, dispatch, each function, the external call, the terminal state. Grep for callers; don't assume.
- Pin line numbers immediately before writing. Grep the function names fresh — if the file was edited earlier in the session, remembered line numbers are stale.
- Get one real payload. The actual request body, the actual outbound call body. A
diagram with
{...}placeholders teaches nothing; the payload is often the punchline. - Find the branch points. These are the diagram. A straight line needs no picture.
Anatomy
Vertical, top to bottom, inside a single fenced block tagged text. Hard cap 78
columns — terminals wrap, and a wrapped diagram is unreadable.
The ← notes below label the technique; a real diagram wouldn't carry them.
┌─ entry ────────────────────────────┐
│ POST /api/v2/thing?flag=1 │ ← real path + query params
│ { id: "ABC123", name: "…" } │ ← real payload
└──────────────────┬─────────────────┘
▼
╔══════════════════════════════╗
║ Lambda Handler — idx.js:654 ║ ← ╔═╗ = major component
╚══════════════╤═══════════════╝
▼
getContext(event) apiUtil.js:12
├─ getVersion(event.path) → 2 ← the VALUE, not the call
└─ isFooRequest(path) → true
▼
handler:735 isFooRequest ──► doTheThing()
├─ :157 param present? else 400-422 ← failure edge, inline
└─ :163 validate(request)
▼
externalCall() client.js:75
POST {host}/v1/allocate
body → { environment, region, alu }
▼
── terminal state / response ──
Each step is functionName() plus a right-aligned file.js:LINE, or a bare :LINE
when it's the same file as the line above. Show resolved values (→ 2, → true),
not just that a call happens.
The four devices that carry the weight
1. Side-by-side entry, funneling into one. When comparing two paths through shared code, start them as parallel boxes and merge the connectors. The funnel is the claim: same code, different data.
┌─ v1 caller ──────────┐ ┌─ v2 caller ──────────┐
│ POST /v1/thing │ │ POST /v2/thing │
│ { name } │ │ { id: "X", name } │
└──────────┬───────────┘ └──────────┬───────────┘
└──────────┬──────────────┘
▼
2. ★ DIVERGENCE callouts. At each branch, name it, cite the line that decides, and
show what each side actually gets. This is where readers stop and learn.
★ DIVERGENCE 1 — schema picked by path substitution
util.js:85 format("{VERSION}/Thing.json")
v=1 → model/v1/Thing.json required: [name] UNTOUCHED
v=2 → model/v2/Thing.json required: [id, name] NEW
3. An explicit convergence bar. Once branches rejoin, say so loudly. It tells the reader they can stop tracking two things — and it's usually the most reassuring line in the whole answer.
═══════════ everything below is IDENTICAL for v1 and v2 ═══════════
4. Failure edges inline. else 400-422, !id → STATE_FAILED, bail. A happy-path-only
diagram hides exactly what the reader will hit in testing.
Nesting one function's internals
When a single function has several meaningful steps, indent them inside a │ … │ block
rather than flattening into the main spine:
┌─────────────────────────────────────────────────┐
│ validateAndGetData() utils.js:48 │
│ │
│ :53 validateRequest(request, event) │
│ :59 transformData(request) │
│ ├─ delete data.digital :87 │
│ └─ delete data.id :93 │
│ v1: no-op (can't exist) │
│ v2: strips it — id is for the │
│ external call only │
└─────────────────────┬───────────────────────────┘
Character palette
Keep it small and consistent:
| Use | Chars |
|---|---|
| Flow / connectors | │ ─ ├ └ ┬ ▼ |
| Entry & detail boxes | ┌ ┐ └ ┘ ─ │ |
| Major component (handler, service) | ╔ ╗ ╚ ╝ ═ ║ |
| Convergence bar / emphasis | ═══ |
| Branch marker | ★ |
| Dispatch arrow, inline | ──► |
| Value resolution | → |
Don't add decoration that carries no information. Every box should exist because something is inside it.
After the diagram
Two or three short observations — each a claim the diagram makes obvious that prose would have buried. Not a summary; the diagram is the summary.
Good ones name a consequence:
The divergence is entirely upstream of the business logic. Once
allocatedIDexists there is one code path — which is why v2 needed zero handler changes.
request.idnever reaches the record. Read once at:72, forwarded, deleted at:93. The stored id is whatever the external service returned, applied at:834.
Then stop. No feature tour, no restating the boxes in sentences.
Don't
- Draw from memory, or reuse line numbers from before an edit. Grep, then draw.
- Sprawl left-to-right. Terminals scroll vertically.
- Exceed 78 columns.
- Diagram a straight line with no branches — write the sentence instead.
- Invent a
file:lineyou didn't verify. Omit the number rather than guess it. - Narrate the diagram afterward in prose. It already said it.
Other shapes
The vertical trace is the default because most questions are "how does X get to Y". When the shape genuinely differs, match it: side-by-side columns for layered architecture, a state table plus transition arrows for state machines, a fan-out tree for queue/worker dispatch. Same rules — real anchors, marked branches, 78 columns, hard stop after the observations.