feat(baoyu-diagram): add SVG-to-PNG @2x conversion script and consolidate references

This commit is contained in:
Jim Liu 宝玉
2026-04-15 21:33:56 -05:00
parent 25b8a7f73d
commit 39792f4360
25 changed files with 586 additions and 6223 deletions
+61 -276
View File
@@ -1,303 +1,88 @@
# Sequence Diagram
# Sequence Diagram Layout
For multi-actor protocols and conversations — "who talks to whom, in what order". A row of actor headers at the top, dashed lifelines dropping down, and numbered horizontal message arrows between them. The classic UML sequence diagram, adapted to this skill's design system.
## Core Elements
## When to use
| Element | Visual | Description |
|---------|--------|-------------|
| Actor/Participant | Box at top + dashed vertical lifeline | Each entity in the interaction |
| Sync message | Solid arrow → | Request or call |
| Async message | Open arrowhead → | Fire-and-forget |
| Return message | Dashed arrow ← | Response |
| Activation bar | Narrow filled rect on lifeline | Entity is processing |
| Self-message | Arrow looping back to same lifeline | Internal processing |
| Note | Rounded rect with folded corner | Annotation |
| Alt/Opt frame | Dashed boundary with label tab | Conditional block |
| Loop frame | Dashed boundary with "loop" tab | Repetition |
- Authentication and authorization flows — OAuth, OIDC, SAML, Kerberos, JWT refresh, mTLS handshakes
- Wire protocols — TCP three-way handshake, TLS handshake, WebSocket upgrade, HTTP/2 multiplex
- RPC patterns — gRPC unary and streaming, JSON-RPC, GraphQL over websockets
- Webhooks, callbacks, and event dispatch — stripe webhooks, GitHub events, PubSub fan-out
- Any "request + response" dance between 24 named services where *order* and *who initiates what* is the point
## Layout Algorithm
**Trigger verbs**: "protocol", "handshake", "auth flow", "request/response", "exchange between", "round trip", "who calls what", "API dance".
1. **Place actors** horizontally across the top, evenly spaced (150-200px apart)
2. **Draw lifelines** as vertical dashed lines from each actor box downward
3. **Place messages** as horizontal arrows between lifelines, top to bottom in time order
4. **Vertical spacing** between messages: 40-50px
5. **Activation bars:** 10px wide, centered on lifeline, spanning from incoming to outgoing message
**When not to use**:
- Single-actor sequential process → `flowchart.md` ("what steps does the build pipeline run")
- Containment / architecture / "what lives inside what" → `structural.md`
- "Feel how X works" / mechanism intuition → `illustrative.md`
**Routing test**: if the prompt names ≥2 distinct actors/participants/services (User + Server, Client + Auth + Resource, Browser + CDN + Origin), prefer sequence even when the noun is "flow" or "process". Single-actor "X flow" stays flowchart.
## Planning before you write SVG
1. **List the actors**, 24 of them, in the order they appear across the page (usually left = initiator, right = downstream services). Each actor needs a short title (≤12 chars for N=4) and an optional single-line role subtitle.
2. **Assign one color ramp to each actor**. Default palette: `[c-gray, c-teal, c-purple, c-blue]` left-to-right. Swap in `c-coral`, `c-pink`, `c-amber`, `c-green` as needed — pick cool/neutral ramps and avoid semantic collisions (green for success, red for error) unless the actor literally means that.
3. **List the messages** as ordered tuples `(sender, receiver, short label)`. Cap at 10 messages — 68 is the sweet spot. If your protocol has more, split into an overview diagram and one detail diagram per sub-flow.
4. **Mark self-messages** — messages that stay on the same lifeline (e.g., "validate token" on the resource server). Limit to 12 per diagram.
5. **Plan a side note** — a short title that names the protocol. For N ≤ 3, place it at top-left beside the first actor header. For N ≥ 4, place it at bottom-left under the lifelines.
6. **Check every message label** against its lane span using the budget formula in `layout-math.md`. Shorten aggressively — labels that barely fit today will overflow if you tweak the actor order.
Save the plan to `plan.md` alongside the SVG so the next iteration can read it.
## Actor headers
Each actor header is a two-line node (title + role) or a single-line node (title only), wrapped in a `c-{ramp}` group. Height is always 48, y=40..88.
Single-line (title only):
## Actor Box
```svg
<g class="c-gray">
<rect x="40" y="40" width="120" height="48" rx="8"/>
<text class="th" x="100" y="64" text-anchor="middle" dominant-baseline="central">User</text>
</g>
<!-- Actor box -->
<rect x="X" y="20" width="130" height="45" rx="6" fill="#0f172a"/>
<rect x="X" y="20" width="130" height="45" rx="6" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<text x="CX" y="47" fill="white" font-size="11" font-weight="600" text-anchor="middle">Actor Name</text>
<!-- Lifeline -->
<line x1="CX" y1="65" x2="CX" y2="BOTTOM" stroke="#334155" stroke-width="1" stroke-dasharray="6,4"/>
```
Two-line (title + role):
## Message Arrows
```svg
<g class="c-teal">
<rect x="200" y="40" width="120" height="48" rx="8"/>
<text class="th" x="260" y="60" text-anchor="middle" dominant-baseline="central">Client app</text>
<text class="ts" x="260" y="78" text-anchor="middle" dominant-baseline="central">Web</text>
</g>
<!-- Sync message (solid arrow) -->
<line x1="FROM_CX" y1="Y" x2="TO_CX" y2="Y" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="MID_X" y="Y-8" fill="#e2e8f0" font-size="9" text-anchor="middle">methodCall()</text>
<!-- Return message (dashed arrow, reversed direction) -->
<line x1="TO_CX" y1="Y" x2="FROM_CX" y2="Y" stroke="#64748b" stroke-width="1" stroke-dasharray="6,3" marker-end="url(#arrow)"/>
<text x="MID_X" y="Y-8" fill="#94a3b8" font-size="8" text-anchor="middle" font-style="italic">response</text>
<!-- Self-message (loop arrow) -->
<path d="M CX,Y L CX+40,Y L CX+40,Y+25 L CX,Y+25" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="CX+45" y="Y+15" fill="#e2e8f0" font-size="8">process()</text>
```
Rules:
- Header width matches `header_w` from `layout-math.md` (e.g., 120 for N=4).
- Header x = lane center header_w/2.
- Title `y = 60` when role is present, `y = 64` when title-only (centered in the 48px box).
- Role `y = 78`, always `ts`.
- Sentence case. "Auth server", not "Auth Server".
### Pill-shaped actor header (optional)
An actor that represents an **external participant**`Human`, `User`, `Environment`, an input source, an output sink — can use a pill-shaped header (`rx = height / 2 = 24`) instead of the default rounded rect. The capsule shape visually separates "outside the system" from "inside the system" without using color.
## Activation Bar
```svg
<g class="c-gray">
<rect x="40" y="40" width="120" height="48" rx="24"/>
<text class="th" x="100" y="64" text-anchor="middle" dominant-baseline="central">Human</text>
</g>
<rect x="CX-5" y="START_Y" width="10" height="H" rx="2" fill="rgba(8,51,68,0.6)" stroke="#22d3ee" stroke-width="1"/>
```
Rules:
- **Only for external actors.** Internal services (Auth server, Client app, Resource server) keep the `rx="8"` rectangle. Mixing pills and rects in the same diagram is fine as long as the distinction tracks "external vs internal" — if every actor is a pill, drop them all back to rectangles.
- **Height is still 48.** `rx` must equal `height / 2 = 24`. Don't adjust the height.
- **No role subtitle** inside a pill. The capsule is for actors whose name is self-explanatory (Human, Environment). If you need a subtitle, you want a rectangle, not a pill.
- **At most 2 pills per diagram.** More than that and the visual contrast between external and internal actors disappears.
## Lifelines
One `<line>` per actor, using `class="lifeline"`, from y=92 down to the tail below the last message.
## Conditional / Loop Frames
```svg
<line class="lifeline" x1="100" y1="92" x2="100" y2="540"/>
<line class="lifeline" x1="260" y1="92" x2="260" y2="540"/>
<line class="lifeline" x1="420" y1="92" x2="420" y2="540"/>
<line class="lifeline" x1="580" y1="92" x2="580" y2="540"/>
<!-- Frame boundary -->
<rect x="X" y="Y" width="W" height="H" rx="4" fill="none" stroke="#64748b" stroke-width="1" stroke-dasharray="4,3"/>
<!-- Frame label tab -->
<rect x="X" y="Y" width="50" height="18" rx="4" fill="rgba(30,41,59,0.8)" stroke="#64748b" stroke-width="1"/>
<text x="X+25" y="Y+13" fill="#94a3b8" font-size="8" font-weight="600" text-anchor="middle">alt</text>
<!-- Condition text -->
<text x="X+60" y="Y+13" fill="#94a3b8" font-size="8" font-style="italic">[condition]</text>
<!-- Divider line for else -->
<line x1="X" y1="MID_Y" x2="X+W" y2="MID_Y" stroke="#64748b" stroke-width="1" stroke-dasharray="4,3"/>
<text x="X+10" y="MID_Y+13" fill="#94a3b8" font-size="8" font-style="italic">[else]</text>
```
All lifelines share the same y2. Compute once: `y2 = last_arrow_y + 24`.
## Numbering
## Messages
Each message is a single `<line>` at its row's `arrow_y`, with a `ts` label above. The stroke class encodes the sender's actor ramp.
For complex sequences (8+ messages), number each message:
```svg
<!-- arrow row 0, arrow_y = 120 -->
<text class="ts" x="180" y="110" text-anchor="middle">1. Click login</text>
<line class="arr-gray" x1="106" y1="120" x2="254" y2="120" marker-end="url(#arrow)"/>
<!-- arrow row 1, arrow_y = 164, right-to-left -->
<text class="ts" x="180" y="154" text-anchor="middle">3. Show consent screen</text>
<line class="arr-purple" x1="254" y1="164" x2="106" y2="164" marker-end="url(#arrow)"/>
<circle cx="FROM_CX-15" cy="Y" r="8" fill="rgba(59,130,246,0.3)" stroke="#60a5fa" stroke-width="1"/>
<text x="FROM_CX-15" y="Y+3" fill="#60a5fa" font-size="7" font-weight="600" text-anchor="middle">1</text>
```
Rules:
- Label `y = arrow_y 10`, centered between sender and receiver x.
- Arrow `y1 = y2 = arrow_y`.
- Arrow endpoints are offset ±6 from the lifeline x so the arrowhead doesn't touch the dashes:
- Left-to-right: `x1 = sender_x + 6`, `x2 = receiver_x 6`
- Right-to-left: `x1 = sender_x 6`, `x2 = receiver_x + 6`
- Stroke class = `arr-{sender_ramp}``arr-gray` for User, `arr-teal` for Client app, etc. Never use `class="arr"` (the default mono-gray arrow class) on a sequence message.
- Label text is sentence case with a numeric prefix: `"1. Click login"`, `"3. Show consent screen"`. Numbers make the reading order unambiguous when arrows cross.
- Keep labels short — verify every label fits its lane span using the budget formula in `layout-math.md`.
## Color Assignment
## Self-messages
A self-message stays on one actor's lifeline. Draw it as a small 16×24 rect on the lifeline, colored in that actor's ramp, with the label to the **left** (so it stays inside the diagram even for the rightmost lane).
```svg
<!-- self-message on Resource server at arrow_y = 472 -->
<rect class="c-blue" x="572" y="460" width="16" height="24" rx="4"/>
<text class="ts" x="564" y="472" text-anchor="end" dominant-baseline="central">9. Validate token</text>
```
Rules:
- `rect_x = lifeline_x 8` (rect is 16 wide, centered on the lifeline).
- `rect_y = arrow_y 12` (rect is 24 tall, centered on the arrow row).
- Label `x = rect_x 8`, `text-anchor="end"`, `dominant-baseline="central"`, `class="ts"`.
- Label still gets its numeric prefix (`"9. Validate token"`), following the overall message order.
- Limit self-messages to 12 per diagram — more than that and the metaphor of "an actor talking to itself" stops reading clearly.
## Message frames
A **frame** is a dashed rounded rect that visually groups a contiguous run of messages under a single condition — *"Until tasks clear"*, *"Iterative research loop"*, *"Only if authenticated"*. It reads like a bracket around the enclosed messages, not like a new container. Anthropic's house-style sequence diagrams use frames for repeating or conditional sub-flows that don't warrant their own diagram.
```svg
<rect x="90" y="108" width="490" height="112"
rx="8" fill="none" stroke="#B4B2A9" stroke-width="1"
stroke-dasharray="5 4"/>
<text class="ts" x="100" y="120" text-anchor="start" dominant-baseline="central">Until tasks clear</text>
```
Rules:
- **Dashed rounded rect**, `rx="8"`, 1px stroke in the muted gray (`#B4B2A9` light / `#5F5E5A` dark — match `.lifeline`). Use inline `stroke-dasharray="5 4"` rather than a new CSS class.
- **Horizontal span**: `x = leftmost_lane_x 20`, `width = rightmost_lane_x leftmost_lane_x + 40`. The frame stretches 20px past the outermost lanes it covers, so the label has clear air at the corner.
- **Vertical span**: `y = first_arrow_y 12`, `height = last_arrow_y first_arrow_y + 24`. Tight enough that the frame feels like a bracket, not a new section.
- **Label position**: top-left inside the frame, at `(frame_x + 10, frame_y + 12)`, `class="ts"`, `text-anchor="start"`, `dominant-baseline="central"`. Keep the label ≤24 characters.
- **No numeric prefix** on the frame label — numbering is for individual messages. The frame is meta.
- **At most 2 frames per diagram**, and **frames never nest**. Two frames can sit one above the other, but they cannot contain each other. If you need nested scoping, split into two diagrams.
- **Draw frames BEFORE the messages** in source order so the dashed border sits behind the arrows and labels.
When to use a frame:
- A **loop** — "Until tests pass", "Until tasks clear", "Iterative research loop"
- A **conditional block** — "Only if authenticated", "When cache miss"
- A **named sub-protocol** — "Token refresh", "Retry with backoff"
Don't use a frame to decorate a single message, and don't use one to group messages that have nothing in common — frames are a scoping tool, not a visual group.
## Side note
A titled annotation for the protocol itself. Uses `class="box"` with a `th` title and an optional `ts` subtitle.
Top-left (for N ≤ 3):
```svg
<rect class="box" x="40" y="40" width="240" height="48" rx="10"/>
<text class="th" x="60" y="60" dominant-baseline="central">OAuth 2.0</text>
<text class="ts" x="60" y="78" dominant-baseline="central">Authorization code flow</text>
```
Bottom-left (for N ≥ 4, because the top row is filled with actor headers):
```svg
<rect class="box" x="40" y="556" width="240" height="48" rx="10"/>
<text class="th" x="60" y="576" dominant-baseline="central">OAuth 2.0</text>
<text class="ts" x="60" y="594" dominant-baseline="central">Authorization code flow</text>
```
Budget: title ≤24 chars, subtitle ≤32 chars. The box is 240 wide, padding 20 per side → usable width 200 → ~24 `th` chars or ~28 `ts` chars.
## Layout patterns
### 2-actor request/response (N=2)
User asks, server responds. Two lanes at x=185 and x=495. Typical message count: 24. Side note at top-left.
### 3-leg handshake (N=2, M=3)
Classic TCP. Same layout as above, three messages going SYN → SYN-ACK → ACK. Color the initiator arrows in one ramp and the responder arrows in another — the alternation reads as the handshake.
### 4-actor OAuth-style flow (N=4, M=810)
User, Client app, Auth server, Resource server. The canonical shape. Lane centers at 100, 260, 420, 580. Side note at bottom-left. One self-message on the Resource server for the "validate token" step.
### Async webhook (N=2 or 3)
Sender fires an event, receiver acknowledges later. Often has a gap in the middle that you can skip — no need to draw "wait 30 seconds" as a message; just put consecutive messages in the natural reading order.
## Parallel independent rounds
A sub-pattern for topics where **multiple independent call/response rounds** happen in sequence but share no lifeline state — each round is its own contained exchange, and the diagram's job is to contrast the structure of the rounds rather than track continuous state on a lifeline. Image #12 (Tool calling vs. Programmatic tool calling) is the canonical case.
**When to use.**
- Comparing tool-call vs. programmatic-script patterns where each round is an independent call/response.
- Showing "N separate rounds of the same exchange" to communicate volume or variety.
- The right half of a 2-up comparison where each side shows a different protocol style.
**When not to use.**
- There's genuine shared state between rounds (conversation history, pending requests) — that's a regular sequence with full lifelines.
- There are only 2 actors and 24 messages — that's a regular 2-actor sequence.
- The rounds are conditional ("if X then do round 2") — that's a frame on a regular sequence.
### Geometry — stacked rounds variant
Each round is a **row-sized mini-exchange** between a fixed source on the left and a round-specific action box on the right. Rounds stack vertically with no shared lifeline. See `layout-math.md` → "Parallel rounds geometry".
| Element | Coordinates |
|--------------------|-------------------------------------------------|
| Source actor | `x=60 y=120 w=120 h=60` (persistent on left) |
| Round 1 action | `x=340 y=100 w=160 h=48 rx=6` |
| Round 2 action | `x=340 y=180 w=160 h=48 rx=6` |
| Round 3 action | `x=340 y=260 w=160 h=48 rx=6` |
| Row pitch | 80 (round_y increases by 80 per round) |
The source actor is drawn **once** at the top-left, not repeated per round. Its box height is just tall enough to span the first round's arrow row, with no lifeline extending below it — the absence of a lifeline is the signal that the source isn't carrying state across rounds.
**Why no lifeline.** A lifeline would visually suggest that state persists; in this pattern each round is a fresh stateless call. The source box itself is the anchor — the left edge of every arrow starts at `source_right_edge`, not at a lifeline x.
**Arrow pairs per round.** Each round is two arrows:
```svg
<!-- Round 1 -->
<text class="ts" x="260" y="115" text-anchor="middle">1. Call tool A</text>
<line class="arr-gray" x1="186" y1="124" x2="334" y2="124" marker-end="url(#arrow)"/>
<text class="ts" x="260" y="145" text-anchor="middle">2. Response</text>
<line class="arr-teal" x1="334" y1="134" x2="186" y2="134" marker-end="url(#arrow)"/>
```
Round k's arrow y values: `call_y = 100 + 24 + (k-1) × 80`, `response_y = call_y + 10`. Action box for round k: `y = call_y 24`.
**Action box colors.** Each round's action box **can** take a different ramp — this is the only place in a sequence diagram where color is allowed to cycle across messages. The reason: each round calls a *different tool* (file write, api call, db query), and the color communicates that variety. Use up to 3 different ramps; more than that and the reader can't remember what each ramp means.
Source actor ramp stays **constant** across all rounds (usually `c-gray` — "Claude" or "Agent").
### Script-wrapper variant
The right half of image #12 uses the same stacked-rounds shape but wraps **multiple action boxes inside a single tall script rect** that represents an inline program executing several tool calls. Uses the `script-icon` pattern from `glyphs.md`.
| Element | Coordinates |
|----------------------|-----------------------------------------|
| Source actor | `x=60 y=120 w=120 h=60` |
| Script wrapper | `x=240 y=100 w=140 h=260 rx=6`, `c-gray`|
| `{ }` label in wrapper | `(310, 124)`, `th` |
| Divider line | `(256, 140) → (364, 140)`, `arr` |
| Wrapped action 1 | `x=252 y=152 w=116 h=48 rx=4` |
| Wrapped action 2 | `x=252 y=212 w=116 h=48 rx=4` |
| Wrapped action 3 | `x=252 y=272 w=116 h=48 rx=4` |
| External action | `x=440 y=200 w=160 h=48 rx=6` (optional)|
The script wrapper replaces the individual action boxes with a single container that contains N smaller colored bands. Only the **script wrapper itself** gets arrows from the source — the inner bands are decorative, showing what's inside the script without each needing its own call arrow.
```svg
<!-- Single arrow from source to script -->
<line class="arr-gray" x1="186" y1="230" x2="234" y2="230" marker-end="url(#arrow)"/>
<text class="ts" x="210" y="220" text-anchor="middle">run script</text>
```
If the script calls an external tool (image #12's right half: a `file_system` external box on the far right), draw a single arrow from the script's right edge to the external box at the script's middle y.
### Side-by-side comparison
The most common use of parallel rounds is inside a 2-up structural subsystem layout (`structural.md` → "Subsystem architecture pattern"): left container shows the stacked-rounds variant, right container shows the script-wrapper variant. Each sibling container is 315 wide, so halve the coordinates above:
- Source actor inside container: `x = container_x + 20, w=100 h=50`
- Action box inside container: `x = container_x + 180, w=120 h=40`
- Round pitch inside container: 64
See `structural.md` → "Rich interior for subsystem containers" for how this interior type registers.
### Rules
- **No lifelines.** If you draw lifelines, you're describing a regular stateful sequence, not parallel independent rounds. Drop them.
- **Source actor appears once.** Never duplicate the source per round — that multiplies the visual noise and breaks the "same caller, different calls" intuition.
- **Numeric prefixes still apply.** Within each round the two arrows are numbered (`1. Call tool A`, `2. Response`), and the numbering resets per round (or continues across rounds, at your choice — document it in the side note).
- **≤3 rounds per diagram.** 4+ rounds runs out of vertical space and overwhelms the comparison. Use two diagrams if you need to show more.
- **No frames around rounds.** A frame is a scoping bracket for a loop or a condition; parallel rounds are already independent, so a frame would be redundant.
- **Action ramps cap at 3.** Each round can take a distinct ramp, but stop at 3 distinct colors.
## Rules of thumb
- **Max 4 actors (comfortable), max 6 actors (absolute ceiling).** 5 is allowed only when every title is ≤12 chars and every subtitle is omitted. 6 requires every title ≤9 chars (so it fits the 100-wide header rect) and no subtitles, and the diagram loses its 40px horizontal safe margin — see `layout-math.md` → "Swim-lane (sequence) layout" for the full N=5 and N=6 coordinate tables. Beyond 6, split into an overview diagram plus one detail diagram per sub-flow — a 7-actor sequence diagram is guaranteed to feel cramped no matter how carefully you pack the labels.
- **610 messages is the sweet spot.** 4 is too sparse (use a flowchart); 12+ is too dense (split).
- **One ramp per actor**, up to 4 total. The default `[gray, teal, purple, blue]` works for most protocols. Swap ramps only when a topic needs it.
- **Number every message.** Even "obvious" single-step diagrams benefit from "1. ...". Readers scan numbers before they read words.
- **Every arrow gets a label.** Sequence diagrams are the one place in this skill where arrow labels are mandatory, not optional — the label *is* the message, and without it the diagram says nothing.
- **Sender's ramp colors the arrow.** Never use `class="arr"` (mono-gray) for a sequence message.
- **Labels above the line, never on it.** Labels float 10px above the arrow at the row midpoint, `text-anchor="middle"`, `class="ts"`.
- **No crossings within a single row.** Two messages at the same `arrow_y` collide — always move one to a new row.
- **No arcs, no curves.** Flat horizontal lines only. Self-messages use a labeled rect, not a small arc.
Assign each actor a distinct color from the palette. Use that color for:
- Actor box stroke
- Activation bar on that lifeline
- Outgoing arrows from that actor (optional, for visual clarity in complex diagrams)