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 +183 -140
- package/dist/browser.js +6 -2
- package/dist/check.js +48 -2
- package/dist/cli.js +2 -2
- package/dist/geometry.d.ts +27 -1
- package/dist/geometry.js +288 -24
- package/dist/guide.d.ts +1 -1
- package/dist/guide.js +11 -2
- package/dist/index.js +247 -103
- package/dist/init.js +5 -2
- package/dist/mcp.js +1 -1
- package/dist/model.d.ts +97 -0
- package/dist/model.js +251 -1
- package/dist/scene.d.ts +9 -0
- package/dist/svg.d.ts +3 -0
- package/dist/svg.js +190 -100
- package/dist/text.d.ts +2 -0
- package/dist/text.js +19 -0
- package/dist/verify.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+

|
|
129
|
+
|
|
130
|
+
The map and the rail (`rail: true`) adds one row for each message, with its payload:
|
|
131
|
+
|
|
132
|
+

|
|
133
|
+
|
|
134
|
+
The rail only (`rail: "only"`) shows the order of the messages without the map:
|
|
135
|
+
|
|
136
|
+

|
|
137
|
+
|
|
138
|
+
Swimlanes (`lanes: true`) give each role a lane and put the steps left to right in time order:
|
|
139
|
+
|
|
140
|
+

|
|
141
|
+
|
|
142
|
+
A long swimlane process wraps into blocks at the page width. An edge between two blocks becomes two labeled stubs:
|
|
143
|
+
|
|
144
|
+

|
|
145
|
+
|
|
146
|
+
The timeline (`timeline: true`) draws a roadmap with one track per team, dated bars, milestones and a today line:
|
|
147
|
+
|
|
148
|
+

|
|
149
|
+
|
|
150
|
+
A state lifecycle marks the first state with `mark: "start"` and each final state with `mark: "end"`:
|
|
151
|
+
|
|
152
|
+

|
|
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
|
-
|
|
|
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
|
-
- [
|
|
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
|
-
##
|
|
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
|
-

|
|
190
|
-
|
|
191
|
-
The map with the rail:
|
|
192
|
-
|
|
193
|
-

|
|
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
|
-

|
|
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
|
|
220
|
-
| -------- |
|
|
221
|
-
| `id` | The name that edges and steps use. It must be unique.
|
|
222
|
-
| `label` | The title in the box.
|
|
223
|
-
| `sub` | A smaller line under the label.
|
|
224
|
-
| `shape` | `"box"`, `"decision"` (a diamond) or `"store"` (a data cylinder).
|
|
225
|
-
| `source` | The code this draws: `path` or `path#symbol`. `flowfig verify` checks it.
|
|
226
|
-
| `lines` | The least number of text lines that a content card keeps.
|
|
227
|
-
| `width` | The width in px. This value replaces the width that layout picks.
|
|
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
|
|
286
|
-
| ---------- |
|
|
287
|
-
| `rail` | `true` draws the rail under the map. `"only"` draws the rail alone.
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
-
| `
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
|
309
|
-
|
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
312
|
-
| `edge
|
|
313
|
-
| `
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
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
|
|
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-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-

|
|
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
|
-

|
|
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
|
-

|
|
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
|
-
|
|
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
|
|
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:
|
|
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) {
|
package/dist/geometry.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {};
|