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 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.9.0
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 23 rules. It finds ids that point nowhere, text wider than its box,
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, 23 rules in `flowfig check` | Syntax errors only | No |
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
- first.svg — 9.7 kB
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
+ ![Flow figure: Shopper, Web shop, In stock?, Order, Payment, Shipping, Orders, Email, Back order. Steps: Item in stock, Item sold out.](docs/auto-layout.svg)
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 | Default |
308
- | -------- | ------------------------------------------------------------------------------ | ---------- |
309
- | `from` | The id of the box or group where the edge starts. | required |
310
- | `to` | The id of the box or group where the edge ends. | required |
311
- | `id` | The name that beats use. | `from->to` |
312
- | `label` | The text on the edge. | none |
313
- | `around` | `"above"` or `"below"` routes the edge over or under the boxes between. | none |
314
- | `quiet` | `true` draws the edge only while a step uses it. | `false` |
315
- | `source` | The code this edge draws: `path` or `path#symbol`. `flowfig verify` checks it. | none |
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 23 rules: 11 errors and 12 warnings.
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
- | `mark-count` | warning | A lifecycle has more than one `start` mark, or a `start` mark and no `end` mark. |
392
- | `lane-column-taken` | warning | Two boxes in one lane share a time column. |
393
- | `lane-end-block` | warning | In wrapped lanes, an edge ends at a lane that no block on its side shows. |
394
- | `stub-crosses-edge` | warning | In wrapped lanes, a stub line crosses another edge. |
395
- | `timeline-dependency-order` | warning | In a timeline, an item does not start after the item that it depends on ends. |
396
- | `timeline-and-lanes` | warning | The figure sets `timeline` and `lanes`. The renderers draw the timeline and ignore `lanes`. |
397
- | `font-estimated` | warning | `theme.font` is set. The SVG check estimates text width for the system font. |
398
- | `color-not-checked` | warning | A color that the check cannot read, so its contrast is not checked. |
399
- | `plain-text` | warning | Text with a code name, a filler word, or a `say` or caption over 20 words. |
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.9.0
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/verify.ts#verify
496
- src/verify.ts#verify -> src/verify.ts#verifyReport src/verify.ts:104
497
- src/verify.ts#verifyReport -> src/verify.ts#emptyCoverage src/verify.ts:48
498
- src/verify.ts#verifyReport -> src/source.ts#links src/verify.ts:49
499
- src/verify.ts#verifyReport -> src/code.ts#outside src/verify.ts:58
500
- src/verify.ts#verifyReport -> src/code.ts#codeFile src/verify.ts:65
501
- src/verify.ts#verifyReport -> src/code.ts#isDefined src/verify.ts:67
502
- src/verify.ts#verifyReport -> src/verify.ts#escape src/verify.ts:69
503
- src/verify.ts#verifyReport -> src/verify.ts#hasHeading src/verify.ts:70
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
+ ![Flow figure: Visitor, Sign-up form, Email valid?, Show error, Create account, Hash password, Users, Welcome email.](docs/from-mermaid.svg)
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 shrinks the figure to half size at
608
- most, then scrolls. If the reader asks for reduced motion, the player shows the last beat and does not move packets.
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
 
@@ -0,0 +1,6 @@
1
+ export type AtlasOptions = {
2
+ root?: string;
3
+ figures?: string;
4
+ };
5
+ export declare function atlasFiles({ root, figures }?: AtlasOptions): Map<string, string>;
6
+ export declare function runAtlas(argv: string[]): Promise<number>;
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
@@ -0,0 +1,3 @@
1
+ import { type FigEdge, type FlowProps } from './model.ts';
2
+ export declare const rowBreaks: WeakSet<FigEdge>;
3
+ export declare function autoLayout(spec: FlowProps): FlowProps;