flowfig 0.4.0 → 0.6.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <h1><img src="docs/logo.svg" alt="flowfig" height="80"/></h1>
4
4
 
5
- ### Claude Code, Cursor, Copilot, Codex, Gemini CLI, Windsurf and Kiro draw the diagram from your code. flowfig checks it before it goes in your README.
5
+ ### Your coding agent draws the diagram from your code. flowfig checks it, and CI fails when the linked code is gone.
6
6
 
7
7
  <p>
8
8
  <a href="https://www.npmjs.com/package/flowfig"><img src="https://img.shields.io/npm/v/flowfig?style=for-the-badge&logo=npm&logoColor=white&label=npm" alt="npm version"/></a>
@@ -16,17 +16,57 @@
16
16
 
17
17
  </div>
18
18
 
19
- An agent with the flowfig instructions made faithful, checked diagrams more often than the same agent without them:
19
+ ## The problem
20
+
21
+ A diagram in a repo drifts from the code. Someone renames a function, and the diagram still shows the old name. No
22
+ check fails, so no one sees the drift.
23
+
24
+ flowfig links each part of a diagram to the code that it draws. `flowfig verify` fails when a linked file or symbol is
25
+ gone. Run `verify` in CI, and a stale diagram fails the pull request.
26
+
27
+ ## Fail CI when the diagram drifts
28
+
29
+ A box, an edge or a message can name the code that it draws:
30
+
31
+ ```json
32
+ { "id": "verify", "label": "verifyPassword", "source": "src/auth/login.ts#verifyPassword" }
33
+ ```
34
+
35
+ Rename `verifyPassword` to `checkPassword`. Then `verify` fails with exit code 1:
20
36
 
21
- | Criterion | Agent without flowfig instructions | Agent with flowfig instructions |
22
- | --------------------------------------------------- | ---------------------------------- | ---------------------------------------------- |
23
- | Diagram faithful to the code (no invented part) | 7 of 8 | 8 of 8 |
24
- | Right diagram form for the task | 5 of 7 | 7 of 7 |
25
- | Passes `flowfig check --strict` | 5 of 7 | 6 of 6 |
26
- | Right action (draws, declines or splits) | 6 of 8 | 7 of 8 |
27
- | Held-out tasks (not used to write the instructions) | – | 2 of 4 pass all 6 criteria; 2 of 4 pass 4 of 6 |
37
+ ```console
38
+ $ npx flowfig verify login.svg
39
+ error missing-symbol login.svg: box "verify" -> src/auth/login.ts#verifyPassword: symbol not found
40
+ 1 error, 0 warnings
41
+ ```
42
+
43
+ `verify` checks that each linked file exists and that the symbol name still occurs in it. A link to a Markdown file can
44
+ name a heading, so a process diagram can follow its SOP document.
28
45
 
29
- [How we measured](#how-we-measured).
46
+ The GitHub Action runs `verify` on every figure in a pull request. For each SVG that the PR changes, it comments the old
47
+ image, the new image and the spec changes. It also names each figure whose linked code the PR changes.
48
+
49
+ ```yaml
50
+ # .github/workflows/figures.yml
51
+ name: figures
52
+ on: pull_request
53
+ jobs:
54
+ figures:
55
+ runs-on: ubuntu-latest
56
+ permissions:
57
+ contents: read
58
+ pull-requests: write
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ with:
62
+ fetch-depth: 0 # the Action reads the old figure from the base commit
63
+ - uses: actions/setup-node@v4
64
+ with:
65
+ node-version: 22
66
+ - uses: iamalvisng/flowfig@v0.6.0
67
+ with:
68
+ figures: 'docs/**/*.svg' # default **/*.svg
69
+ ```
30
70
 
31
71
  ## Use it
32
72
 
@@ -53,49 +93,79 @@ draw the refund process with a lane for each team
53
93
  The agent writes an SVG and ends its reply with `npx flowfig open <path>`. Run that command to see the animation in your
54
94
  browser.
55
95
 
96
+ `init` sets up 7 agents: Claude Code, Cursor, GitHub Copilot, Codex and other `AGENTS.md` agents, Gemini CLI, Windsurf
97
+ and Kiro.
98
+
56
99
  ## One command
57
100
 
58
101
  ```bash
59
102
  npx flowfig draw "how does login work"
60
103
  ```
61
104
 
62
- `draw` runs Claude Code on the repo. It gives Claude Code the flowfig guide, `npx flowfig` and the tools Read, Glob and Grep.
63
- The permission mode is `acceptEdits`, so Claude Code can write files. `draw` disallows the tools Edit, MultiEdit and
64
- NotebookEdit. Claude Code writes the spec `how-does-login-work.json`, then renders `how-does-login-work.svg`. Then `draw`
65
- checks the figure itself with `check --strict` and `verify`, and removes the spec file. If the agent writes no new figure,
66
- `draw` exits 1 and shows the tool calls that the permission rules denied. `draw` prints the result, the reply of the agent
67
- and the cost of the run. `draw` needs Claude Code on the machine and a login. `draw` does not run on Windows yet.
68
- `--out` sets the path (it must end in `.svg`), `--model` sets the model, `--max-turns` sets the turn cap (default 40) and
69
- `--json` prints `{ out, reply, cost, session, findings }` for scripts. `--open` opens the figure in the default browser after the check.
105
+ `draw` runs Claude Code on the repo with the flowfig guide. It gives Claude Code the tools Read, Glob and Grep, and
106
+ `npx flowfig`. `draw` disallows the tools Edit, MultiEdit and NotebookEdit. Claude Code writes the figure. Then `draw`
107
+ checks the figure with `check --strict` and `verify`. `draw` prints the result, the reply of the agent and the cost of
108
+ the run.
70
109
 
71
- ## The problem
110
+ `draw` needs Claude Code on the machine and a login. `draw` does not run on Windows yet. `--out` sets the path,
111
+ `--model` sets the model, `--max-turns` sets the turn cap (default 40), `--json` prints the result for scripts, and
112
+ `--open` opens the figure.
113
+
114
+ ## How it works
72
115
 
73
- - A diagram in a repo drifts from the code. No one sees the drift.
74
- - An agent that you ask for a diagram invents parts that the code does not have.
75
- - A static picture cannot show the order of the calls or their payloads.
76
-
77
- ## What flowfig does
78
-
79
- - **One command sets up 7 agents.** `npx flowfig init` writes instructions for Claude Code, Cursor, GitHub Copilot, Codex and
80
- others through `AGENTS.md`, Gemini CLI, Windsurf and Kiro.
81
- - **The agent writes JSON, not pictures.** The agent reads the code and writes a JSON spec of the parts, the calls and their
82
- order. flowfig does the layout.
83
- - **`flowfig check` finds the faults a machine can read.** It finds ids that point nowhere, text wider than its box, edges
84
- through boxes, overlapping labels, hidden edges, text too small at the README width, and low contrast. The agent reads the
85
- faults and fixes the spec.
86
- - **The output is one animated SVG with no script.** The SVG plays in a GitHub README, PR or issue. It follows the light or dark
87
- mode of the reader.
88
- - **Every SVG carries its own spec.** `npx flowfig --spec figure.svg` prints the spec. Any agent can read a diagram back, change
89
- it, and render it again.
90
- - **Three forms and a React player.** The forms are the map, the map with a lifeline rail for sequences, and the rail alone. The
91
- React player adds tabs, pause and hover. The package has zero runtime dependencies. Only the React player needs React.
116
+ 1. **The agent writes JSON, not pictures.** The agent reads the code and writes a spec of the parts, the calls and their
117
+ order. flowfig does the layout.
118
+ 2. **flowfig checks the spec.** `flowfig check` has 22 rules. It finds ids that point nowhere, text wider than its box,
119
+ edges through boxes, overlapping labels and low contrast. A render runs the same check and writes nothing on an error.
120
+ The agent reads the faults and fixes the spec.
121
+ 3. **The output is one animated SVG with no script.** The SVG plays in a GitHub README, PR or issue. It follows the light
122
+ or dark mode of the reader. The SVG carries its spec, so `npx flowfig --spec figure.svg` prints it back.
123
+
124
+ ## Forms
125
+
126
+ The map (the default) shows the parts and the messages between them:
127
+
128
+ ![A request that misses the cache, then hits the cache](docs/cached-request.svg)
129
+
130
+ The map and the rail (`rail: true`) adds one row for each message, with its payload:
131
+
132
+ ![An order checkout: the map of the parts, and a rail of the messages under it](docs/checkout.svg)
133
+
134
+ The rail only (`rail: "only"`) shows the order of the messages without the map:
135
+
136
+ ![The checkout messages as a rail, without the map](docs/checkout-rail-only.svg)
137
+
138
+ Swimlanes (`lanes: true`) give each role a lane and put the steps left to right in time order:
139
+
140
+ ![The refund process across Customer, Support and Finance](docs/refund-process.svg)
141
+
142
+ A long swimlane process wraps into blocks at the page width. An edge between two blocks becomes two labeled stubs:
143
+
144
+ ![A returns process in blocks of lanes, with labeled stubs between the blocks](docs/returns-process.svg)
145
+
146
+ The timeline (`timeline: true`) draws a roadmap with one track per team, dated bars, milestones and a today line:
147
+
148
+ ![A Q4 roadmap with three tracks, milestones and a today line](docs/roadmap.svg)
149
+
150
+ A state lifecycle marks the first state with `mark: "start"` and each final state with `mark: "end"`:
151
+
152
+ ![An order status lifecycle with a start dot and two end rings](docs/order-status.svg)
153
+
154
+ ## Open and share
155
+
156
+ `npx flowfig open figure.svg` shows the figure in your default browser.
157
+
158
+ `npx flowfig gif figure.svg` writes an animated GIF for Slack, Notion, X and slides, where SVG animation does not play.
159
+
160
+ The options are in [Open and share a figure](#open-and-share-a-figure).
92
161
 
93
162
  ## flowfig, Mermaid and a hand-drawn image
94
163
 
95
164
  | | flowfig | Mermaid | Hand-drawn image |
96
165
  | ------------------------------ | --------------------------------- | ------------------------------------ | ---------------- |
97
166
  | Made by an agent from the code | Yes, with the `init` instructions | Yes, as Mermaid text | No |
98
- | Checked for faults | Yes, 11 rules in `flowfig check` | Syntax errors only | No |
167
+ | Linked to the code | Yes, `verify` fails in CI | No | No |
168
+ | Checked for faults | Yes, 22 rules in `flowfig check` | Syntax errors only | No |
99
169
  | Animated | Yes, one packet per message | No | No |
100
170
  | Shows payloads and order | Yes, on the map and the rail | Order and text in a sequence diagram | No |
101
171
  | Plays in a GitHub README | Yes, as an SVG | Yes, GitHub renders it | Yes, as an image |
@@ -106,18 +176,16 @@ and the cost of the run. `draw` needs Claude Code on the machine and a login. `d
106
176
 
107
177
  - [Install](#install)
108
178
  - [Your first figure](#your-first-figure)
109
- - [Three forms](#three-forms)
179
+ - [Read and change a figure](#read-and-change-a-figure)
110
180
  - [The spec](#the-spec)
111
181
  - [Check a figure](#check-a-figure)
112
182
  - [Open and share a figure](#open-and-share-a-figure)
113
183
  - [Verify in CI](#verify-in-ci)
114
- - [For teams](#for-teams)
115
184
  - [MCP server](#mcp-server)
116
185
  - [Use in React](#use-in-react)
117
186
  - [Use from Node](#use-from-node)
118
187
  - [Use with your coding agent](#use-with-your-coding-agent)
119
188
  - [What flowfig does not draw](#what-flowfig-does-not-draw)
120
- - [How we measured](#how-we-measured)
121
189
  - [License](#license)
122
190
  - [Development](#development)
123
191
 
@@ -176,28 +244,7 @@ The command writes `first.svg`:
176
244
  The SVG has no script and fetches no font. GitHub shows the SVG in a README, a PR or an issue. The SVG follows the light or dark
177
245
  color scheme of the reader.
178
246
 
179
- ## Three forms
180
-
181
- | Form | Spec | Use it when |
182
- | ------------- | -------------- | ----------------------------------------------------------------------- |
183
- | The map | (default) | The reader must see where each part runs and which parts talk. |
184
- | Map and rail | `rail: true` | The reader must see the parts, and also the order and payload of calls. |
185
- | The rail only | `rail: "only"` | The order of the messages is the point, and the map adds nothing to it. |
186
-
187
- The map:
188
-
189
- ![A request that misses the cache, then hits the cache](docs/cached-request.svg)
190
-
191
- The map with the rail:
192
-
193
- ![An order checkout: the map of the parts, and a rail of the messages under it](docs/checkout.svg)
194
-
195
- The rail draws one row for each message, with its payload.
196
- Hops in one beat share a parallel band. An `async` hop has a dashed arrow and an `async` tag.
197
-
198
- The rail only, from the same checkout spec:
199
-
200
- ![The checkout messages as a rail, without the map](docs/checkout-rail-only.svg)
247
+ ## Read and change a figure
201
248
 
202
249
  Each SVG from the CLI carries its own spec. These commands made the rail-only figure from `docs/checkout.svg`:
203
250
 
@@ -216,15 +263,19 @@ full reference, run `npx flowfig docs`.
216
263
 
217
264
  ### Boxes
218
265
 
219
- | Field | Meaning | Default |
220
- | -------- | ------------------------------------------------------------------------- | ----------- |
221
- | `id` | The name that edges and steps use. It must be unique. | required |
222
- | `label` | The title in the box. | required |
223
- | `sub` | A smaller line under the label. | none |
224
- | `shape` | `"box"`, `"decision"` (a diamond) or `"store"` (a data cylinder). | `"box"` |
225
- | `source` | The code this draws: `path` or `path#symbol`. `flowfig verify` checks it. | none |
226
- | `lines` | The least number of text lines that a content card keeps. | none |
227
- | `width` | The width in px. This value replaces the width that layout picks. | from layout |
266
+ | Field | Meaning | Default |
267
+ | -------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------- |
268
+ | `id` | The name that edges and steps use. It must be unique. | required |
269
+ | `label` | The title in the box. | required |
270
+ | `sub` | A smaller line under the label. | none |
271
+ | `shape` | `"box"`, `"decision"` (a diamond) or `"store"` (a data cylinder). | `"box"` |
272
+ | `source` | The code this draws: `path` or `path#symbol`. `flowfig verify` checks it. | none |
273
+ | `lines` | The least number of text lines that a content card keeps. | none |
274
+ | `width` | The width in px. This value replaces the width that layout picks. | from layout |
275
+ | `at` | In a `lanes` figure: the time column of the box, from 0. | the order of first use in the steps |
276
+ | `from` | In a `timeline` figure: the start date of the item, as YYYY-MM-DD. A box with only `from` is a milestone. | none |
277
+ | `to` | In a `timeline` figure: the last day of the item, as YYYY-MM-DD. | none |
278
+ | `mark` | `"start"` draws a dot before the box. `"end"` draws a ring after the box. Use it on a state lifecycle. | none |
228
279
 
229
280
  ### Groups
230
281
 
@@ -275,20 +326,24 @@ A beat is an edge id, an array of edge ids that run at the same time, or an obje
275
326
  | `ms` | The length of the beat in ms. | `speed` (900) |
276
327
 
277
328
  In a hop, `back: true` runs the packet from `to` to `from`. `data` is a small card on the packet. `async: true` marks a message
278
- that does not wait for an answer.
329
+ that does not wait for an answer. On the rail, an `async` hop has a dashed arrow and an `async` tag. The hops of one beat
330
+ share a parallel band on the rail.
279
331
 
280
332
  Content is an array of rows, or a React node in the player. A row is `{ text, tag, tone, meta, mark, mono }`. Only `text` is
281
333
  required. The tones are `blue` (the default), `purple`, `green`, `orange` and `gray`.
282
334
 
283
335
  ### Figure options
284
336
 
285
- | Field | Meaning | Default |
286
- | ---------- | ------------------------------------------------------------------------- | -------- |
287
- | `rail` | `true` draws the rail under the map. `"only"` draws the rail alone. | `false` |
288
- | `speed` | The time in ms for a packet to cross one edge. | 900 |
289
- | `theme` | `{ accent, fg, muted, bg, surface, border, font }`. Each key is optional. | built-in |
290
- | `autoplay` | Start to play when the figure mounts. Only the React player reads it. | `true` |
291
- | `check` | Run the check rules in the browser. Only the React player reads it. | `false` |
337
+ | Field | Meaning | Default |
338
+ | ---------- | ------------------------------------------------------------------------------------ | -------- |
339
+ | `rail` | `true` draws the rail under the map. `"only"` draws the rail alone. | `false` |
340
+ | `lanes` | `true` draws swimlanes: a `column` group of labeled groups, one lane per role. | off |
341
+ | `timeline` | `true` draws a timeline: one labeled group per track, with the boxes at their dates. | off |
342
+ | `today` | In a `timeline` figure: the date of the today line, as YYYY-MM-DD. | none |
343
+ | `speed` | The time in ms for a packet to cross one edge. | 900 |
344
+ | `theme` | `{ accent, fg, muted, bg, surface, border, font }`. Each key is optional. | built-in |
345
+ | `autoplay` | Start to play when the figure mounts. Only the React player reads it. | `true` |
346
+ | `check` | Run the check rules in the browser. Only the React player reads it. | `false` |
292
347
 
293
348
  ## Check a figure
294
349
 
@@ -303,25 +358,38 @@ figure: 5 boxes, 3 groups, 4 edges, 2 steps, 6 messages
303
358
 
304
359
  The last line gives the counts of the parts of the figure. Compare the counts with the parts that you planned.
305
360
 
306
- | Rule | Severity | What it finds |
307
- | ------------------- | -------- | ---------------------------------------------------------------------------- |
308
- | `unknown-id` | error | An edge, step or beat names a box or edge that does not exist. |
309
- | `duplicate-id` | error | Two boxes, groups or edges have the same id. |
310
- | `hidden-edge` | error | A `quiet` edge that no beat uses, so the figure never shows it. |
311
- | `text-overflow` | error | Text that needs more width than its box has. |
312
- | `edge-crosses-box` | error | An edge that goes through a box that is not one of its ends. |
313
- | `label-overlap` | error | Two edge labels overlap, or an edge label covers a box. |
314
- | `low-contrast` | error | A text and background pair below 4.5:1, in the light, dark or custom theme. |
315
- | `empty-step` | warning | A step with no beats. |
316
- | `small-text` | warning | At the page width, the smallest text is below the minimum size. |
317
- | `font-estimated` | warning | `theme.font` is set. The SVG check estimates text width for the system font. |
318
- | `color-not-checked` | warning | A color that the check cannot read, so its contrast is not checked. |
361
+ `flowfig check` has 22 rules: 11 errors and 11 warnings.
362
+
363
+ | Rule | Severity | What it finds |
364
+ | --------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
365
+ | `unknown-id` | error | An edge, step or beat names a box, group or edge that does not exist. |
366
+ | `duplicate-id` | error | Two boxes, two groups or two edges have the same id. |
367
+ | `hidden-edge` | error | A `quiet` edge that no beat uses, so the figure never shows it. |
368
+ | `text-overflow` | error | Text that needs more width than its box has. |
369
+ | `edge-crosses-box` | error | An edge that goes through a box that is not one of its ends. |
370
+ | `label-overlap` | error | Two edge labels overlap, or a label covers a box or an edge, or a stub label crosses a lane border. |
371
+ | `low-contrast` | error | A text and background pair below 4.5:1, in the light, dark or custom theme, or on a tone tint. |
372
+ | `lanes-need-column` | error | The layout of a `lanes` or `timeline` figure is not a column group of labeled groups. |
373
+ | `bad-at` | error | An `at` value that is not an integer of 0 or more. |
374
+ | `timeline-need-from` | error | A box in a timeline has no `from` date. |
375
+ | `bad-date` | error | A `from`, `to` or `today` value is not a real YYYY-MM-DD date, or a `to` is before its `from`. |
376
+ | `empty-step` | warning | A step with no beats. |
377
+ | `small-text` | warning | At the page width, the smallest text is below the minimum size. |
378
+ | `bad-source` | warning | A `source` that is not `path` or `path#symbol`. |
379
+ | `mark-count` | warning | A lifecycle has more than one `start` mark, or a `start` mark and no `end` mark. |
380
+ | `lane-column-taken` | warning | Two boxes in one lane share a time column. |
381
+ | `lane-end-block` | warning | In wrapped lanes, an edge ends at a lane that no block on its side shows. |
382
+ | `stub-crosses-edge` | warning | In wrapped lanes, a stub line crosses another edge. |
383
+ | `timeline-dependency-order` | warning | In a timeline, an item does not start after the item that it depends on ends. |
384
+ | `timeline-and-lanes` | warning | The figure sets `timeline` and `lanes`. The renderers draw the timeline and ignore `lanes`. |
385
+ | `font-estimated` | warning | `theme.font` is set. The SVG check estimates text width for the system font. |
386
+ | `color-not-checked` | warning | A color that the check cannot read, so its contrast is not checked. |
319
387
 
320
388
  | Option | Effect |
321
389
  | ----------------- | ---------------------------------------------------------------------------------------------------------- |
322
390
  | `--strict` | Every warning becomes an error. |
323
391
  | `--json` | Print the findings as a JSON array, for scripts. Each finding has `rule`, `severity`, `ids` and `message`. |
324
- | `--width <px>` | The page width for `small-text`. Default: 830. |
392
+ | `--width <px>` | The page width for `small-text` and for the lanes wrap. Default: 830. |
325
393
  | `--min-text <px>` | The smallest text size the reader must get. Default: 10. |
326
394
 
327
395
  The exit code is 0 with no errors, 1 with one or more errors, and 2 for bad use, such as a missing input, an unknown flag, or a `--width` value that is not a number.
@@ -363,39 +431,30 @@ fails when the file or the symbol is gone. The action runs `verify` on every fig
363
431
  the new image for each SVG the PR changes, with the spec changes as a list, and it names each figure whose linked code the PR
364
432
  changes.
365
433
 
366
- A process figure for a team links its boxes to the SOP document, not to code: `"source": "docs/sop/refunds.md#step-3-approve-the-refund"`. The symbol is the heading as a GitHub anchor. If the heading is gone, `verify` fails.
434
+ A process figure for a team links its boxes to the SOP document, not to code: `"source": "docs/sop/refunds.md#step-3-approve-or-reject"`. The symbol is the heading as a GitHub anchor. If the heading is gone, `verify` fails.
367
435
 
368
436
  ```yaml
369
- permissions:
370
- contents: read
371
- pull-requests: write
372
- steps:
373
- - uses: actions/checkout@v4
374
- with:
375
- fetch-depth: 0
376
- - uses: actions/setup-node@v4
377
- with:
378
- node-version: 22
379
- - uses: iamalvisng/flowfig@v0.2.0
380
- with:
381
- figures: 'docs/**/*.svg' # default **/*.svg
437
+ # .github/workflows/figures.yml
438
+ name: figures
439
+ on: pull_request
440
+ jobs:
441
+ figures:
442
+ runs-on: ubuntu-latest
443
+ permissions:
444
+ contents: read
445
+ pull-requests: write
446
+ steps:
447
+ - uses: actions/checkout@v4
448
+ with:
449
+ fetch-depth: 0 # the Action reads the old figure from the base commit
450
+ - uses: actions/setup-node@v4
451
+ with:
452
+ node-version: 22
453
+ - uses: iamalvisng/flowfig@v0.6.0
454
+ with:
455
+ figures: 'docs/**/*.svg' # default **/*.svg
382
456
  ```
383
457
 
384
- A state lifecycle marks the first state with `mark: "start"` and each final state with `mark: "end"`.
385
-
386
- ![An order status lifecycle with a start dot and two end rings](docs/order-status.svg)
387
-
388
- ## For teams
389
-
390
- A process is a diagram too. `lanes: true` draws one lane per role and the steps left to right in time order. Each step
391
- links to a heading in the SOP, so `verify` fails when the SOP changes under the diagram.
392
-
393
- ![The refund process across Customer, Support and Finance](docs/refund-process.svg)
394
-
395
- A plan is a figure too. `timeline: true` draws one track per team, with each item as a bar from its `from` date to its `to` date. A milestone has only `from`, and `today` draws the line for now.
396
-
397
- ![A Q4 roadmap with three tracks, milestones and a today line](docs/roadmap.svg)
398
-
399
458
  ## MCP server
400
459
 
401
460
  `npx flowfig mcp` serves the tools `docs`, `check`, `render`, `verify` and `diff` over stdio, for an agent with no shell.
@@ -515,22 +574,6 @@ flowfig draws parts, the messages between the parts, and their order. flowfig dr
515
574
  and converts a Mermaid `gantt` to it. flowfig does not draw class diagrams, ER diagrams, charts of numbers or mind maps. Use Mermaid
516
575
  or a chart library for those.
517
576
 
518
- ## How we measured
519
-
520
- We ran an evaluation before this release: 47 coding-agent runs, with Claude Sonnet as the agent. The runs covered 12 tasks in 5
521
- test repos: a TypeScript login API, a Python ETL pipeline, an order state machine, a 20-service platform and a mixed CLI repo.
522
- The agent ran without the flowfig instructions and with them. An Opus grader scored each run against an answer key.
523
-
524
- 4 tasks were held out: they shaped no instruction. On the held-out tasks, 2 of 4 runs passed all 6 criteria. The other 2 passed
525
- 4 of 6: one drew an edge from the wrong box, and one reply stated a runtime effect wrongly.
526
-
527
- A first draft of the instructions scored worse than no instructions: 5 of 8 faithful. We rewrote the instructions from the
528
- failures and tested them again. The "with" column comes from the version that we tested on all 8 tasks. The shipped instructions
529
- add two refinements: the scope of a repo with many flows, and each edge taken from the caller in the code. 9 more runs tested
530
- the two refinements on the tasks that still failed.
531
-
532
- The evaluation also found 3 faults in flowfig itself. We fixed all 3 before this release.
533
-
534
577
  ## License
535
578
 
536
579
  MIT. See [LICENSE](LICENSE).
package/dist/browser.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // The capture browser of `flowfig gif`: find it, start it in headless mode, and talk CDP to it over a pipe. Node only.
2
- import { spawn } from 'node:child_process';
2
+ import { spawn, spawnSync } from 'node:child_process';
3
3
  import { win32 } from 'node:path';
4
4
  const MAC = ['Google Chrome', 'Microsoft Edge', 'Chromium', 'Brave Browser'].map((n) => `/Applications/${n}.app/Contents/MacOS/${n}`);
5
5
  const LINUX = [
@@ -75,8 +75,12 @@ export function launch(path, profile, timeoutMs = 30_000) {
75
75
  // The browser starts helper processes in its process group. A helper can outlive the browser and keep a pipe open.
76
76
  const killGroup = () => {
77
77
  try {
78
- if (process.platform === 'win32')
78
+ // Windows has no process group: taskkill /T stops the helpers of the browser too.
79
+ if (process.platform === 'win32') {
80
+ if (child.pid)
81
+ spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
79
82
  child.kill('SIGKILL');
83
+ }
80
84
  else if (child.pid)
81
85
  process.kill(-child.pid, 'SIGKILL');
82
86
  }
package/dist/check.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // The rules behind `flowfig check`. They never draw and never measure: a renderer gives a Scene, and these read it. So one rule set
2
2
  // covers the SVG (estimated text) and the React player (measured text), and any later view that gives a scene.
3
3
  import { DARK, LIGHT, ON_ACCENT, TONES, edgeId, isGroup, dayOf, isLanesLayout, validAt, laneColumns, nodes, str, toBeat, } from './model.js';
4
+ import { crosses } from './geometry.js';
4
5
  import { textWidth } from './text.js';
5
6
  import { owners, parseSource } from './source.js';
6
7
  const err = (rule, ids, message) => ({ rule, severity: 'error', ids, message });
@@ -111,6 +112,7 @@ const at = ([p0, p1, p2, p3], t) => {
111
112
  const [a, b, c, d] = [u * u * u, 3 * u * u * t, 3 * u * t * t, t * t * t];
112
113
  return { x: a * p0.x + b * p1.x + c * p2.x + d * p3.x, y: a * p0.y + b * p1.y + c * p2.y + d * p3.y };
113
114
  };
115
+ const lerp = (p, q, t) => ({ x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t });
114
116
  // The margins keep a touch from counting: an edge leaves along a border, and pills sit edge to edge.
115
117
  const inside = (p, r, pad) => p.x > r.x + pad && p.x < r.x + r.w - pad && p.y > r.y + pad && p.y < r.y + r.h - pad;
116
118
  const overlap = (a, b, pad) => a.x + pad < b.x + b.w - pad && b.x + pad < a.x + a.w - pad && a.y + pad < b.y + b.h - pad && b.y + pad < a.y + a.h - pad;
@@ -125,13 +127,57 @@ export function checkScene(scene, { width = 830, minText = 10 } = {}) {
125
127
  out.push(err('text-overflow', [b.id], `box "${b.id}": "${t.text}" needs ${Math.ceil(need)} px, has ${Math.floor(t.room)} px`));
126
128
  }
127
129
  for (const e of scene.edges) {
128
- if (e.behind)
130
+ if (e.behind && !e.elbow)
129
131
  continue;
130
- const pts = Array.from({ length: 33 }, (_, i) => at(e.curve, i / 32));
132
+ const line = e.elbow ?? e.pts;
133
+ const pts = line
134
+ ? line.slice(1).flatMap((q, k) => Array.from({ length: 17 }, (_, i) => lerp(line[k], q, i / 16)))
135
+ : Array.from({ length: 33 }, (_, i) => at(e.curve, i / 32));
131
136
  for (const b of scene.boxes)
132
137
  if (b.id !== e.from && b.id !== e.to && pts.some((p) => inside(p, b.rect, 2)))
133
138
  out.push(err('edge-crosses-box', [e.id, b.id], `edge "${e.id}" passes through box "${b.id}"`));
134
139
  }
140
+ // A stub pill (the label of an edge with `pts`) must not sit on another edge's path.
141
+ const pills = scene.edges.filter((e) => e.pts && e.label);
142
+ const under = new Set();
143
+ for (const e of scene.edges) {
144
+ if (e.behind)
145
+ continue;
146
+ const pts = e.pts
147
+ ? e.pts.slice(1).flatMap((q, k) => Array.from({ length: 17 }, (_, i) => lerp(e.pts[k], q, i / 16)))
148
+ : Array.from({ length: 33 }, (_, i) => at(e.curve, i / 32));
149
+ for (const f of pills)
150
+ if (f.id !== e.id && !under.has(e.id + ' ' + f.id) && pts.some((p) => inside(p, f.label, 2))) {
151
+ under.add(e.id + ' ' + f.id);
152
+ out.push(err('label-overlap', [e.id, f.id], `edge "${e.id}" passes under the pill of edge "${f.id}"`));
153
+ }
154
+ }
155
+ // A stub line (a segment of an edge with `pts`) must not cross the path of another edge.
156
+ const path = (e) => e.pts
157
+ ? e.pts.slice(1).flatMap((q, k) => Array.from({ length: 17 }, (_, i) => lerp(e.pts[k], q, i / 16)))
158
+ : Array.from({ length: 33 }, (_, i) => at(e.curve, i / 32));
159
+ const stubs = scene.edges.filter((e) => e.pts && e.pts.length > 1);
160
+ const hit = new Set();
161
+ for (const f of stubs)
162
+ for (const e of scene.edges) {
163
+ if (e.behind || e === f || (e.id === f.id && e.pts) || hit.has(f.id + ' ' + e.id))
164
+ continue;
165
+ const p = path(e);
166
+ if (f.pts.slice(1).some((a, k) => p.slice(1).some((b, i) => crosses(f.pts[k], a, p[i], b)))) {
167
+ hit.add(f.id + ' ' + e.id);
168
+ out.push(warn('stub-crosses-edge', [f.id, e.id], `the stub of edge "${f.id}" crosses edge "${e.id}"`));
169
+ }
170
+ }
171
+ // A stub pill or an edge label sits inside one band: a pill across a band border reads as part of two lanes.
172
+ for (const f of scene.edges.filter((e) => e.label))
173
+ for (const l of scene.lanes ?? []) {
174
+ const [p, r] = [f.label, l.rect];
175
+ const inside = p.x >= r.x && p.x + p.w <= r.x + r.w && p.y >= r.y && p.y + p.h <= r.y + r.h;
176
+ if (!inside && overlap(p, r, 0))
177
+ out.push(err('label-overlap', [f.id], f.pts
178
+ ? `the pill of edge "${f.id}" crosses the border of lane "${l.id}"`
179
+ : `the label of edge "${f.id}" crosses the border of lane "${l.id}"`));
180
+ }
135
181
  const labeled = scene.edges.filter((e) => e.label);
136
182
  labeled.forEach((e, i) => {
137
183
  for (const f of labeled.slice(i + 1))
package/dist/cli.js CHANGED
@@ -229,7 +229,7 @@ if (args[0] === 'gif') {
229
229
  await (await launching?.catch(() => undefined))?.close();
230
230
  try {
231
231
  // The retries cover Windows, where the browser can hold a file lock for a short time after the exit.
232
- rmSync(temp, { recursive: true, force: true, maxRetries: 3 });
232
+ rmSync(temp, { recursive: true, force: true, maxRetries: 10 });
233
233
  }
234
234
  catch (e) {
235
235
  console.error(`warning: could not remove ${temp}: ${e.message}`);
@@ -352,7 +352,7 @@ if (errors)
352
352
  process.exit(1);
353
353
  const dest = out ?? (input === '-' ? 'figure.svg' : input.replace(/\.[^./\\]+$/, '') + '.svg');
354
354
  // The spec rides along in <metadata>: an SVG is then its own source, and no JSON has to be kept.
355
- const withSpec = svgWithSpec(props);
355
+ const withSpec = svgWithSpec(props, opts);
356
356
  writeFileSync(dest, withSpec);
357
357
  console.log(`${dest} — ${(withSpec.length / 1024).toFixed(1)} kB`);
358
358
  if (open) {
@@ -9,11 +9,27 @@ export type Pt = {
9
9
  x: number;
10
10
  y: number;
11
11
  };
12
+ /** A cross-block edge of wrapped lanes: the two drawn stubs, their pills, and their points for `check`. `d` then holds both stubs,
13
+ * so a packet runs the source stub and jumps to the target stub. */
14
+ export type Stub = {
15
+ parts: [string, string];
16
+ pills: [Rect, Rect];
17
+ pts: [Pt[], Pt[]];
18
+ short?: true;
19
+ tight: [boolean, boolean];
20
+ };
21
+ /** `elbow` holds the corners of an elbow path, so `check` can test its straight runs. */
12
22
  export type Routed = {
13
23
  id: string;
14
24
  d: string;
15
25
  mid: Pt;
16
26
  curve: [Pt, Pt, Pt, Pt];
27
+ stub?: Stub;
28
+ elbow?: Pt[];
29
+ };
30
+ /** A rect that a route keeps clear of. `box` marks a box: an elbow's horizontal runs keep clear of a box, not of a label. */
31
+ export type Avoid = Rect & {
32
+ box?: boolean;
17
33
  };
18
34
  type Around = 'above' | 'below';
19
35
  export declare function route(edges: {
@@ -23,5 +39,15 @@ export declare function route(edges: {
23
39
  around?: Around;
24
40
  sides?: [Side, Side];
25
41
  elbow?: boolean;
26
- }[], rects: Record<string, Rect>, tips?: Set<string>, avoid?: Rect[]): Routed[];
42
+ stub?: number[];
43
+ bands?: [Rect | undefined, Rect | undefined];
44
+ labelW?: number;
45
+ }[], rects: Record<string, Rect>, tips?: Set<string>, avoid?: Avoid[], // what an elbow's vertical run and a stub pill must not cross (the timeline's outside labels; the boxes and gutters of lanes)
46
+ area?: Rect, // where a stub pill may sit: the lanes right of the gutter
47
+ lanes?: {
48
+ bands: Rect[];
49
+ boxes: Rect[];
50
+ }): Routed[];
51
+ /** True if the segment a-b crosses the segment c-d. The ends of a-b are trimmed by 1.5 px, so a touch at a box side does not count. */
52
+ export declare function crosses(a: Pt, b: Pt, c: Pt, d: Pt): boolean;
27
53
  export {};