flowfig 0.9.0 → 0.11.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 +309 -61
- package/dist/atlas.d.ts +6 -0
- package/dist/atlas.js +122 -0
- package/dist/auto.d.ts +3 -0
- package/dist/auto.js +397 -0
- package/dist/browser.js +3 -1
- package/dist/check.d.ts +2 -1
- package/dist/check.js +61 -19
- package/dist/cli.js +29 -6
- package/dist/coverage.js +10 -7
- package/dist/diff.d.ts +10 -0
- package/dist/diff.js +137 -7
- package/dist/geometry.d.ts +4 -0
- package/dist/geometry.js +45 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +177 -83
- package/dist/load.js +4 -3
- package/dist/mermaid.d.ts +19 -0
- package/dist/mermaid.js +617 -0
- package/dist/model.d.ts +19 -6
- package/dist/model.js +60 -17
- package/dist/open.d.ts +1 -0
- package/dist/open.js +11 -8
- package/dist/page.d.ts +2 -1
- package/dist/page.js +5 -4
- package/dist/svg.d.ts +7 -2
- package/dist/svg.js +76 -26
- package/dist/trace.js +10 -3
- package/dist/verify.d.ts +2 -0
- package/dist/verify.js +36 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,11 +65,14 @@ jobs:
|
|
|
65
65
|
- uses: actions/setup-node@v4
|
|
66
66
|
with:
|
|
67
67
|
node-version: 22
|
|
68
|
-
- uses: iamalvisng/flowfig@v0.
|
|
68
|
+
- uses: iamalvisng/flowfig@v0.11.0
|
|
69
69
|
with:
|
|
70
70
|
figures: 'docs/**/*.svg' # default **/*.svg
|
|
71
|
+
# coverage: true # also fail the job if a figure is stale
|
|
71
72
|
```
|
|
72
73
|
|
|
74
|
+
Set `coverage: true` to run `flowfig coverage --strict` after the comment. The job then fails if a figure is stale or fails verify.
|
|
75
|
+
|
|
73
76
|
## Use it
|
|
74
77
|
|
|
75
78
|
Set up your repo:
|
|
@@ -117,7 +120,7 @@ the run.
|
|
|
117
120
|
|
|
118
121
|
1. **The agent writes JSON, not pictures.** The agent reads the code and writes a spec of the parts, the calls and their
|
|
119
122
|
order. flowfig does the layout.
|
|
120
|
-
2. **flowfig checks the spec.** `flowfig check` has
|
|
123
|
+
2. **flowfig checks the spec.** `flowfig check` has 26 rules. It finds ids that point nowhere, text wider than its box,
|
|
121
124
|
edges through boxes, overlapping labels and low contrast. A render runs the same check and writes nothing on an error.
|
|
122
125
|
The agent reads the faults and fixes the spec.
|
|
123
126
|
3. **The output is one animated SVG with no script.** The SVG plays in a GitHub README, PR or issue. It follows the light
|
|
@@ -167,7 +170,7 @@ The options are in [Open and share a figure](#open-and-share-a-figure).
|
|
|
167
170
|
| ------------------------------ | --------------------------------- | ------------------------------------ | ---------------- |
|
|
168
171
|
| Made by an agent from the code | Yes, with the `init` instructions | Yes, as Mermaid text | No |
|
|
169
172
|
| Linked to the code | Yes, `verify` fails in CI | No | No |
|
|
170
|
-
| Checked for faults | Yes,
|
|
173
|
+
| Checked for faults | Yes, 26 rules in `flowfig check` | Syntax errors only | No |
|
|
171
174
|
| Animated | Yes, one packet per message | No | No |
|
|
172
175
|
| Shows payloads and order | Yes, on the map and the rail | Order and text in a sequence diagram | No |
|
|
173
176
|
| Plays in a GitHub README | Yes, as an SVG | Yes, GitHub renders it | Yes, as an image |
|
|
@@ -185,6 +188,8 @@ The options are in [Open and share a figure](#open-and-share-a-figure).
|
|
|
185
188
|
- [Verify in CI](#verify-in-ci)
|
|
186
189
|
- [Trace the calls of a function](#trace-the-calls-of-a-function)
|
|
187
190
|
- [Find stale figures](#find-stale-figures)
|
|
191
|
+
- [One page for all figures](#one-page-for-all-figures)
|
|
192
|
+
- [Convert Mermaid](#convert-mermaid)
|
|
188
193
|
- [MCP server](#mcp-server)
|
|
189
194
|
- [Use in React](#use-in-react)
|
|
190
195
|
- [Use from Node](#use-from-node)
|
|
@@ -242,7 +247,8 @@ figure: 3 boxes, 0 groups, 2 edges, 1 step, 4 messages
|
|
|
242
247
|
browser -> api: GET /user
|
|
243
248
|
api -> db: read the user
|
|
244
249
|
step "Load a user": 4 hops
|
|
245
|
-
|
|
250
|
+
alt: Flow figure: Browser, API, Database. Steps: Load a user.
|
|
251
|
+
first.svg — 10.1 kB
|
|
246
252
|
```
|
|
247
253
|
|
|
248
254
|
The render prints the check result, the counts, one line per edge and one line per step. Read these lines to check the figure
|
|
@@ -266,8 +272,12 @@ npx flowfig --spec docs/checkout.svg > checkout.json # print the spec in the S
|
|
|
266
272
|
npx flowfig checkout.json docs/checkout-rail-only.svg
|
|
267
273
|
npx flowfig verify docs/checkout.svg # check each source and each edge against the code
|
|
268
274
|
npx flowfig diff old.svg docs/checkout.svg # list what changed in the spec
|
|
275
|
+
npx flowfig diff old.svg docs/checkout.svg --svg diff.svg # draw the change in one figure
|
|
269
276
|
```
|
|
270
277
|
|
|
278
|
+
The diff SVG is a still figure. Green marks an added box or edge, red and dashed marks a removed one, and orange marks a changed one.
|
|
279
|
+
The diff SVG has no spec. The Action and `coverage` skip it. `verify` and `--spec` report that it has no spec.
|
|
280
|
+
|
|
271
281
|
## The spec
|
|
272
282
|
|
|
273
283
|
A spec has a `layout`, `edges`, and optional `steps`. The full types have doc comments, so your editor shows each field. For the
|
|
@@ -282,6 +292,7 @@ full reference, run `npx flowfig docs`.
|
|
|
282
292
|
| `sub` | A smaller line under the label. | none |
|
|
283
293
|
| `shape` | `"box"`, `"decision"` (a diamond) or `"store"` (a data cylinder). | `"box"` |
|
|
284
294
|
| `source` | The code this draws: `path` or `path#symbol`. `flowfig verify` checks it. | none |
|
|
295
|
+
| `detail` | A more detailed figure: the SVG path from the repo root. `flowfig atlas` links the box to it. | none |
|
|
285
296
|
| `lines` | The least number of text lines that a content card keeps. | none |
|
|
286
297
|
| `width` | The width in px. This value replaces the width that layout picks. | from layout |
|
|
287
298
|
| `at` | In a `lanes` figure: the time column of the box, from 0. | the order of first use in the steps |
|
|
@@ -301,18 +312,49 @@ full reference, run `npx flowfig docs`.
|
|
|
301
312
|
| `direction` | `"row"` puts the children side by side. `"column"` stacks them. | `"row"` |
|
|
302
313
|
| `gap` | The smallest space between the children in px. It grows to fit the labels of edges that cross the group. | column: 28; row: fits the widest label, at least 56 |
|
|
303
314
|
| `align` | `"start"`, `"center"` or `"end"`, across the direction. | `"center"` (a column stretches) |
|
|
315
|
+
| `auto` | On the root layout: `true` makes flowfig place the boxes. See [Automatic layout](#automatic-layout). | off |
|
|
316
|
+
|
|
317
|
+
### Automatic layout
|
|
318
|
+
|
|
319
|
+
Set `"auto": true` on the root layout and list the boxes. flowfig places them in ranks, in the direction of the edges.
|
|
320
|
+
Use it when you do not want to plan rows and columns, or when `check` reports an edge through a box.
|
|
321
|
+
|
|
322
|
+
```json
|
|
323
|
+
{
|
|
324
|
+
"layout": {
|
|
325
|
+
"auto": true,
|
|
326
|
+
"children": [
|
|
327
|
+
{ "id": "api", "label": "API" },
|
|
328
|
+
{ "id": "queue", "label": "Queue", "shape": "store" },
|
|
329
|
+
{ "label": "Workers", "children": [{ "id": "w", "label": "Worker" }] }
|
|
330
|
+
]
|
|
331
|
+
},
|
|
332
|
+
"edges": [
|
|
333
|
+
{ "from": "api", "to": "queue", "label": "enqueue" },
|
|
334
|
+
{ "from": "queue", "to": "w", "label": "pull" }
|
|
335
|
+
]
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Render the spec with `npx flowfig spec.json docs/auto-layout.svg`. This figure has 9 boxes, 1 frame, 1 decision, 1 store and 2 steps:
|
|
340
|
+
|
|
341
|
+

|
|
342
|
+
|
|
343
|
+
A group with a `label` or an `id` stays together as one frame. flowfig removes a group with neither, with its `direction`, `gap`
|
|
344
|
+
and `align`. flowfig ignores `around` on the edges. A `direction` on the root forces `"row"` or `"column"`. `lanes` and
|
|
345
|
+
`timeline` figures ignore `auto`.
|
|
304
346
|
|
|
305
347
|
### Edges
|
|
306
348
|
|
|
307
|
-
| Field | Meaning
|
|
308
|
-
| -------- |
|
|
309
|
-
| `from` | The id of the box or group where the edge starts.
|
|
310
|
-
| `to` | The id of the box or group where the edge ends.
|
|
311
|
-
| `id` | The name that beats use.
|
|
312
|
-
| `label` | The text on the edge.
|
|
313
|
-
| `around` | `"above"` or `"
|
|
314
|
-
| `quiet` | `true` draws the edge only while a step uses it.
|
|
315
|
-
| `source` | The code this edge draws: `path` or `path#symbol`. `flowfig verify` checks it.
|
|
349
|
+
| Field | Meaning | Default |
|
|
350
|
+
| -------- | --------------------------------------------------------------------------------------------------- | ---------- |
|
|
351
|
+
| `from` | The id of the box or group where the edge starts. | required |
|
|
352
|
+
| `to` | The id of the box or group where the edge ends. | required |
|
|
353
|
+
| `id` | The name that beats use. | `from->to` |
|
|
354
|
+
| `label` | The text on the edge. | none |
|
|
355
|
+
| `around` | `"above"`, `"below"`, `"left"` or `"right"` routes the edge around the boxes between, on that side. | none |
|
|
356
|
+
| `quiet` | `true` draws the edge only while a step uses it. | `false` |
|
|
357
|
+
| `source` | The code this edge draws: `path` or `path#symbol`. `flowfig verify` checks it. | none |
|
|
316
358
|
|
|
317
359
|
### Steps
|
|
318
360
|
|
|
@@ -357,6 +399,9 @@ required. The tones are `blue` (the default), `purple`, `green`, `orange` and `g
|
|
|
357
399
|
| `autoplay` | Start to play when the figure mounts. Only the React player reads it. | `true` |
|
|
358
400
|
| `check` | Run the check rules in the browser. Only the React player reads it. | `false` |
|
|
359
401
|
|
|
402
|
+
A figure is as wide as its boxes need, and at least 320 px. The step captions wrap at the figure
|
|
403
|
+
width, so a narrow figure fits a phone screen at full size. A rail-only figure is at least 560 px.
|
|
404
|
+
|
|
360
405
|
## Check a figure
|
|
361
406
|
|
|
362
407
|
`flowfig check` reads a spec and lists the faults. The input is `-` (stdin), a `.json` file, a `.ts` module or an SVG from the
|
|
@@ -370,33 +415,36 @@ figure: 5 boxes, 3 groups, 4 edges, 2 steps, 6 messages
|
|
|
370
415
|
|
|
371
416
|
The last line gives the counts of the parts of the figure. Compare the counts with the parts that you planned.
|
|
372
417
|
|
|
373
|
-
`flowfig check` has
|
|
374
|
-
|
|
375
|
-
| Rule | Severity | What it finds
|
|
376
|
-
| --------------------------- | -------- |
|
|
377
|
-
| `unknown-id` | error | An edge, step or beat names a box, group or edge that does not exist.
|
|
378
|
-
| `duplicate-id` | error | Two boxes, two groups or two edges have the same id.
|
|
379
|
-
| `hidden-edge` | error | A `quiet` edge that no beat uses, so the figure never shows it.
|
|
380
|
-
| `text-overflow` | error | Text that needs more width than its box has.
|
|
381
|
-
| `edge-crosses-box` | error | An edge that goes through a box that is not one of its ends.
|
|
382
|
-
| `label-overlap` | error | Two edge labels overlap, or a label covers a box or an edge, or a stub label crosses a lane border.
|
|
383
|
-
| `low-contrast` | error | A text and background pair below 4.5:1, in the light, dark or custom theme, or on a tone tint.
|
|
384
|
-
| `lanes-need-column` | error | The layout of a `lanes` or `timeline` figure is not a column group of labeled groups.
|
|
385
|
-
| `bad-at` | error | An `at` value that is not an integer of 0 or more.
|
|
386
|
-
| `timeline-need-from` | error | A box in a timeline has no `from` date.
|
|
387
|
-
| `bad-date` | error | A `from`, `to` or `today` value is not a real YYYY-MM-DD date, or a `to` is before its `from`.
|
|
388
|
-
| `empty-step` | warning | A step with no beats.
|
|
389
|
-
| `small-text` | warning | At the page width, the smallest text is below the minimum size.
|
|
390
|
-
| `bad-source` | warning | A `source` that is not `path` or `path#symbol`.
|
|
391
|
-
| `
|
|
392
|
-
| `
|
|
393
|
-
| `lane-
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
418
|
+
`flowfig check` has 26 rules: 11 errors and 15 warnings.
|
|
419
|
+
|
|
420
|
+
| Rule | Severity | What it finds |
|
|
421
|
+
| --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
422
|
+
| `unknown-id` | error | An edge, step or beat names a box, group or edge that does not exist. |
|
|
423
|
+
| `duplicate-id` | error | Two boxes, two groups or two edges have the same id. |
|
|
424
|
+
| `hidden-edge` | error | A `quiet` edge that no beat uses, so the figure never shows it. |
|
|
425
|
+
| `text-overflow` | error | Text that needs more width than its box has. |
|
|
426
|
+
| `edge-crosses-box` | error | An edge that goes through a box that is not one of its ends. |
|
|
427
|
+
| `label-overlap` | error | Two edge labels overlap, or a label covers a box or an edge, or a stub label crosses a lane border. |
|
|
428
|
+
| `low-contrast` | error | A text and background pair below 4.5:1, in the light, dark or custom theme, or on a tone tint. |
|
|
429
|
+
| `lanes-need-column` | error | The layout of a `lanes` or `timeline` figure is not a column group of labeled groups. |
|
|
430
|
+
| `bad-at` | error | An `at` value that is not an integer of 0 or more. |
|
|
431
|
+
| `timeline-need-from` | error | A box in a timeline has no `from` date. |
|
|
432
|
+
| `bad-date` | error | A `from`, `to` or `today` value is not a real YYYY-MM-DD date, or a `to` is before its `from`. |
|
|
433
|
+
| `empty-step` | warning | A step with no beats. |
|
|
434
|
+
| `small-text` | warning | At the page width, the smallest text is below the minimum size. |
|
|
435
|
+
| `bad-source` | warning | A `source` that is not `path` or `path#symbol`. |
|
|
436
|
+
| `bad-detail` | warning | A `detail` that is not a relative path that ends in `.svg`. |
|
|
437
|
+
| `mark-count` | warning | A lifecycle has more than one `start` mark, or a `start` mark and no `end` mark. |
|
|
438
|
+
| `lane-column-taken` | warning | Two boxes in one lane share a time column. |
|
|
439
|
+
| `lane-end-block` | warning | In wrapped lanes, an edge ends at a lane that no block on its side shows. |
|
|
440
|
+
| `stub-crosses-edge` | warning | In wrapped lanes, a stub line crosses another edge. |
|
|
441
|
+
| `long-edge` | warning | An edge at least 600 px long that is 1.6 times or more the straight distance between its ends. |
|
|
442
|
+
| `timeline-dependency-order` | warning | In a timeline, an item does not start after the item that it depends on ends. An item may start on the day of a milestone. |
|
|
443
|
+
| `timeline-and-lanes` | warning | The figure sets `timeline` and `lanes`. The renderers draw the timeline and ignore `lanes`. |
|
|
444
|
+
| `font-estimated` | warning | `theme.font` is set. The SVG check estimates text width for the system font. |
|
|
445
|
+
| `color-not-checked` | warning | A color that the check cannot read, so its contrast is not checked. |
|
|
446
|
+
| `plain-text` | warning | Text with a code name, a filler word, or a `say` or caption over 20 words. |
|
|
447
|
+
| `auto-ignored` | warning | `auto` with `lanes` or `timeline`, `auto` on a nested group, or `around` on an edge with `auto`. |
|
|
400
448
|
|
|
401
449
|
| Option | Effect |
|
|
402
450
|
| ----------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
@@ -455,6 +503,8 @@ docs/login.svg: 7 of 7 boxes defined; edges: 9 found, 0 not found, 1 unsure, 1 n
|
|
|
455
503
|
- unsure: `verify` cannot decide, for example when a receiver has no declared type. `verify` lists it with the reason. It never fails CI.
|
|
456
504
|
- not checked: the edge has no `source`, or the language is not supported.
|
|
457
505
|
|
|
506
|
+
`verify` also checks `detail`. If the file is missing or has no figure spec, `verify` warns with `missing-detail`. `--strict` makes it an error.
|
|
507
|
+
|
|
458
508
|
A method source is `path#Owner.name`. An edge `source` names the function that makes the call.
|
|
459
509
|
|
|
460
510
|
`via` on an edge names the route, queue, topic, table, file or key that both sides use. For a `via` edge, found means that the caller code and the callee file both use that token. It does not prove that a handler serves it.
|
|
@@ -480,7 +530,7 @@ jobs:
|
|
|
480
530
|
- uses: actions/setup-node@v4
|
|
481
531
|
with:
|
|
482
532
|
node-version: 22
|
|
483
|
-
- uses: iamalvisng/flowfig@v0.
|
|
533
|
+
- uses: iamalvisng/flowfig@v0.11.0
|
|
484
534
|
with:
|
|
485
535
|
figures: 'docs/**/*.svg' # default **/*.svg
|
|
486
536
|
```
|
|
@@ -491,25 +541,44 @@ jobs:
|
|
|
491
541
|
before it writes a figure. Then the agent reads only the lines that the trace names. Each edge passes the same check as
|
|
492
542
|
`verify`.
|
|
493
543
|
|
|
544
|
+
`src/login.ts`:
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
import { verifyPassword } from './password.ts';
|
|
548
|
+
import { findUser } from './users.ts';
|
|
549
|
+
|
|
550
|
+
export function login(name: string, password: string) {
|
|
551
|
+
const user = findUser(name);
|
|
552
|
+
return verifyPassword(user, password);
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
`src/password.ts`:
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
export function verifyPassword(user: { hash: string }, password: string) {
|
|
560
|
+
return user.hash === password.split('').reverse().join('');
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
`src/users.ts`:
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
export function findUser(name: string) {
|
|
568
|
+
return { name, hash: 'drowssap' };
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
494
572
|
```console
|
|
495
|
-
$ npx flowfig trace src/
|
|
496
|
-
src/
|
|
497
|
-
src/
|
|
498
|
-
src/
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
src/verify.ts#verifyReport -> src/model.ts#nodes src/verify.ts:79
|
|
505
|
-
src/verify.ts#verifyReport -> src/edges.ts#edgeResult src/verify.ts:81
|
|
506
|
-
src/verify.ts#verifyReport -> src/model.ts#edgeId src/verify.ts:91
|
|
507
|
-
src/verify.ts#verifyReport -> src/model.ts#toBeat src/verify.ts:95
|
|
508
|
-
stop depth 2: 11 symbols not followed
|
|
509
|
-
summary: 13 symbols, 12 found, 0 unsure, 0 open, 4 calls outside the repo, 0.1 s
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
- An edge line gives the caller, the callee, and the file and line of the call.
|
|
573
|
+
$ npx flowfig trace src/login.ts#login
|
|
574
|
+
src/login.ts#login
|
|
575
|
+
5 src/users.ts#findUser
|
|
576
|
+
6 src/password.ts#verifyPassword
|
|
577
|
+
summary: 3 symbols, 2 found, 0 unsure, 0 open, 1 call outside the repo, 0.0 s
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
- A header line gives a caller as `file#symbol`. Each call line under it gives the line of the call, then the callee.
|
|
581
|
+
The call is in the file of the caller. A callee in the same file shows only its symbol.
|
|
513
582
|
- `unsure`: trace cannot name the target. An example is a call of a parameter.
|
|
514
583
|
- `open`: the target is a name in a string, for example a route table entry. Read that line yourself.
|
|
515
584
|
- `stop`: the trace reached `--depth` or `--max`. The line gives the number of functions that it did not follow.
|
|
@@ -560,6 +629,8 @@ prints one line that says so.
|
|
|
560
629
|
|
|
561
630
|
A figure covers a code file if one of its links names that file. With `--entries`, `coverage` lists each matched code
|
|
562
631
|
file that no figure covers. With no `--entries`, it prints one count line for each top folder.
|
|
632
|
+
`coverage` does not count test files as code: `*.test.*`, `*.spec.*`, `__tests__/`, `test_*.py`, `*_test.py`, `*_test.go`,
|
|
633
|
+
`*Test.java`, `*Tests.java`, `*Test.cs`, `*Tests.cs`, and files under `tests/` or `src/test/`. An entry that `--entries` names in full still counts.
|
|
563
634
|
|
|
564
635
|
| Option | Meaning | Default |
|
|
565
636
|
| ------------------ | ------------------------------------------------------------- | ------------------ |
|
|
@@ -571,6 +642,180 @@ file that no figure covers. With no `--entries`, it prints one count line for ea
|
|
|
571
642
|
|
|
572
643
|
`coverage` skips `node_modules`, `dist` and each folder with a name that starts with a dot.
|
|
573
644
|
|
|
645
|
+
## One page for all figures
|
|
646
|
+
|
|
647
|
+
`flowfig atlas` writes one web page for each figure, and an index page. A box can link to a more detailed figure with
|
|
648
|
+
`detail`. The path starts at the repo root, as `source` does.
|
|
649
|
+
|
|
650
|
+
`docs/system.svg`:
|
|
651
|
+
|
|
652
|
+
```json
|
|
653
|
+
{
|
|
654
|
+
"layout": {
|
|
655
|
+
"children": [
|
|
656
|
+
{ "id": "up", "label": "Whole system", "detail": "docs/flows/orders.svg" },
|
|
657
|
+
{ "id": "db", "label": "Orders table" }
|
|
658
|
+
]
|
|
659
|
+
},
|
|
660
|
+
"edges": []
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
`docs/flows/orders.svg`:
|
|
665
|
+
|
|
666
|
+
```json
|
|
667
|
+
{
|
|
668
|
+
"layout": {
|
|
669
|
+
"children": [
|
|
670
|
+
{ "id": "web", "label": "Web app" },
|
|
671
|
+
{ "id": "orders", "label": "Order service", "detail": "docs/system.svg" }
|
|
672
|
+
]
|
|
673
|
+
},
|
|
674
|
+
"edges": []
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
`docs/system.svg` links down to the order flow. `docs/flows/orders.svg` links back up.
|
|
679
|
+
|
|
680
|
+
```console
|
|
681
|
+
$ npx flowfig atlas
|
|
682
|
+
/path/to/repo/atlas/index.html — 2 figure pages
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
The command prints the full path of `index.html`.
|
|
686
|
+
|
|
687
|
+
| Option | Meaning | Default |
|
|
688
|
+
| ------------------ | ------------------------------------------------------------- | ------------------ |
|
|
689
|
+
| `--figures <glob>` | The SVG files to read. Only an SVG with a figure spec counts. | `**/*.svg` |
|
|
690
|
+
| `--root <dir>` | The repo root. `detail` paths start here. | the current folder |
|
|
691
|
+
| `--out <dir>` | The folder to write. | `atlas` |
|
|
692
|
+
| `--open` | Open `index.html` in the default browser. | off |
|
|
693
|
+
|
|
694
|
+
The command writes `index.html`, one `.html` page for each figure, and an empty `.nojekyll` file. It deletes nothing. If a file of the same name exists and the atlas did not write it, the command writes nothing and exits with 2.
|
|
695
|
+
All links are relative, so the pages work from a file, a sub-path or any file host. The exit code is 0 when the site is
|
|
696
|
+
written, whatever the health of the figures. It is 1 when no figure matches, and 2 for a bad flag.
|
|
697
|
+
|
|
698
|
+
The index lists each figure with its health from `coverage`: `ok`, `stale`, `fail` or `none`. A figure with a bad spec
|
|
699
|
+
has no page. Its row shows `fail` and the error.
|
|
700
|
+
|
|
701
|
+
Each figure page has these parts, in this order:
|
|
702
|
+
|
|
703
|
+
1. A link "All figures".
|
|
704
|
+
2. The figure path and its health.
|
|
705
|
+
3. The figure.
|
|
706
|
+
4. A "Details" list with one link for each box that has `detail`.
|
|
707
|
+
5. The transcript of the figure.
|
|
708
|
+
|
|
709
|
+
A link works in these places:
|
|
710
|
+
|
|
711
|
+
- In the README, an SVG is an image, so a box has no link. The README render does not change.
|
|
712
|
+
- On an atlas page, a click on a box opens the page of its `detail` figure. The mouse is the only way to use this link.
|
|
713
|
+
- The "Details" list has the same links with names. Use it with the keyboard and a screen reader.
|
|
714
|
+
- If the target figure is not in the atlas, the box has no link. The list row says "not in the atlas".
|
|
715
|
+
|
|
716
|
+
The atlas draws each spec with the installed flowfig and the default options. A page can differ from the committed SVG
|
|
717
|
+
if you drew that SVG with `--width` or `--min-text`. A figure that you remove keeps its old page in `--out`.
|
|
718
|
+
|
|
719
|
+
To publish the pages on GitHub Pages, run `npx flowfig atlas --out docs/atlas` and commit the result. Then set Pages to
|
|
720
|
+
"Deploy from a branch" with the folder `/docs`.
|
|
721
|
+
|
|
722
|
+
## Convert Mermaid
|
|
723
|
+
|
|
724
|
+
`flowfig from-mermaid` turns a Mermaid flowchart, sequence diagram, state diagram or `gantt` into a flowfig spec. The converter uses no model.
|
|
725
|
+
Then `check` and `verify` can test the figure.
|
|
726
|
+
|
|
727
|
+
```console
|
|
728
|
+
$ cat login.mmd
|
|
729
|
+
sequenceDiagram
|
|
730
|
+
participant W as Web app
|
|
731
|
+
W->>A: POST /login
|
|
732
|
+
A-->>W: 200 session cookie
|
|
733
|
+
A-)Q: user.logged_in
|
|
734
|
+
$ npx flowfig from-mermaid login.mmd > login.json
|
|
735
|
+
$ npx flowfig login.json login.svg
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
The input is a `.mmd` file with one diagram, or a Markdown file. In a Markdown file, each fenced block with the language
|
|
739
|
+
`mermaid` is one diagram. `-` reads one diagram from stdin. With one diagram, the command prints the spec. With
|
|
740
|
+
`--out <dir>`, the command writes one JSON file for each diagram, such as `docs-1.json` and `docs-2.json`.
|
|
741
|
+
|
|
742
|
+
The command exits 1 if a diagram has a line that the converter does not read. It prints `<file>:<line>: <reason>` for each
|
|
743
|
+
line, and writes no spec for that diagram. The command never guesses.
|
|
744
|
+
|
|
745
|
+
This flowchart has a decision, a subgraph and a store:
|
|
746
|
+
|
|
747
|
+
```mermaid
|
|
748
|
+
flowchart TD
|
|
749
|
+
A([Visitor]) --> B[Sign-up form]
|
|
750
|
+
B --> C{Email valid?}
|
|
751
|
+
C -->|no| X[Show error]
|
|
752
|
+
C -->|yes| D
|
|
753
|
+
subgraph auth [Account service]
|
|
754
|
+
D[Create account] --> E[Hash password]
|
|
755
|
+
end
|
|
756
|
+
E --> F[(Users)]
|
|
757
|
+
E --> G[Welcome email]
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
`npx flowfig from-mermaid signup.mmd > signup.json` converts it. `npx flowfig signup.json docs/from-mermaid.svg` renders it:
|
|
761
|
+
|
|
762
|
+

|
|
763
|
+
|
|
764
|
+
A flowchart (`flowchart` or `graph`) becomes a still map with automatic layout:
|
|
765
|
+
|
|
766
|
+
- Nodes: `A`, `A[text]`, `A(text)`, `A([text])`, `A((text))`, `A{text}` (a decision) and `A[(text)]` (a store). The text can
|
|
767
|
+
have quotes. `<br>` becomes a space. `#quot;`, `#amp;`, `#lt;`, `#gt;`, `#apos;`, `#nbsp;` and `#NN;` (a decimal code)
|
|
768
|
+
become their characters.
|
|
769
|
+
- Links: `-->`, `-.->`, `==>`, with a label as `-->|text|` or `-- text -->`. Chains (`A --> B --> C`) and `&`
|
|
770
|
+
(`A & B --> C`) are read.
|
|
771
|
+
- `subgraph id`, `subgraph id [title]`, `subgraph id["title"]` or `subgraph title` ... `end` becomes a group with a frame. A subgraph with no
|
|
772
|
+
node is dropped.
|
|
773
|
+
- `classDef`, `class`, `style`, `linkStyle`, `click`, `direction`, `:::class` and `%%` comments are ignored.
|
|
774
|
+
- In all four diagram types, `accTitle:`, `accDescr:` and `accDescr { ... }` are ignored.
|
|
775
|
+
|
|
776
|
+
A sequence diagram becomes a rail (`rail: "only"`):
|
|
777
|
+
|
|
778
|
+
- Each `participant` or `actor` is a box. A participant that first shows in a message is a box too.
|
|
779
|
+
- Each pair of participants gets one edge. Each message is one hop on that edge, with its text in `data`.
|
|
780
|
+
- A message in the other direction of the edge is a hop with `back: true`. `-)` gives `async: true`.
|
|
781
|
+
- With no `alt`, the figure has one step with all the messages.
|
|
782
|
+
- Each `alt` or `else` branch becomes one step. The step has the messages before the `alt`, the messages of that branch, and
|
|
783
|
+
the messages after the `end`. The step label is the branch condition.
|
|
784
|
+
- `loop`, `opt`, `par`, `and` and `rect` lines are ignored. Their messages stay in order in each step.
|
|
785
|
+
- `activate`, `deactivate`, `+` and `-`, `Note` and `autonumber` are ignored.
|
|
786
|
+
|
|
787
|
+
A state diagram (`stateDiagram` or `stateDiagram-v2`) becomes a still map with automatic layout:
|
|
788
|
+
|
|
789
|
+
- Each state is a box. `state "Long name" as A` sets the label. `A : text` puts the text under the label, as Mermaid does.
|
|
790
|
+
- `A --> B` and `A --> B : text` become an edge with a label.
|
|
791
|
+
- `[*] --> A` at the top level gives A `mark: "start"`. `A --> [*]` at the top level gives A `mark: "end"`.
|
|
792
|
+
- `state A { ... }` becomes a group with a frame. A `[*]` in it gives no edge and no mark.
|
|
793
|
+
- `state A <<choice>>` becomes a decision. Its label is its id.
|
|
794
|
+
- `direction`, `classDef`, `class`, `%%` comments, `note` and `hide empty description` are ignored.
|
|
795
|
+
|
|
796
|
+
A `gantt` becomes a timeline (`timeline: true`):
|
|
797
|
+
|
|
798
|
+
- Each `section` is a track. Each task is a box in its track. A task with no id gets the id `task-1`, `task-2` and so on.
|
|
799
|
+
- A task can have a start date or `after <id>`, and an end date or a duration in days (`10d`) or weeks (`2w`).
|
|
800
|
+
A task with only a duration starts when the task above it ends.
|
|
801
|
+
- Mermaid reads an end date as the start of that day. So the box `to` is the day before the end date.
|
|
802
|
+
- `after a b` starts the task when the last of `a` and `b` ends. Each id gives an edge to the task.
|
|
803
|
+
- `milestone` with `0d` gives a box with only `from`. `crit` gives `tone: "red"`. `done` and `active` are ignored.
|
|
804
|
+
- `title`, `axisFormat`, `tickInterval`, `todayMarker`, `weekday` and `click` are ignored. The today line uses the flowfig default.
|
|
805
|
+
|
|
806
|
+
Limits:
|
|
807
|
+
|
|
808
|
+
- `RL` reads as `LR`, and `BT` reads as `TB`. The automatic layout sets the order of the boxes.
|
|
809
|
+
- `-.->` and `==>` give a plain edge. flowfig has no dotted or thick edge.
|
|
810
|
+
- A link with no arrow (`---`, `-.-`, `===`) is an error. Use `-->` to give it a direction.
|
|
811
|
+
- Other node shapes, Markdown strings, `<-->`, `--o`, `--x`, `-x` and a message to the same participant are errors.
|
|
812
|
+
- A second `alt` and an `alt` inside an `alt` are errors.
|
|
813
|
+
- In a state diagram, `<<fork>>`, `<<join>>`, a concurrent region (`--`), `:::class` and a transition to the same state
|
|
814
|
+
are errors. A `[*]` on a composite state, a description on a composite state and a label on a `[*]` transition are errors too.
|
|
815
|
+
- A state with a start and an end transition is an error, because a box has one mark.
|
|
816
|
+
- In a `gantt`, a `dateFormat` other than `YYYY-MM-DD`, `excludes`, `weekend`, `inclusiveEndDates`, `until`, a duration in
|
|
817
|
+
hours and a milestone with a duration are errors. These change the dates, and the converter never guesses a date.
|
|
818
|
+
|
|
574
819
|
## MCP server
|
|
575
820
|
|
|
576
821
|
`npx flowfig mcp` serves the tools `docs`, `check`, `render`, `verify` and `diff` over stdio, for an agent with no shell.
|
|
@@ -600,12 +845,15 @@ The player has these controls:
|
|
|
600
845
|
- One tab for each step, with a progress line. A click on a tab starts that step.
|
|
601
846
|
- A pause button, and a speed button that changes between 1× and 2×.
|
|
602
847
|
- A full screen button. Esc closes full screen.
|
|
848
|
+
- The step tabs take the Left, Right, Home and End keys.
|
|
603
849
|
- A hover on a box highlights its edges. With the rail, a click on a row starts that message, and a hover highlights its edge.
|
|
604
850
|
- A hover on a box, an edge or a rail row with a `source` shows the `source`. The SVG shows it as a tooltip too.
|
|
605
851
|
|
|
606
852
|
The player props are the spec fields. `theme` sets the colors, `speed` sets the packet time, and `autoplay={false}` stops the
|
|
607
|
-
auto start. `check` runs the check rules. If a figure is wider than its container, the player
|
|
608
|
-
|
|
853
|
+
auto start. `check` runs the check rules. If a figure is wider than its container, the player changes the figure to fit.
|
|
854
|
+
In a box under 600 px, the player keeps reader text at 11 px or more. If the map does not fit, the player draws the map as a column.
|
|
855
|
+
In a box of 600 px or more, the player folds the map only when text would fall under 9 px.
|
|
856
|
+
A lanes map or a timeline map stops at that size and scrolls sideways in its pane. If the reader asks for reduced motion, the player shows the last beat and does not move packets.
|
|
609
857
|
|
|
610
858
|
The player has no dark theme of its own. For dark mode, set the `--fig-*` CSS variables on `.flowfig`:
|
|
611
859
|
|
package/dist/atlas.d.ts
ADDED
package/dist/atlas.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, posix, resolve } from 'node:path';
|
|
3
|
+
import { coverageLines, coverageReport } from './coverage.js';
|
|
4
|
+
import { loadSpec, specOf } from './load.js';
|
|
5
|
+
import { altText, nodes, str } from './model.js';
|
|
6
|
+
import { openFile } from './open.js';
|
|
7
|
+
import { esc, pageHtml } from './page.js';
|
|
8
|
+
import { toSvg } from './svg.js';
|
|
9
|
+
const USAGE = 'usage: flowfig atlas [--figures <glob>] [--root <dir>] [--out <dir>] [--open]';
|
|
10
|
+
const CSS = `body { font: 16px/1.5 system-ui, sans-serif; margin: 0 auto; max-width: 960px; padding: 16px }
|
|
11
|
+
table { border-collapse: collapse; width: 100% }
|
|
12
|
+
td, th { text-align: left; padding: 6px 10px; border-bottom: 1px solid color-mix(in srgb, CanvasText 20%, transparent) }
|
|
13
|
+
strong[data-state] { padding: 1px 8px; border-radius: 4px; border: 1px solid currentColor; font-size: 14px }
|
|
14
|
+
strong[data-state="ok"] { color: light-dark(#116329, #7ee787) }
|
|
15
|
+
strong[data-state="stale"] { color: light-dark(#8a5a00, #e3b341) }
|
|
16
|
+
strong[data-state="fail"] { color: light-dark(#b42318, #ff7b72) }
|
|
17
|
+
strong[data-state="none"] { color: GrayText }`;
|
|
18
|
+
const badge = (f) => `<strong data-state="${f.state}">${f.state}</strong>`;
|
|
19
|
+
const pageOf = (figure) => {
|
|
20
|
+
const page = figure.replace(/\.svg$/, '.html');
|
|
21
|
+
return page === 'index.html' ? 'index.figure.html' : page;
|
|
22
|
+
};
|
|
23
|
+
const MARK = '<meta name="generator" content="flowfig atlas">';
|
|
24
|
+
const url = (path) => path.split('/').map(encodeURIComponent).join('/');
|
|
25
|
+
const hrefTo = (from, to) => url(posix.relative(posix.dirname(from), to));
|
|
26
|
+
export function atlasFiles({ root = process.cwd(), figures } = {}) {
|
|
27
|
+
const report = coverageReport({ root, figures });
|
|
28
|
+
const rows = report.figures.sort((a, b) => a.figure.localeCompare(b.figure));
|
|
29
|
+
if (!rows.length)
|
|
30
|
+
return new Map();
|
|
31
|
+
const specs = new Map();
|
|
32
|
+
for (const { figure } of rows) {
|
|
33
|
+
try {
|
|
34
|
+
specs.set(figure, loadSpec(JSON.parse(specOf(readFileSync(resolve(root, figure), 'utf8'), figure)), figure));
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
}
|
|
38
|
+
const files = new Map();
|
|
39
|
+
const shell = (title, body) => `<!doctype html>\n<meta charset="utf-8">\n${MARK}\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<title>${esc(title)}</title>\n<style>\n:root { color-scheme: light dark }\n${CSS}\n</style>\n${body}\n`;
|
|
40
|
+
for (const f of rows) {
|
|
41
|
+
const spec = specs.get(f.figure);
|
|
42
|
+
if (!spec)
|
|
43
|
+
continue;
|
|
44
|
+
const page = pageOf(f.figure);
|
|
45
|
+
const targets = nodes(spec.layout).flatMap((n) => {
|
|
46
|
+
const to = typeof n.detail === 'string' ? posix.normalize(n.detail) : '';
|
|
47
|
+
return to && specs.has(to) ? [{ n, to }] : [];
|
|
48
|
+
});
|
|
49
|
+
const hrefs = new Map(targets.map(({ n, to }) => [n.id, hrefTo(page, pageOf(to))]));
|
|
50
|
+
const svg = toSvg(spec, { links: Object.fromEntries(hrefs) });
|
|
51
|
+
const details = nodes(spec.layout)
|
|
52
|
+
.filter((n) => typeof n.detail === 'string')
|
|
53
|
+
.map((n) => {
|
|
54
|
+
const name = esc(str(n.label) || n.id);
|
|
55
|
+
const href = hrefs.get(n.id);
|
|
56
|
+
return `<li>${name}: ${href == null ? `${esc(n.detail)} (not in the atlas)` : `<a href="${esc(href)}">${esc(n.detail)}</a>`}</li>`;
|
|
57
|
+
});
|
|
58
|
+
const { title, desc } = altText(spec);
|
|
59
|
+
const before = `<style>\n${CSS}\n</style>\n<main>\n<p><a href="${esc(hrefTo(page, 'index.html'))}">All figures</a></p>\n` +
|
|
60
|
+
`<h1>${esc(f.figure)}</h1>\n<p>${badge(f)} ${esc(f.detail)}</p>\n`;
|
|
61
|
+
const after = (details.length ? `\n<h2>Details</h2>\n<ul>\n${details.join('\n')}\n</ul>` : '') +
|
|
62
|
+
`\n<details><summary>Transcript</summary><pre style="white-space: pre-wrap">${esc(desc)}</pre></details>\n</main>`;
|
|
63
|
+
files.set(page, pageHtml(svg, `${title} (${f.figure})`, before, after, `${MARK}\n`));
|
|
64
|
+
}
|
|
65
|
+
const body = rows
|
|
66
|
+
.map((f) => {
|
|
67
|
+
const spec = specs.get(f.figure);
|
|
68
|
+
const name = spec ? `<a href="${esc(url(pageOf(f.figure)))}">${esc(f.figure)}</a>` : esc(f.figure);
|
|
69
|
+
return `<tr><td>${name}</td><td>${badge(f)}</td><td>${esc(f.detail)}</td><td>${spec ? esc(altText(spec).title) : ''}</td></tr>`;
|
|
70
|
+
})
|
|
71
|
+
.join('\n');
|
|
72
|
+
files.set('index.html', shell('Figures', `<main>\n<h1>Figures</h1>\n<p>${esc(coverageLines(report).at(-1))}</p>\n<table>\n<tr><th>Figure</th><th>Health</th><th>Detail</th><th>Title</th></tr>\n${body}\n</table>\n</main>`));
|
|
73
|
+
files.set('.nojekyll', '');
|
|
74
|
+
return files;
|
|
75
|
+
}
|
|
76
|
+
export async function runAtlas(argv) {
|
|
77
|
+
const fail = (message, code) => (console.error(message), code);
|
|
78
|
+
const args = [...argv];
|
|
79
|
+
const open = args.includes('--open');
|
|
80
|
+
if (open)
|
|
81
|
+
args.splice(args.indexOf('--open'), 1);
|
|
82
|
+
const opts = {};
|
|
83
|
+
for (const name of ['--figures', '--root', '--out']) {
|
|
84
|
+
const at = args.indexOf(name);
|
|
85
|
+
if (at === -1)
|
|
86
|
+
continue;
|
|
87
|
+
const value = args.splice(at, 2)[1];
|
|
88
|
+
if (!value || value.startsWith('-'))
|
|
89
|
+
return fail(`${name} needs a value`, 2);
|
|
90
|
+
opts[name] = value;
|
|
91
|
+
}
|
|
92
|
+
if (args.length)
|
|
93
|
+
return fail(args[0].startsWith('-') ? `unknown flag ${args[0]}` : USAGE, 2);
|
|
94
|
+
const root = resolve(opts['--root'] ?? '.');
|
|
95
|
+
if (!existsSync(root))
|
|
96
|
+
return fail(`${root}: the folder does not exist`, 2);
|
|
97
|
+
const files = atlasFiles({ root, figures: opts['--figures'] });
|
|
98
|
+
if (!files.size)
|
|
99
|
+
return fail('no figure SVG matches', 1);
|
|
100
|
+
const out = resolve(opts['--out'] ?? 'atlas');
|
|
101
|
+
const mine = [...files.keys()].filter((p) => p !== '.nojekyll');
|
|
102
|
+
const foreign = mine.find((p) => existsSync(join(out, p)) && !readFileSync(join(out, p), 'utf8').includes(MARK));
|
|
103
|
+
if (foreign)
|
|
104
|
+
return fail(`${join(out, foreign)}: the file exists and flowfig atlas did not write it; nothing was written`, 2);
|
|
105
|
+
for (const [path, text] of files) {
|
|
106
|
+
if (!text && existsSync(join(out, path)))
|
|
107
|
+
continue;
|
|
108
|
+
mkdirSync(dirname(join(out, path)), { recursive: true });
|
|
109
|
+
writeFileSync(join(out, path), text);
|
|
110
|
+
}
|
|
111
|
+
const index = join(out, 'index.html');
|
|
112
|
+
console.log(`${index} — ${files.size - 2} figure page${files.size === 3 ? '' : 's'}`);
|
|
113
|
+
if (open) {
|
|
114
|
+
try {
|
|
115
|
+
await openFile(index);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
return fail(`open: ${e.message}`, 1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
package/dist/auto.d.ts
ADDED