pi-visualize-code-changes 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,340 @@
1
+ # Choosing and writing the right diagram
2
+
3
+ Read this when the change needs anything beyond a basic `flowchart TD`. Each
4
+ section gives the selection rationale, a working skeleton, and how to express
5
+ before/after/diff in that diagram type — the diff view is the awkward part for
6
+ non-flowchart types, and the answers differ.
7
+
8
+ ## Contents
9
+
10
+ 1. [Picking a lens](#picking-a-lens)
11
+ 2. [Control flow — flowchart](#control-flow--flowchart)
12
+ 3. [Module & dependency graphs](#module--dependency-graphs)
13
+ 4. [Sequence diagrams](#sequence-diagrams)
14
+ 5. [State machines](#state-machines)
15
+ 6. [Data flow](#data-flow)
16
+ 7. [Class & schema diagrams](#class--schema-diagrams)
17
+ 8. [When the merged diff view stops working](#when-the-merged-diff-view-stops-working)
18
+
19
+ ---
20
+
21
+ ## Picking a lens
22
+
23
+ Ask what question the reader arrives with. The diagram type follows from the
24
+ question, not from the code's language or framework.
25
+
26
+ | Reader's question | Lens | Type |
27
+ |---|---|---|
28
+ | "Which path does execution take now?" | Control flow | `flowchart TD` |
29
+ | "What depends on what after the move?" | Dependency graph | `flowchart LR` |
30
+ | "Who calls whom, in what order?" | Sequence | `sequenceDiagram` |
31
+ | "What states can this be in?" | State machine | `stateDiagram-v2` |
32
+ | "How does the data get transformed?" | Data flow | `flowchart LR` |
33
+ | "What shape is this type/table now?" | Structure | `classDiagram` / `erDiagram` |
34
+
35
+ A change often admits two valid lenses — a new caching layer is both a control
36
+ flow change and a dependency change. When `AskUserQuestion` / `ask_user_question` is available, offer
37
+ the two strongest candidates rather than guessing. Two diagrams answering two
38
+ different questions is fine; two diagrams answering the same question is padding.
39
+
40
+ ---
41
+
42
+ ## Control flow — flowchart
43
+
44
+ The default, and right for most logic changes. `TD` (top-down) suits branching and
45
+ error paths; `LR` suits pipelines.
46
+
47
+ ```mermaid
48
+ flowchart TD
49
+ IN["handleUpload(file)"] --> V{"size < 10MB?"}
50
+ V -->|no| ERR["reject 413"]
51
+ V -->|yes| SCAN["virusScan(file)"]
52
+ SCAN --> S3["putObject(bucket)"]
53
+ S3 --> DB["insert files row"]
54
+ DB --> OK["201 + file id"]
55
+ ```
56
+
57
+ Shape conventions readers already know, worth keeping consistent:
58
+
59
+ | Shape | Syntax | Means |
60
+ |---|---|---|
61
+ | Rectangle | `A["doThing()"]` | a step |
62
+ | Diamond | `B{"ok?"}` | a decision |
63
+ | Rounded | `C("entry point")` | start/end |
64
+ | Cylinder | `D[("sessions")]` | datastore |
65
+ | Subroutine | `E[["renderPage()"]]` | call into another documented flow |
66
+
67
+ Label decision edges with the condition (`-->|yes|`). An unlabelled branch forces
68
+ the reader back into the source, which defeats the purpose.
69
+
70
+ **Diff view:** merged graph, `:::added` / `:::removed` / `:::changed` / `:::same`
71
+ per node, dotted red edges for dead paths. This is the case the SKILL.md example
72
+ covers.
73
+
74
+ ---
75
+
76
+ ## Module & dependency graphs
77
+
78
+ For changes that move code rather than alter logic — extracting a module,
79
+ inverting a dependency, introducing a layer. `LR` reads best since dependency is
80
+ naturally left-to-right.
81
+
82
+ ```mermaid
83
+ flowchart LR
84
+ subgraph api["api/"]
85
+ H["handlers.py"]
86
+ end
87
+ subgraph core["core/"]
88
+ S["service.py"]
89
+ R["repo.py"]
90
+ end
91
+ subgraph infra["infra/"]
92
+ DB[("postgres")]
93
+ end
94
+ H --> S
95
+ S --> R
96
+ R --> DB
97
+ ```
98
+
99
+ Use `subgraph` for the layer/package boundary — the boundary is usually the whole
100
+ point of the change. One node per module, not per function; if a module needs
101
+ internal detail, that is a second diagram.
102
+
103
+ **Diff view:** colour the *edges* as much as the nodes. Dependency changes are
104
+ edge changes, and a graph that only recolours boxes will look nearly identical
105
+ before and after. Show a removed dependency as a dotted red edge that still
106
+ appears in the merged view, so the reader sees what was severed.
107
+
108
+ ### The fate map
109
+
110
+ When a change is mostly relocation — a module split, a package extraction, a
111
+ layer being carved out — plain colour coding fails. Every unit is "removed from
112
+ here, added over there", so everything lights up and the reader learns nothing
113
+ about where to spend attention.
114
+
115
+ Put the old container on one side and the new ones on the other, then draw one
116
+ edge per unit, labelled with what happened to it:
117
+
118
+ ```mermaid
119
+ flowchart LR
120
+ subgraph OLD["billing.py — deleted"]
121
+ O_CS["calculate_subtotal"]:::same
122
+ O_GI["generate_invoice"]:::same
123
+ O_AT["apply_tax — flat rate"]:::removed
124
+ end
125
+ subgraph NEW["billing/ — new package"]
126
+ N_CS["calculator.calculate_subtotal"]:::same
127
+ N_GI["invoice.generate_invoice"]:::changed
128
+ N_AT["tax.apply_tax — brackets"]:::added
129
+ end
130
+
131
+ O_CS -->|"moved verbatim"| N_CS
132
+ O_GI -->|"moved, 3 lines changed"| N_GI
133
+ O_AT -.->|"deleted, not moved"| N_AT
134
+
135
+ classDef added fill:#d4f8d4,stroke:#2ea043,color:#1f2328
136
+ classDef removed fill:#ffd7d5,stroke:#cf222e,color:#1f2328
137
+ classDef changed fill:#fff5cc,stroke:#bf8700,color:#1f2328
138
+ classDef same fill:#f6f8fa,stroke:#8c959f,color:#1f2328
139
+ linkStyle 0 stroke:#8c959f
140
+ linkStyle 1 stroke:#bf8700
141
+ linkStyle 2 stroke:#cf222e
142
+ ```
143
+
144
+ The edge labels are the payload. `moved verbatim` means *skip this file*, which
145
+ is often the most valuable sentence in the whole document.
146
+
147
+ Earn those labels rather than guessing them. Git reports a wholesale file
148
+ replacement the same way whether code was relocated untouched or rewritten from
149
+ scratch, so compare the actual bodies:
150
+
151
+ ```bash
152
+ git show main:billing.py | awk '/^def calculate_subtotal/,/^$/' > /tmp/before.txt
153
+ git show HEAD:billing/calculator.py | awk '/^def calculate_subtotal/,/^$/' > /tmp/after.txt
154
+ diff /tmp/before.txt /tmp/after.txt && echo "identical -- moved, not rewritten"
155
+ ```
156
+
157
+ Pairing the fate map with a small table — file, what happened, review effort —
158
+ turns the diagram into something a reviewer can act on immediately.
159
+
160
+ ---
161
+
162
+ ## Sequence diagrams
163
+
164
+ Right when ordering, round trips, or who-initiates-what is the substance —
165
+ auth handshakes, retries, webhook flows, anything touching multiple services.
166
+ A flowchart cannot express "then B replies to A" without lying about time.
167
+
168
+ ```mermaid
169
+ sequenceDiagram
170
+ autonumber
171
+ participant C as Client
172
+ participant A as API
173
+ participant Q as Queue
174
+ participant W as Worker
175
+
176
+ C->>A: POST /jobs
177
+ A->>Q: enqueue(job)
178
+ A-->>C: 202 Accepted
179
+ Q->>W: deliver(job)
180
+ W-->>Q: ack
181
+ ```
182
+
183
+ `autonumber` earns its place — it gives the reader stable numbers to refer to.
184
+ Declare participants explicitly to control column order. Use `-->>` for replies
185
+ and `-)` for fire-and-forget.
186
+
187
+ Structural blocks, each closed by `end`:
188
+
189
+ ```
190
+ alt token expired
191
+ A-->>C: 401
192
+ else valid
193
+ A-->>C: 200
194
+ end
195
+
196
+ loop every 30s
197
+ W->>A: heartbeat
198
+ end
199
+
200
+ opt if cache miss
201
+ A->>DB: query
202
+ end
203
+ ```
204
+
205
+ **Diff view:** sequence diagrams have no `classDef`, so the flowchart colouring
206
+ approach does not transfer. Two options that do work:
207
+
208
+ - `rect rgb(212,248,212)` blocks wrapping added exchanges, `rgb(255,215,213)` for
209
+ removed ones — the closest thing to the standard colour coding:
210
+
211
+ ```
212
+ rect rgb(212, 248, 212)
213
+ A->>T: verifyToken(jwt)
214
+ end
215
+ ```
216
+
217
+ - Annotate message text directly: `A->>B: [ADDED] verifyToken(jwt)` and
218
+ `Note over A,B: removed — checkPassword()`.
219
+
220
+ The `rect` approach is better when added/removed steps are contiguous; annotation
221
+ is better when changes are scattered through a long exchange.
222
+
223
+ ---
224
+
225
+ ## State machines
226
+
227
+ For lifecycle changes — order status, connection handling, job states, feature
228
+ flags. Use it when the change adds or removes a state or a transition, which is
229
+ exactly the kind of change a diff obscures badly.
230
+
231
+ ```mermaid
232
+ stateDiagram-v2
233
+ [*] --> Pending
234
+ Pending --> Running: start()
235
+ Running --> Succeeded: exit 0
236
+ Running --> Failed: exit != 0
237
+ Failed --> Pending: retry (max 3)
238
+ Succeeded --> [*]
239
+ ```
240
+
241
+ Label transitions with what triggers them. `[*]` marks entry and exit.
242
+
243
+ **Diff view:** `stateDiagram-v2` does support `classDef` and `:::`, so the normal
244
+ colour coding works:
245
+
246
+ ```
247
+ Cancelled:::added
248
+ classDef added fill:#d4f8d4,stroke:#2ea043,color:#1f2328
249
+ ```
250
+
251
+ A newly reachable state and a newly *unreachable* one are both easy to miss in a
252
+ diff and obvious in this view — call them out in prose too.
253
+
254
+ ---
255
+
256
+ ## Data flow
257
+
258
+ For changes to how data is shaped and moved: a new transformation step, a changed
259
+ schema, a different serialisation boundary. `LR` with the payload shape in the
260
+ edge labels:
261
+
262
+ ```mermaid
263
+ flowchart LR
264
+ SRC[("orders table")] -->|"rows"| X["normalise()"]
265
+ X -->|"OrderDTO[]"| AGG["aggregateByRegion()"]
266
+ AGG -->|"RegionSummary[]"| OUT["render CSV"]
267
+ ```
268
+
269
+ Putting the type or shape on the edge is what distinguishes this from a control
270
+ flow diagram, and it is usually where the change lives — a field added to a DTO
271
+ shows up as an edge-label change even when every node is untouched.
272
+
273
+ ---
274
+
275
+ ## Class & schema diagrams
276
+
277
+ For changes to type structure, inheritance, or database schema.
278
+
279
+ ```mermaid
280
+ classDiagram
281
+ class Session {
282
+ +String id
283
+ +String userId
284
+ +DateTime expiresAt
285
+ +isValid() bool
286
+ }
287
+ class TokenSession {
288
+ +String jwt
289
+ }
290
+ Session <|-- TokenSession
291
+ ```
292
+
293
+ Relations: `<|--` inheritance, `*--` composition, `o--` aggregation, `-->`
294
+ association.
295
+
296
+ For database work `erDiagram` is usually the better fit:
297
+
298
+ ```mermaid
299
+ erDiagram
300
+ USER ||--o{ SESSION : has
301
+ SESSION {
302
+ uuid id PK
303
+ uuid user_id FK
304
+ timestamptz expires_at
305
+ }
306
+ ```
307
+
308
+ **Diff view:** neither type supports `classDef` reliably. Mark changed members in
309
+ the member text (`+jwt String << added >>`) or annotate with a `note`. For schema
310
+ changes, a small added/removed/changed table beneath the diagram is often clearer
311
+ than colour anyway, since column-level detail is what reviewers check.
312
+
313
+ ---
314
+
315
+ ## When the merged diff view stops working
316
+
317
+ The merged graph is the default because it puts the change in one picture. It
318
+ fails when before and after share almost no structure — a genuine rewrite rather
319
+ than an edit. Symptoms: more than about half the nodes are `added` or `removed`,
320
+ or the graph has two disconnected components that only meet at the entry point.
321
+
322
+ At that point the merged view is just the two diagrams overlaid with extra colour,
323
+ and readers do better with an explicit side-by-side:
324
+
325
+ ```mermaid
326
+ flowchart LR
327
+ subgraph before["Before"]
328
+ direction TD
329
+ A1["parse()"] --> B1["validateManual()"]
330
+ end
331
+ subgraph after["After"]
332
+ direction TD
333
+ A2["parse()"] --> B2["schema.validate()"]
334
+ B2 --> C2["coerceTypes()"]
335
+ end
336
+ ```
337
+
338
+ Say in the prose why you switched — "this was a rewrite rather than an edit, so a
339
+ merged view would not be legible" tells the reader something true about the change
340
+ itself, which is useful information rather than an apology.
@@ -0,0 +1,191 @@
1
+ # Mermaid syntax pitfalls
2
+
3
+ The failure modes that actually break generated diagrams, roughly in order of how
4
+ often they bite. Run `scripts/validate_mermaid.py` rather than trusting a visual
5
+ scan — several of these render *silently wrong* rather than erroring.
6
+
7
+ ## Contents
8
+
9
+ 1. [Unquoted characters in labels](#1-unquoted-characters-in-labels)
10
+ 2. [Reserved words](#2-reserved-words)
11
+ 3. [Theme-safe styling](#3-theme-safe-styling)
12
+ 4. [Style classes](#4-style-classes)
13
+ 5. [Edge styling by index](#5-edge-styling-by-index)
14
+ 6. [Subgraphs](#6-subgraphs)
15
+ 7. [Comments](#7-comments)
16
+ 8. [Sequence-diagram specifics](#8-sequence-diagram-specifics)
17
+ 9. [Node ids](#9-node-ids)
18
+
19
+ ---
20
+
21
+ ## 1. Unquoted characters in labels
22
+
23
+ The number one break. `(`, `)`, `[`, `]`, `{`, `}`, and `|` terminate a label
24
+ early, and since code labels are full of call signatures this hits constantly.
25
+
26
+ ```
27
+ A[login(req)] ✗ Parse error
28
+ A["login(req)"] ✓
29
+
30
+ B[arr[0]] ✗
31
+ B["arr[0]"] ✓
32
+
33
+ C[map|filter] ✗
34
+ C["map|filter"] ✓
35
+ ```
36
+
37
+ Quote defensively — a quoted label never hurts, so wrap anything containing
38
+ `()[]{}|:,;`. For a literal double quote inside a label use `#quot;`, and for
39
+ other awkward characters use HTML entities: `#35;` is `#`, `#59;` is `;`.
40
+
41
+ Angle brackets need entities too, because Mermaid renders labels as HTML:
42
+
43
+ ```
44
+ D["Vec<String>"] ✗ renders as an empty tag
45
+ D["Vec&lt;String&gt;"] ✓
46
+ ```
47
+
48
+ ## 2. Reserved words
49
+
50
+ `end` breaks flowcharts when used as a node id or bare label — the parser reads it
51
+ as closing a `subgraph`:
52
+
53
+ ```
54
+ flowchart TD
55
+ start --> end ✗
56
+ start --> endNode ✓
57
+ start --> E["end"] ✓
58
+ ```
59
+
60
+ `graph`, `subgraph`, `class`, `click`, `style`, and `direction` are similarly
61
+ unsafe as bare ids. Capitalising (`End`) works but reads oddly; a descriptive
62
+ rename is better.
63
+
64
+ ## 3. Theme-safe styling
65
+
66
+ Diagrams get read in both light and dark mode. `classDef` that sets only `fill`
67
+ leaves text colour inherited, so dark-mode readers get dark text on a light fill:
68
+
69
+ ```
70
+ classDef added fill:#d4f8d4 ✗ unreadable in dark mode
71
+ classDef added fill:#d4f8d4,stroke:#2ea043,color:#1f2328 ✓
72
+ ```
73
+
74
+ Always set `fill`, `stroke`, and `color` together. The palette in SKILL.md is
75
+ chosen to pass contrast in both themes.
76
+
77
+ Avoid `%%{init: {'theme':'dark'}}%%` — it pins one theme and looks broken for
78
+ half your readers.
79
+
80
+ ## 4. Style classes
81
+
82
+ Referencing an undefined class is *not* an error. The diagram renders, silently
83
+ unstyled — so a visual check passes while the colour coding that carried your
84
+ meaning is gone.
85
+
86
+ ```
87
+ A["x"]:::added ← needs a matching classDef somewhere in the block
88
+ classDef added fill:#d4f8d4,stroke:#2ea043,color:#1f2328
89
+ ```
90
+
91
+ Two application forms:
92
+
93
+ ```
94
+ A["x"]:::added inline
95
+ class A,B,C added statement form, several nodes at once
96
+ ```
97
+
98
+ `classDef` must live in the same block as the nodes it styles; nothing carries
99
+ across fenced blocks. The validator flags this case specifically.
100
+
101
+ ## 5. Edge styling by index
102
+
103
+ `linkStyle` takes 0-based indices counted in **declaration order across the whole
104
+ block**, including edges declared inside subgraphs:
105
+
106
+ ```
107
+ linkStyle 2,4 stroke:#cf222e,stroke-dasharray:5
108
+ linkStyle 3 stroke:#2ea043
109
+ ```
110
+
111
+ Two traps. An out-of-range index is a hard error (the validator catches it). A
112
+ merely *wrong* index is not — it silently paints the wrong edge, which is worse,
113
+ because the diagram then asserts something false. Recount after any edit that adds
114
+ or reorders edges.
115
+
116
+ Where dashed-vs-solid carries the meaning, encoding it in the arrow itself is
117
+ more robust than an index that drifts:
118
+
119
+ ```
120
+ A -.-> B dotted, survives reordering
121
+ A ==> B thick
122
+ ```
123
+
124
+ ## 6. Subgraphs
125
+
126
+ Give a subgraph an explicit id when the title has spaces, otherwise the title
127
+ becomes the id and breaks references:
128
+
129
+ ```
130
+ subgraph Auth layer ✗
131
+ subgraph auth["Auth layer"] ✓
132
+ ...
133
+ end
134
+ ```
135
+
136
+ Every `subgraph` needs its own `end`. Edges may cross subgraph boundaries freely,
137
+ but declare them *outside* the subgraph blocks to keep layout predictable.
138
+
139
+ ## 7. Comments
140
+
141
+ `%%` must start the line. A trailing comment after content is unreliable:
142
+
143
+ ```
144
+ A --> B %% explanation ✗ can swallow the edge
145
+ %% explanation
146
+ A --> B ✓
147
+ ```
148
+
149
+ ## 8. Sequence-diagram specifics
150
+
151
+ Messages use a different arrow vocabulary from flowcharts — flowchart arrows are
152
+ invalid here:
153
+
154
+ ```
155
+ A->>B: solid arrow, request
156
+ A-->>B: dashed arrow, response
157
+ A-)B: async
158
+ ```
159
+
160
+ Declare participants up front to fix left-to-right order; otherwise it is decided
161
+ by first appearance:
162
+
163
+ ```
164
+ sequenceDiagram
165
+ participant C as Client
166
+ participant S as Service
167
+ ```
168
+
169
+ Colons separate the message from its text, so a colon *inside* message text needs
170
+ escaping or rewording. `alt`/`else`/`opt`/`loop` blocks each close with `end`.
171
+
172
+ ## 9. Node ids
173
+
174
+ Ids must start with a letter or underscore, and cannot contain spaces or hyphens.
175
+ The label carries the human-readable name:
176
+
177
+ ```
178
+ my-node["Label"] ✗ hyphen
179
+ order id["Label"] ✗ space
180
+ myNode["Label"] ✓
181
+ order_id["Label"] ✓
182
+ ```
183
+
184
+ Reusing an id re-references the same node — useful for converging paths, but a
185
+ silent merge if it was accidental. Declare the label once and reference the bare
186
+ id afterwards:
187
+
188
+ ```
189
+ A["parse()"] --> B
190
+ C --> A
191
+ ```