flowfig 0.1.0 → 0.2.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
@@ -80,6 +80,8 @@ Then ask your agent: "draw a diagram of how login works in this repo".
80
80
  - [Three forms](#three-forms)
81
81
  - [The spec](#the-spec)
82
82
  - [Check a figure](#check-a-figure)
83
+ - [Verify in CI](#verify-in-ci)
84
+ - [MCP server](#mcp-server)
83
85
  - [Use in React](#use-in-react)
84
86
  - [Use from Node](#use-from-node)
85
87
  - [Use with your coding agent](#use-with-your-coding-agent)
@@ -172,6 +174,8 @@ Each SVG from the CLI carries its own spec. These commands made the rail-only fi
172
174
  npx flowfig --spec docs/checkout.svg > checkout.json # print the spec in the SVG
173
175
  # edit checkout.json: set "props.rail" to "only"
174
176
  npx flowfig checkout.json docs/checkout-rail-only.svg
177
+ npx flowfig verify docs/checkout.svg # check that each source still exists
178
+ npx flowfig diff old.svg docs/checkout.svg # list what changed in the spec
175
179
  ```
176
180
 
177
181
  ## The spec
@@ -181,14 +185,15 @@ full reference, run `npx flowfig docs`.
181
185
 
182
186
  ### Boxes
183
187
 
184
- | Field | Meaning | Default |
185
- | ------- | ----------------------------------------------------------------- | ----------- |
186
- | `id` | The name that edges and steps use. It must be unique. | required |
187
- | `label` | The title in the box. | required |
188
- | `sub` | A smaller line under the label. | none |
189
- | `shape` | `"box"`, `"decision"` (a diamond) or `"store"` (a data cylinder). | `"box"` |
190
- | `lines` | The least number of text lines that a content card keeps. | none |
191
- | `width` | The width in px. This value replaces the width that layout picks. | from layout |
188
+ | Field | Meaning | Default |
189
+ | -------- | ------------------------------------------------------------------------- | ----------- |
190
+ | `id` | The name that edges and steps use. It must be unique. | required |
191
+ | `label` | The title in the box. | required |
192
+ | `sub` | A smaller line under the label. | none |
193
+ | `shape` | `"box"`, `"decision"` (a diamond) or `"store"` (a data cylinder). | `"box"` |
194
+ | `source` | The code this draws: `path` or `path#symbol`. `flowfig verify` checks it. | none |
195
+ | `lines` | The least number of text lines that a content card keeps. | none |
196
+ | `width` | The width in px. This value replaces the width that layout picks. | from layout |
192
197
 
193
198
  ### Groups
194
199
 
@@ -205,14 +210,15 @@ full reference, run `npx flowfig docs`.
205
210
 
206
211
  ### Edges
207
212
 
208
- | Field | Meaning | Default |
209
- | -------- | ----------------------------------------------------------------------- | ---------- |
210
- | `from` | The id of the box or group where the edge starts. | required |
211
- | `to` | The id of the box or group where the edge ends. | required |
212
- | `id` | The name that beats use. | `from->to` |
213
- | `label` | The text on the edge. | none |
214
- | `around` | `"above"` or `"below"` routes the edge over or under the boxes between. | none |
215
- | `quiet` | `true` draws the edge only while a step uses it. | `false` |
213
+ | Field | Meaning | Default |
214
+ | -------- | ------------------------------------------------------------------------------ | ---------- |
215
+ | `from` | The id of the box or group where the edge starts. | required |
216
+ | `to` | The id of the box or group where the edge ends. | required |
217
+ | `id` | The name that beats use. | `from->to` |
218
+ | `label` | The text on the edge. | none |
219
+ | `around` | `"above"` or `"below"` routes the edge over or under the boxes between. | none |
220
+ | `quiet` | `true` draws the edge only while a step uses it. | `false` |
221
+ | `source` | The code this edge draws: `path` or `path#symbol`. `flowfig verify` checks it. | none |
216
222
 
217
223
  ### Steps
218
224
 
@@ -229,13 +235,13 @@ Each step is one story. The player shows one tab for each step. With no steps, t
229
235
 
230
236
  A beat is an edge id, an array of edge ids that run at the same time, or an object:
231
237
 
232
- | Field | Meaning | Default |
233
- | ------- | ---------------------------------------------------------------------------------- | -------------- |
234
- | `edges` | One hop or an array of hops. A hop is an edge id or `{ edge, back, data, async }`. | none (a pause) |
235
- | `say` | The line of narration for the beat. | none |
236
- | `show` | `{ boxId: content }` fills the content card of a box until the step ends. | none |
237
- | `light` | The ids of the boxes to highlight for this beat only. | none |
238
- | `ms` | The length of the beat in ms. | `speed` (900) |
238
+ | Field | Meaning | Default |
239
+ | ------- | ------------------------------------------------------------------------------------------ | -------------- |
240
+ | `edges` | One hop or an array of hops. A hop is an edge id or `{ edge, back, data, async, source }`. | none (a pause) |
241
+ | `say` | The line of narration for the beat. | none |
242
+ | `show` | `{ boxId: content }` fills the content card of a box until the step ends. | none |
243
+ | `light` | The ids of the boxes to highlight for this beat only. | none |
244
+ | `ms` | The length of the beat in ms. | `speed` (900) |
239
245
 
240
246
  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
241
247
  that does not wait for an answer.
@@ -294,6 +300,43 @@ A render runs the same check first. If the check finds an error, the render writ
294
300
  In React, `<Flow check />` runs the same rules on the layout that the browser drew. The player prints each fault with
295
301
  `console.warn`.
296
302
 
303
+ ## Verify in CI
304
+
305
+ A box, an edge or a hop can name the code it draws: `"source": "src/auth/login.ts#verifyPassword"`. `npx flowfig verify docs/login.svg`
306
+ fails when the file or the symbol is gone. The action runs `verify` on every figure in a pull request. It comments the old and
307
+ 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
308
+ changes.
309
+
310
+ ```yaml
311
+ permissions:
312
+ contents: read
313
+ pull-requests: write
314
+ steps:
315
+ - uses: actions/checkout@v4
316
+ with:
317
+ fetch-depth: 0
318
+ - uses: actions/setup-node@v4
319
+ with:
320
+ node-version: 22
321
+ - uses: iamalvisng/flowfig@v0.2.0
322
+ with:
323
+ figures: 'docs/**/*.svg' # default **/*.svg
324
+ ```
325
+
326
+ ## MCP server
327
+
328
+ `npx flowfig mcp` serves the tools `docs`, `check`, `render`, `verify` and `diff` over stdio, for an agent with no shell.
329
+ `render` writes the SVG file and returns the check lines and the path, so no SVG text goes through the model.
330
+
331
+ `npx flowfig init` registers the server for Claude Code (`.mcp.json`), Cursor (`.cursor/mcp.json`), GitHub Copilot
332
+ (`.vscode/mcp.json`), Gemini CLI (`.gemini/settings.json`) and Kiro (`.kiro/settings/mcp.json`). It merges one entry into
333
+ the file and keeps every other server. `--no-mcp` skips this step. If you formatted the file with other spacing, the first
334
+ run rewrites it once. For any other client, add:
335
+
336
+ ```json
337
+ { "mcpServers": { "flowfig": { "command": "npx", "args": ["flowfig", "mcp"] } } }
338
+ ```
339
+
297
340
  ## Use in React
298
341
 
299
342
  ```tsx
@@ -348,6 +391,8 @@ writeFileSync('first.svg', toSvg(spec));
348
391
  - `check(spec, options)` returns the findings. It also takes `width` and `minText`.
349
392
  - `render(spec, options)` returns `{ svg, scene }`. The scene is the layout that the check reads. Its shape can change.
350
393
 
394
+ `flowfig/verify` is for Node only. It exports `verify`, `links`, `owners` and `parseSource`, the same checks as `flowfig verify`.
395
+
351
396
  `toSvg` does not put the spec in the SVG. Only the CLI adds the spec, which `--spec` reads back.
352
397
 
353
398
  ## Use with your coding agent
@@ -378,8 +423,9 @@ On a terminal, `init` shows a picker. The picker selects the agents that the rep
378
423
  | `--agents <ids>` | Write for these agents, for example `--agents claude,cursor`. |
379
424
  | `--all-agents` | Write for all 7 agents. |
380
425
  | `-y`, `--yes` | Write for the agents that the repo uses, with no picker. |
381
- | `--global` | Write only the Claude Code skill, to your home directory. |
426
+ | `--global` | Write the Claude skill and the MCP entry to your home folder. |
382
427
  | `--dry-run` | Print what `init` would write, and write nothing. |
428
+ | `--no-mcp` | Do not register the MCP server. |
383
429
  | `--list-agents` | Print the agent ids. |
384
430
 
385
431
  With no terminal and no `-y`, `--agents` or `--all-agents`, `init` prints the agent ids and exits with code 2.
package/dist/check.js CHANGED
@@ -2,6 +2,7 @@
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, edgeId, isGroup, nodes, str, toBeat } from './model.js';
4
4
  import { textWidth } from './text.js';
5
+ import { owners, parseSource } from './source.js';
5
6
  const err = (rule, ids, message) => ({ rule, severity: 'error', ids, message });
6
7
  const warn = (rule, ids, message) => ({ rule, severity: 'warning', ids, message });
7
8
  const groupIds = (g) => [...(g.id ? [g.id] : []), ...g.children.flatMap((c) => (isGroup(c) ? groupIds(c) : []))];
@@ -43,6 +44,10 @@ export function checkSpec(fig) {
43
44
  if (e.quiet && !used.has(edges[i]))
44
45
  out.push(err('hidden-edge', [edges[i]], `edge "${edges[i]}" is quiet and no beat uses it, so the figure never shows it`));
45
46
  });
47
+ // A link with a bad form never verifies, so say so here, where the spec is checked.
48
+ for (const [who, source] of owners(fig))
49
+ if (source != null && !parseSource(source))
50
+ out.push(warn('bad-source', [], `${who}: source "${source}" is not path or path#symbol`));
46
51
  return out;
47
52
  }
48
53
  /** A point on the cubic Bézier at `t`. */
@@ -73,7 +78,7 @@ export function checkScene(scene, { width = 830, minText = 10 } = {}) {
73
78
  const labeled = scene.edges.filter((e) => e.label);
74
79
  labeled.forEach((e, i) => {
75
80
  for (const f of labeled.slice(i + 1))
76
- if (overlap(e.label, f.label, 1))
81
+ if ((e.step == null || f.step == null || e.step === f.step) && overlap(e.label, f.label, 1))
77
82
  out.push(err('label-overlap', [e.id, f.id], `the labels of edges "${e.id}" and "${f.id}" overlap`));
78
83
  for (const b of scene.boxes)
79
84
  if (overlap(e.label, b.rect, 1))
package/dist/cli.js CHANGED
@@ -15,7 +15,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
15
15
  * flowfig figure.ts out.svg # a module whose default export is a spec
16
16
  * flowfig --spec out.svg # print back the spec the SVG carries
17
17
  * flowfig check <input> [--json] # list the faults; the input can also be an SVG this wrote
18
+ * flowfig verify <input>... [--root dir] # check that the code each figure links to still exists
19
+ * flowfig diff <old> <new> [--json|--md] # list the spec changes between two figures
18
20
  * flowfig docs # print the full guide (Markdown)
21
+ * flowfig mcp # serve check, render, verify, diff and docs over MCP (stdio)
19
22
  * flowfig init [dir] # write flowfig instructions for the coding agents of a repo
20
23
  *
21
24
  * A render checks first and writes nothing on an error (--no-check skips that). --strict makes warnings errors; --width and
@@ -28,10 +31,20 @@ import { resolve } from 'node:path';
28
31
  import { pathToFileURL } from 'node:url';
29
32
  import { GUIDE } from './guide.js';
30
33
  import { runInit } from './init.js';
31
- import { counts } from './model.js';
32
- import { check, toSvg } from './svg.js';
33
- const SPEC_OPEN = '<metadata id="figure-spec"><![CDATA[';
34
- const SPEC_CLOSE = ']]></metadata>';
34
+ import { serve } from './mcp.js';
35
+ import { diff, formatDiff } from './diff.js';
36
+ import { loadSpec, reportLines, sortFindings, specOf, svgWithSpec } from './load.js';
37
+ import { check } from './svg.js';
38
+ import { links, verify } from './verify.js';
39
+ const USAGE = `usage: flowfig <-|spec.json|figure.ts> [out.svg] render a figure; a spec on stdin with -
40
+ flowfig check <-|spec.json|figure.ts|figure.svg> list the faults; the input can be an SVG this wrote
41
+ flowfig --spec figure.svg print the spec the SVG carries
42
+ flowfig verify <input>... [--root <dir>] [--json] [--strict] check the code links of one or more figures
43
+ flowfig diff <old> <new> [--json|--md] list the spec changes between two figures
44
+ flowfig docs print the guide (Markdown)
45
+ flowfig mcp serve check, render, verify, diff and docs over MCP (stdio)
46
+ flowfig init [dir] [--agents <ids>] [-y] [--global] [--dry-run] [--no-mcp] write flowfig instructions for the coding agents of a repo
47
+ flags for render and check: --strict (warnings are errors), --json, --width <px>, --min-text <px>, --no-check (render only)`;
35
48
  /** Bad use, not a bad figure: exit 2 with a message, not a stack trace. A declaration, so TypeScript narrows after a call. */
36
49
  function usage(message) {
37
50
  console.error(message);
@@ -49,22 +62,96 @@ const value = (name) => {
49
62
  const n = Number(args.splice(i, 2)[1] || NaN);
50
63
  return n > 0 && Number.isFinite(n) ? n : usage(`${name} needs a positive number`);
51
64
  };
52
- function specOf(svg, name) {
53
- const at = svg.indexOf(SPEC_OPEN);
54
- if (at === -1)
55
- usage(`${name}: no figure spec inside this SVG`);
56
- return svg.slice(at + SPEC_OPEN.length, svg.indexOf(SPEC_CLOSE, at));
65
+ if (!args[0] || ['help', '--help', '-h'].includes(args[0])) {
66
+ if (args[0])
67
+ console.log(USAGE);
68
+ else
69
+ usage(USAGE);
70
+ process.exit(0);
57
71
  }
58
72
  if (args[0] === 'docs') {
59
73
  process.stdout.write(GUIDE);
60
74
  process.exit(0);
61
75
  }
76
+ if (args[0] === 'mcp') {
77
+ await serve(process.stdin, process.stdout);
78
+ // A pipe write is asynchronous: wait until the queued response lines are out before the exit.
79
+ await new Promise((done) => process.stdout.write('', () => done(undefined)));
80
+ process.exit(0);
81
+ }
62
82
  if (args[0] === 'init')
63
83
  process.exit(await runInit(args.slice(1)));
64
84
  if (args[0] === '--spec') {
65
85
  if (!args[1])
66
86
  usage('--spec needs the path of an SVG');
67
- console.log(specOf(readFileSync(args[1], 'utf8'), args[1]));
87
+ let svg = '';
88
+ try {
89
+ svg = readFileSync(args[1], 'utf8');
90
+ }
91
+ catch (e) {
92
+ usage(`${args[1]}: ${e.message}`);
93
+ }
94
+ try {
95
+ console.log(specOf(svg, args[1]));
96
+ }
97
+ catch (e) {
98
+ usage(e.message);
99
+ }
100
+ process.exit(0);
101
+ }
102
+ /** A spec from stdin, a JSON file, an SVG this wrote, or a module. Exit 2 with a message on any failure. */
103
+ async function load(input) {
104
+ try {
105
+ if (input.endsWith('.json') || input.endsWith('.svg'))
106
+ return loadSpec(input);
107
+ if (input === '-')
108
+ return loadSpec(JSON.parse(readFileSync(0, 'utf8')), input);
109
+ return loadSpec((await import(__rewriteRelativeImportExtension(pathToFileURL(resolve(input)).href))).default, input);
110
+ }
111
+ catch (e) {
112
+ const m = e.message;
113
+ return usage(m.startsWith(`${input}: `) ? m : `${input}: ${m}`);
114
+ }
115
+ }
116
+ if (args[0] === 'verify') {
117
+ args.shift();
118
+ const json = flag('--json'), strict = flag('--strict');
119
+ const at = args.indexOf('--root');
120
+ const root = at === -1 ? process.cwd() : (args.splice(at, 2)[1] ?? usage('--root needs a folder'));
121
+ const unknown = args.find((a) => a.startsWith('-') && a !== '-');
122
+ if (unknown)
123
+ usage(`unknown flag ${unknown}`);
124
+ if (!args.length)
125
+ usage('verify needs at least one figure');
126
+ const findings = [];
127
+ const all = [];
128
+ for (const input of args) {
129
+ const props = await load(input);
130
+ const found = sortFindings([...check(props), ...verify(props, { root })], strict).map((f) => ({ ...f, figure: input }));
131
+ findings.push(...found);
132
+ all.push(...links(props).map((l) => ({ ...l, figure: input })));
133
+ }
134
+ const errors = findings.filter((f) => f.severity === 'error').length;
135
+ if (json)
136
+ console.log(JSON.stringify({ findings, links: all }, null, 2));
137
+ else {
138
+ const n = (k, word) => `${k} ${word}${k === 1 ? '' : 's'}`;
139
+ for (const f of findings)
140
+ console.log(`${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.figure}: ${f.message}`);
141
+ console.log(`${n(errors, 'error')}, ${n(findings.length - errors, 'warning')}`);
142
+ }
143
+ process.exit(errors ? 1 : 0);
144
+ }
145
+ if (args[0] === 'diff') {
146
+ args.shift();
147
+ const json = flag('--json'), md = flag('--md');
148
+ const unknown = args.find((a) => a.startsWith('-') && a !== '-');
149
+ if (unknown)
150
+ usage(`unknown flag ${unknown}`);
151
+ if (args.length !== 2)
152
+ usage('diff needs two figures: flowfig diff <old> <new> [--json|--md]');
153
+ const changes = diff(await load(args[0]), await load(args[1]));
154
+ console.log(json ? JSON.stringify(changes, null, 2) : formatDiff(changes, md ? 'md' : 'text'));
68
155
  process.exit(0);
69
156
  }
70
157
  const command = args[0] === 'check' ? args.shift() : 'render';
@@ -75,44 +162,17 @@ if (unknown)
75
162
  usage(`unknown flag ${unknown}`);
76
163
  const [input, out] = args;
77
164
  if (!input)
78
- usage('usage: flowfig <-|spec.json|figure.ts> [out.svg] | flowfig check <-|spec.json|figure.ts|figure.svg> [--json] | flowfig docs | flowfig init [dir]');
79
- let loaded;
80
- try {
81
- loaded =
82
- input === '-'
83
- ? JSON.parse(readFileSync(0, 'utf8'))
84
- : input.endsWith('.json')
85
- ? JSON.parse(readFileSync(input, 'utf8'))
86
- : input.endsWith('.svg')
87
- ? JSON.parse(specOf(readFileSync(input, 'utf8'), input))
88
- : (await import(__rewriteRelativeImportExtension(pathToFileURL(resolve(input)).href))).default;
89
- }
90
- catch (e) {
91
- usage(`${input}: ${e.message}`);
92
- }
93
- const props = loaded?.props ?? loaded;
94
- if (!props?.layout)
95
- usage(`${input}: no figure props (expected { props: { layout, edges, steps } })`);
96
- if (!Array.isArray(props.edges))
97
- usage(`${input}: edges must be an array (use [] for none)`);
98
- if (props.steps?.some((s) => !Array.isArray(s?.flow)))
99
- usage(`${input}: each step needs a flow array`);
100
- const findings = skip
101
- ? []
102
- : check(props, opts)
103
- .map((f) => (strict ? { ...f, severity: 'error' } : f))
104
- .sort((a, b) => (a.severity === b.severity ? a.rule.localeCompare(b.rule) : a.severity === 'error' ? -1 : 1));
165
+ usage(USAGE);
166
+ const props = await load(input);
167
+ const findings = skip ? [] : sortFindings(check(props, opts), strict);
105
168
  const errors = findings.filter((f) => f.severity === 'error').length;
106
- /** The findings for a person: one line each, colored only on a terminal. A render prints them to stderr, next to its own output. */
169
+ /** The findings for a person: one line each, colored only on a terminal, then the counts. A render prints them to stderr, next to
170
+ * its own output. A clean render still prints `0 errors, 0 warnings`: the agent copies that line into its reply. */
107
171
  const report = (print, tty) => {
108
- const paint = (s, code) => (tty && !process.env.NO_COLOR ? `\x1b[${code}m${s}\x1b[0m` : s);
109
- for (const f of findings)
110
- print(`${paint(f.severity.padEnd(8), f.severity === 'error' ? 31 : 33)} ${f.rule.padEnd(18)} ${f.message}`);
111
- const n = (k, word) => `${k} ${word}${k === 1 ? '' : 's'}`;
112
- if (findings.length || print === console.log)
113
- print(`${n(errors, 'error')}, ${n(findings.length - errors, 'warning')}`);
114
- const c = counts(props);
115
- print(`figure: ${n(c.boxes, 'box').replace('boxs', 'boxes')}, ${n(c.groups, 'group')}, ${n(c.edges, 'edge')}, ${n(c.steps, 'step')}, ${n(c.messages, 'message')}`);
172
+ // The severity field is 8 characters wide, so the painted field keeps the same width.
173
+ const paint = (s) => tty && !process.env.NO_COLOR ? s.replace(/^(error {3}|warning )/, (w) => `\x1b[${w[0] === 'e' ? 31 : 33}m${w}\x1b[0m`) : s;
174
+ for (const line of reportLines(props, findings, !skip))
175
+ print(paint(line));
116
176
  };
117
177
  if (command === 'check') {
118
178
  if (json)
@@ -126,8 +186,6 @@ if (errors)
126
186
  process.exit(1);
127
187
  const dest = out ?? (input === '-' ? 'figure.svg' : input.replace(/\.[^./\\]+$/, '') + '.svg');
128
188
  // The spec rides along in <metadata>: an SVG is then its own source, and no JSON has to be kept.
129
- // `]]>` would close the CDATA early; it cannot appear in JSON-encoded text, but be sure.
130
- const spec = JSON.stringify({ props }).replaceAll(']]>', ']]\\u003e');
131
- const withSpec = toSvg(props).replace(/(<svg[^>]*>\n?)/, (tag) => `${tag}${SPEC_OPEN}${spec}${SPEC_CLOSE}\n`);
189
+ const withSpec = svgWithSpec(props);
132
190
  writeFileSync(dest, withSpec);
133
191
  console.log(`${dest} — ${(withSpec.length / 1024).toFixed(1)} kB`);
package/dist/diff.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { type FlowProps } from './model.ts';
2
+ export type Change = {
3
+ kind: 'box' | 'edge' | 'step' | 'message' | 'rail';
4
+ op: 'added' | 'removed' | 'changed';
5
+ id: string;
6
+ detail?: string;
7
+ };
8
+ export declare function diff(before: FlowProps, after: FlowProps): Change[];
9
+ export declare function formatDiff(changes: Change[], as: 'text' | 'md'): string;
package/dist/diff.js ADDED
@@ -0,0 +1,59 @@
1
+ // The spec diff: what a PR changed in a figure, by id. The SVG geometry follows from the spec, so the spec is the diff.
2
+ import { edgeId, isGroup, str, toBeat } from './model.js';
3
+ const show = (v) => (v == null ? '(none)' : `"${str(v)}"`);
4
+ /** The fields that changed, as `name "old" -> "new"` parts; empty when equal. */
5
+ const fields = (a, b, names) => names.filter((k) => str(a[k]) !== str(b[k])).map((k) => `${String(k)} ${show(a[k])} -> ${show(b[k])}`);
6
+ /** Compare two maps by key. The order is stable: changed first, then removed, then added. */
7
+ function compare(kind, before, after, fieldsOf) {
8
+ const out = [];
9
+ for (const [id, a] of before) {
10
+ const b = after.get(id);
11
+ if (!b)
12
+ continue;
13
+ const d = fieldsOf(a, b);
14
+ if (d.length)
15
+ out.push({ kind, op: 'changed', id, detail: d.join('; ') });
16
+ }
17
+ for (const id of before.keys())
18
+ if (!after.has(id))
19
+ out.push({ kind, op: 'removed', id });
20
+ for (const id of after.keys())
21
+ if (!before.has(id))
22
+ out.push({ kind, op: 'added', id });
23
+ return out;
24
+ }
25
+ const messages = (fig) => new Map((fig.steps ?? [])
26
+ .flatMap((s) => s.flow.flatMap((b) => toBeat(b).hops.map((h) => `${str(s.label)}: ${h.edge}${h.back ? ' back' : ''}${h.data != null ? ` "${str(h.data)}"` : ''}`)))
27
+ .map((id) => [id, id]));
28
+ export function diff(before, after) {
29
+ const boxes = (f) => {
30
+ const m = new Map();
31
+ // The group is its label, else its id; the top level has none.
32
+ const walk = (g, parent) => g.children.forEach((c) => (isGroup(c) ? walk(c, c.label != null ? str(c.label) : c.id) : m.set(c.id, { ...c, group: parent })));
33
+ walk(f.layout);
34
+ return m;
35
+ };
36
+ const edges = (f) => new Map(f.edges.map((e) => [edgeId(e), e]));
37
+ const steps = (f) => new Map((f.steps ?? []).map((s) => [str(s.label), s]));
38
+ const out = [
39
+ ...compare('box', boxes(before), boxes(after), (a, b) => fields(a, b, ['label', 'sub', 'shape', 'source', 'group'])),
40
+ ...compare('edge', edges(before), edges(after), (a, b) => fields(a, b, ['from', 'to', 'label', 'source'])),
41
+ ...compare('step', steps(before), steps(after), () => []),
42
+ ...compare('message', messages(before), messages(after), () => []),
43
+ ];
44
+ // The rail is a boolean or 'only', so str() would hide it.
45
+ if ((before.rail ?? false) !== (after.rail ?? false))
46
+ out.push({ kind: 'rail', op: 'changed', id: 'rail', detail: `${before.rail ?? '(none)'} -> ${after.rail ?? '(none)'}` });
47
+ return out;
48
+ }
49
+ export function formatDiff(changes, as) {
50
+ if (!changes.length)
51
+ return as === 'md' ? '- no change in the spec' : 'no change in the spec';
52
+ return changes
53
+ .map((c) => {
54
+ const id = as === 'md' ? `\`${c.id}\`` : c.id;
55
+ const line = `${c.kind} ${c.op}: ${id}${c.detail ? ` (${c.detail})` : ''}`;
56
+ return as === 'md' ? `- ${line}` : line;
57
+ })
58
+ .join('\n');
59
+ }
package/dist/guide.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** The full guide, as Markdown. `flowfig docs` prints it. */
2
- export declare const GUIDE = "# flowfig guide\n\nflowfig draws animated flow figures. A figure has boxes, the edges between the boxes, and steps. In each step, a packet moves along\nthe edges. The output is one animated SVG with no script. The SVG plays in a GitHub README, in PR and issue comments, and in blog\nposts. A figure is about 5 to 50 kB.\n\nYou write a JSON spec. You never write SVG by hand. The command `npx flowfig` checks the spec and renders it.\n\n## What flowfig draws\n\nA spec has three forms.\n\n- The map (the default). The map shows the boxes and the edges. A packet moves along an edge in each beat.\n- The map with a rail (`\"rail\": true`). The rail is a lifeline diagram under the map. It has one row for each message.\n- The rail alone (`\"rail\": \"only\"`). Use it when the reader needs only the order of the messages, for example when the map is already in the same document.\n\nUse the rail for a sequence of messages: a request lifecycle, an API call chain, or a Mermaid `sequenceDiagram`. Put each message on\nan edge. Put each phase in a step. Put messages that run at the same time in one beat. Mark a message that does not wait for an\nanswer with `\"async\": true`.\n\n## Scope: what flowfig draws and what it does not\n\nflowfig draws how something works: parts, the messages between them, and the order of the messages.\n\n| The user asks for | Draw |\n| ------------------------------------------ | ------------------------------------------------------------------------------------------ |\n| An architecture, a data flow, a pipeline | The map. Group parts by service, network or trust boundary. `store` for data at rest. |\n| A request lifecycle, a call chain | The map with `\"rail\": true`. One message per hop, with its real payload in `data`. |\n| A sequence diagram only | `\"rail\": \"only\"`. |\n| A state lifecycle, a state machine | One box per state, one edge per transition, labeled with its event. One step per path. Mark each end state, for example with `sub: \"end state\"`. |\n| A flowchart with branches | `shape: \"decision\"` for each branch. One step per path. |\n\nflowfig does not draw class or ER diagrams (fields, types, cardinality), Gantt charts, timelines with dates, charts of numbers,\nor mind maps. If the user asks for one of these, say that flowfig does not draw it, suggest Mermaid (`classDiagram`,\n`erDiagram`, `gantt`), and draw nothing with flowfig.\n\n## Workflow\n\n### 1. Pick the scope\n\nIf the code has more than one flow, name each flow in the reply. Then draw one flow, or draw one structure view and call it a\nstructure view. If the user asks for the whole system, draw one top view with groups, or a set of figures that together cover\nevery part. Keep each figure to 12 boxes or fewer.\n\n### 2. Write the fact list\n\nBefore the spec, write a list of the facts that the figure must show. Take each fact from the code and from its wiring: config\nfiles, compose files, route tables and queue bindings (or from the pasted Mermaid). Give each fact its `file:line`:\n\n- every part (service, function, store, queue, external system);\n- every call, in order, written as `caller \u2192 callee: payload (file:line of the call)`. The edge in the spec goes from that caller\n to that callee;\n- every call that does not wait for an answer (fire and forget, a queue, a webhook, a callback), and every consumer of each queue\n or topic;\n- every branch and every error path (a rejected input, a retry, a dead-letter path);\n- every end state.\n\nIf the code does not show a fact, leave the fact out. Do not guess a part, a name or an order.\n\n### 3. Write the spec\n\nEach fact becomes a box, an edge, a beat, a `data` card or a `show` row. Use the real names from the code as labels, and\nreal example data from the code or its tests. Keep each fact that you leave out, with the reason, for the reply.\n\nMermaid mapping:\n\n| Mermaid | flowfig |\n| ---------------------------------------- | ------------------------------------------------------------------ |\n| `participant` | a box |\n| `A->>B: text` | an edge from A to B, and a hop with `data: \"text\"` |\n| `B-->>A: text` (a reply) | a hop on the same edge with `back: true` |\n| `A-)B: text` (async) | a hop with `async: true` |\n| `par ... and ... end` | the hops of all branches in one beat |\n| `alt` / `opt` | one step per path |\n| `stateDiagram` states and transitions | boxes, and edges labeled with the event |\n\n### 4. Render, then fix\n\n```bash\nnpx flowfig - out.svg <<'SPEC'\n{ \"props\": { \"layout\": ..., \"edges\": [...], \"steps\": [...] } }\nSPEC\n```\n\nThe quoted `<<'SPEC'` heredoc passes the JSON with no change. The command checks the spec first. If the check finds an error, the\ncommand writes no SVG and lists the faults. Fix each fault and render again.\n\n- Fix a layout fault by changing the layout first: put parts in rows or columns, route an edge `around`, or change a gap.\n Never remove a fact from the fact list to pass the check. If you must remove one, name it in the reply.\n- If `small-text` appears, the figure is too wide. Shorten labels, or split the figure. Use more steps in one figure before\n you make a second figure. Do not change `--width` to pass the check: the reader sees the figure at the real page width.\n- Fix every other warning, or tell the user why it stays.\n- Do not use `--no-check` to hide a fault.\n\n### 5. Read the figure back\n\nRun `npx flowfig --spec out.svg`. Compare the spec with the fact list. Check that each edge goes from the caller to the callee\nthat its fact names. Add each fact that is missing, fix each wrong edge, and render again.\n\n### 6. Look at the SVG\n\nThe check estimates the text width, so it cannot see everything. If you can open a browser, open the SVG and take a\nscreenshot. Wait a few seconds and take a second screenshot to see a later beat. If you cannot open a browser, write \"not\nlooked at\" in the reply. Never report a look that you did not do. A browser tool can block `file://` URLs. If it does, serve the folder\nwith `python3 -m http.server`.\n\n### 7. Write the reply\n\nThe reply has these parts, in this order:\n\n1. the path of each SVG;\n2. what each figure shows: the parts and the steps. Copy the counts from the `figure:` line of the check output. Copy every name\n from the read-back spec. Never count from memory;\n3. the scope: the flow you drew, why, and the other flows that exist;\n4. what the figure leaves out, and why;\n5. the check line, exactly as the command printed it, and whether you looked at the SVG.\n\nTo change an SVG later, print its spec with `--spec`, change the spec, and render again.\n\n## Spec reference\n\n`props` has these fields.\n\n| Field | Type | Meaning |\n| ---------- | ----------------------- | ---------------------------------------------------------------------------------------------- |\n| `layout` | group | The boxes and how they line up. Required. |\n| `edges` | edge[] | The arrows between the boxes. Required. |\n| `steps` | step[] | The stories the figure tells. With no steps, the figure is a still map. |\n| `rail` | `true` or `\"only\"` | Draw the steps as a lifeline rail under the map. `\"only\"` draws the rail without the map. |\n| `speed` | number | Milliseconds a packet takes to cross one edge. Default: 900. |\n| `theme` | object | Colors: `accent`, `fg`, `muted`, `bg`, `surface`, `border`, `font`. Each color you omit keeps its built-in value. |\n\n### layout\n\nThe layout is a tree. A group has `children` and these optional fields.\n\n- `id`: a name that an edge can use to reach the group as a whole.\n- `label`: the title on the frame. Only a group with a `label` gets a frame.\n- `direction`: `\"row\"` (the default) or `\"column\"`.\n- `gap`: the space between children in px. Default: 28 for a column. For a row, the gap fits the widest edge label between two children (at least 56).\n- `align`: `\"start\"`, `\"center\"` or `\"end\"`. This sets where the children line up across the direction.\n\nEvery other item is a box.\n\n| Box field | Meaning |\n| --------- | --------------------------------------------------------------------------------------------------------- |\n| `id` | Required. The name that edges and steps use. It must be unique in the figure. |\n| `label` | Required. The title in the box. |\n| `sub` | A smaller line under the label. |\n| `shape` | `\"box\"` (default), `\"decision\"` (a diamond) or `\"store\"` (a database cylinder for data at rest). |\n| `width` | Width in px. This overrides the width that the layout picks. |\n| `lines` | The least number of text lines that a content card keeps. The card still grows to fit its content. |\n\n### edges\n\nAn edge has these fields.\n\n- `id`: the name that steps use. Default: `from->to`.\n- `from` and `to`: the `id` of a box or a group.\n- `label`: text on the edge, drawn as a small pill.\n- `around`: `\"above\"` or `\"below\"`. This routes the edge over or under the boxes in between (loops, skip-ahead edges).\n- `quiet`: if `true`, the SVG draws the edge only while a step uses it.\n\n### steps\n\nA step has `label` and `flow`. It can also have `caption` (the line under the figure while no beat has a `say`) and `nodes` (ids of\nboxes to highlight for the whole step).\n\n`flow` is a list of beats. A beat is one of these:\n\n- an edge id (a string);\n- an array of edge ids, which run at the same time;\n- an object `{ edges?, say?, show?, light?, ms? }`. A beat with no `edges` is a pause.\n\nThe beat fields:\n\n- `edges`: an edge id, a hop object, or an array of these. A hop object is `{ edge, back?, data?, async? }`.\n - `back: true` runs the packet from the `to` box to the `from` box.\n - `data` is a small card that moves with the packet.\n - `async: true` marks a message that does not wait for an answer. The rail draws it dashed, with an `async` tag.\n- `say`: the caption for the beat.\n- `show`: `{ boxId: [row, ...] }`. This fills the content card of each named box. The card keeps the rows until the step ends.\n- `light`: ids of boxes to highlight for this beat.\n- `ms`: how long the beat lasts. Default: the time one packet needs to cross an edge.\n\nA row of a content card is `{ tag?, tone?, text, meta?, mark?, mono? }`.\n\n- `tag`: a short colored label at the start of the line.\n- `tone`: the color of the tag: `blue` (default), `purple`, `green`, `orange` or `gray`.\n- `text`: the main text.\n- `meta`: muted text after the main text.\n- `mark`: a mark at the right end of the line, such as a check or \"new\".\n- `mono`: if `true`, the text uses a monospace font.\n\n## Content rules\n\n- Give every box a stable `id`.\n- Use `shape: \"store\"` for data at rest (a database, a cache, a file, a queue). Use a plain box for a part that does work. Use `shape: \"decision\"` for a branch.\n- Keep one clear main path, left to right. Put side parts in a `column` group.\n- Use `quiet: true` on a long edge that crosses the picture.\n- Keep an SVG to one or two steps. The SVG plays every step in a loop, with no controls.\n- Use the React player, `<Flow {...props} />` from `flowfig`, for a page that needs tabs, pause or hover.\n\n## Example\n\n```json\n{\n \"props\": {\n \"layout\": {\n \"children\": [\n { \"id\": \"client\", \"label\": \"Browser\" },\n { \"id\": \"api\", \"label\": \"API Server\" },\n { \"id\": \"db\", \"label\": \"Database\", \"shape\": \"store\" }\n ]\n },\n \"edges\": [\n { \"id\": \"req\", \"from\": \"client\", \"to\": \"api\", \"label\": \"GET /users/42\" },\n { \"id\": \"q\", \"from\": \"api\", \"to\": \"db\", \"label\": \"SELECT\" }\n ],\n \"steps\": [\n {\n \"label\": \"read\",\n \"flow\": [\n { \"edges\": { \"edge\": \"req\", \"data\": \"GET /users/42\" }, \"say\": \"The browser asks for user 42.\" },\n { \"edges\": \"q\", \"show\": { \"db\": [{ \"tag\": \"row\", \"tone\": \"gray\", \"text\": \"Ada Lovelace\" }] }, \"say\": \"The API server reads the row.\" },\n { \"edges\": { \"edge\": \"req\", \"back\": true, \"data\": \"200 OK\" }, \"say\": \"The API server sends the user.\" }\n ]\n }\n ]\n }\n}\n```\n\n## Commands\n\n```bash\nnpx flowfig - out.svg < spec.json # render a spec from stdin\nnpx flowfig spec.json out.svg # render a spec file\nnpx flowfig figure.ts out.svg # render a module whose default export is a spec\nnpx flowfig check <input> [--json] # list the faults. The input can also be an SVG that flowfig wrote.\nnpx flowfig --spec out.svg # print the spec that an SVG carries\nnpx flowfig docs # print this guide\nnpx flowfig init # write these instructions for the coding agents of a repo\n```\n\nA render checks first and writes nothing on an error. `--strict` makes warnings errors. `--width` sets the page width.\n`--min-text` sets the smallest text that the reader should get. `--no-check` skips the check.\n";
2
+ export declare const GUIDE = "# flowfig guide\n\nflowfig draws animated flow figures. A figure has boxes, the edges between the boxes, and steps. In each step, a packet moves along\nthe edges. The output is one animated SVG with no script. The SVG plays in a GitHub README, in PR and issue comments, and in blog\nposts. A figure is about 5 to 50 kB.\n\nYou write a JSON spec. You never write SVG by hand. The command `npx flowfig` checks the spec and renders it.\n\n## What flowfig draws\n\nA spec has three forms.\n\n- The map (the default). The map shows the boxes and the edges. A packet moves along an edge in each beat.\n- The map with a rail (`\"rail\": true`). The rail is a lifeline diagram under the map. It has one row for each message.\n- The rail alone (`\"rail\": \"only\"`). Use it when the reader needs only the order of the messages, for example when the map is already in the same document.\n\nUse the rail for a sequence of messages: a request lifecycle, an API call chain, or a Mermaid `sequenceDiagram`. Put each message on\nan edge. Put each phase in a step. Put messages that run at the same time in one beat. Mark a message that does not wait for an\nanswer with `\"async\": true`.\n\n## Scope: what flowfig draws and what it does not\n\nflowfig draws how something works: parts, the messages between them, and the order of the messages.\n\n| The user asks for | Draw |\n| ------------------------------------------ | ------------------------------------------------------------------------------------------ |\n| An architecture, a data flow, a pipeline | The map. Group parts by service, network or trust boundary. `store` for data at rest. |\n| A request lifecycle, a call chain | The map with `\"rail\": true`. One message per hop, with its real payload in `data`. |\n| A sequence diagram only | `\"rail\": \"only\"`. |\n| A state lifecycle, a state machine | One box per state, one edge per transition, labeled with its event. One step per path. Mark each end state, for example with `sub: \"end state\"`. |\n| A flowchart with branches | `shape: \"decision\"` for each branch. One step per path. |\n\nflowfig does not draw class or ER diagrams (fields, types, cardinality), Gantt charts, timelines with dates, charts of numbers,\nor mind maps. If the user asks for one of these, say that flowfig does not draw it, suggest Mermaid (`classDiagram`,\n`erDiagram`, `gantt`), and draw nothing with flowfig.\n\n## Workflow\n\n### 1. Pick the scope\n\nIf the code has more than one flow, name each flow in the reply. Then draw one flow, or draw one structure view and call it a\nstructure view. If the user asks for the whole system, draw one top view with groups, or a set of figures that together cover\nevery part. Keep each figure to 12 boxes or fewer.\n\n### 2. Write the fact list\n\nBefore the spec, write a list of the facts that the figure must show. Take each fact from the code and from its wiring: config\nfiles, compose files, route tables and queue bindings (or from the pasted Mermaid). Give each fact its `file:line`:\n\n- every part (service, function, store, queue, external system);\n- every call, in order, written as `caller \u2192 callee: payload (file:line of the call)`. The edge in the spec goes from that caller\n to that callee;\n- every call that does not wait for an answer (fire and forget, a queue, a webhook, a callback), and every consumer of each queue\n or topic;\n- every branch and every error path (a rejected input, a retry, a dead-letter path);\n- every end state.\n\nIf the code does not show a fact, leave the fact out. Do not guess a part, a name or an order.\n\n### 3. Write the spec\n\nEach fact becomes a box, an edge, a beat, a `data` card or a `show` row. Use the real names from the code as labels, and\nreal example data from the code or its tests. Keep each fact that you leave out, with the reason, for the reply.\nGive each box and edge that draws code a `source`, from the fact list: `\"src/auth/login.ts#verifyPassword\"`. A store or an outside part may have none.\n\nMermaid mapping:\n\n| Mermaid | flowfig |\n| ---------------------------------------- | ------------------------------------------------------------------ |\n| `participant` | a box |\n| `A->>B: text` | an edge from A to B, and a hop with `data: \"text\"` |\n| `B-->>A: text` (a reply) | a hop on the same edge with `back: true` |\n| `A-)B: text` (async) | a hop with `async: true` |\n| `par ... and ... end` | the hops of all branches in one beat |\n| `alt` / `opt` | one step per path |\n| `stateDiagram` states and transitions | boxes, and edges labeled with the event |\n\n### 4. Render, then fix\n\n```bash\nnpx flowfig - out.svg <<'SPEC'\n{ \"props\": { \"layout\": ..., \"edges\": [...], \"steps\": [...] } }\nSPEC\n```\n\nThe quoted `<<'SPEC'` heredoc passes the JSON with no change. The command checks the spec first. If the check finds an error, the\ncommand writes no SVG and lists the faults. Fix each fault and render again.\n\n- Fix a layout fault by changing the layout first: put parts in rows or columns, route an edge `around`, or change a gap.\n Never remove a fact from the fact list to pass the check. If you must remove one, name it in the reply.\n- If `small-text` appears, the figure is too wide. Put the parts in two rows, move a detail from `label` to `sub`, or split\n the figure. Use more steps in one figure before you make a second figure. Keep the real name from the code in the label:\n `ReportsController` stays `ReportsController`, not `Controller`. Do not change `--width` to pass the check: the\n reader sees the figure at the real page width.\n- Fix every other warning, or tell the user why it stays.\n- Do not use `--no-check` to hide a fault.\n\n### 5. Read the figure back\n\nRun `npx flowfig --spec out.svg`. Compare the spec with the fact list. Check that each edge goes from the caller to the callee\nthat its fact names. Add each fact that is missing, fix each wrong edge, and render again.\n\nRun `npx flowfig verify out.svg` from the repo root. Fix each `missing-file` or `missing-symbol`.\n\n### 6. Look at the SVG\n\nThe check estimates the text width, so it cannot see everything. If you can open a browser, open the SVG and take a\nscreenshot. Wait a few seconds and take a second screenshot to see a later beat. If you cannot open a browser, write \"not\nlooked at\" in the reply. Never report a look that you did not do. A browser tool can block `file://` URLs. If it does, serve the folder\nwith `python3 -m http.server`.\n\n### 7. Write the reply\n\nThe reply has these parts, in this order:\n\n1. the path of each SVG;\n2. what each figure shows: the parts and the steps. Copy the counts from the `figure:` line of the check output. Copy every name\n from the read-back spec. Never count from memory;\n3. the scope: the flow you drew, why, and the other flows that exist;\n4. what the figure leaves out, and why;\n5. the two check lines that the render printed, `0 errors, 0 warnings` and `figure: ...`, copied as printed, and whether\n you looked at the SVG.\n\nTo change an SVG later, print its spec with `--spec`, change the spec, and render again.\n\n## Spec reference\n\n`props` has these fields.\n\n| Field | Type | Meaning |\n| ---------- | ----------------------- | ---------------------------------------------------------------------------------------------- |\n| `layout` | group | The boxes and how they line up. Required. |\n| `edges` | edge[] | The arrows between the boxes. Required. |\n| `steps` | step[] | The stories the figure tells. With no steps, the figure is a still map. |\n| `rail` | `true` or `\"only\"` | Draw the steps as a lifeline rail under the map. `\"only\"` draws the rail without the map. |\n| `speed` | number | Milliseconds a packet takes to cross one edge. Default: 900. |\n| `theme` | object | Colors: `accent`, `fg`, `muted`, `bg`, `surface`, `border`, `font`. Each color you omit keeps its built-in value. |\n\n### layout\n\nThe layout is a tree. A group has `children` and these optional fields.\n\n- `id`: a name that an edge can use to reach the group as a whole.\n- `label`: the title on the frame. Only a group with a `label` gets a frame.\n- `direction`: `\"row\"` (the default) or `\"column\"`.\n- `gap`: the space between children in px. Default: 28 for a column. For a row, the gap fits the widest edge label between two children (at least 56).\n- `align`: `\"start\"`, `\"center\"` or `\"end\"`. This sets where the children line up across the direction.\n\nEvery other item is a box.\n\n| Box field | Meaning |\n| --------- | --------------------------------------------------------------------------------------------------------- |\n| `id` | Required. The name that edges and steps use. It must be unique in the figure. |\n| `label` | Required. The title in the box. |\n| `sub` | A smaller line under the label. |\n| `shape` | `\"box\"` (default), `\"decision\"` (a diamond) or `\"store\"` (a database cylinder for data at rest). |\n| `width` | Width in px. This overrides the width that the layout picks. |\n| `source` | The code this draws: `path` or `path#symbol`, relative to the repo root. `flowfig verify` checks it. |\n| `lines` | The least number of text lines that a content card keeps. The card still grows to fit its content. |\n\n### edges\n\nAn edge has these fields.\n\n- `id`: the name that steps use. Default: `from->to`.\n- `from` and `to`: the `id` of a box or a group.\n- `label`: text on the edge, drawn as a small pill.\n- `around`: `\"above\"` or `\"below\"`. This routes the edge over or under the boxes in between (loops, skip-ahead edges).\n- `quiet`: if `true`, the SVG draws the edge only while a step uses it.\n- `source`: the code this edge draws, as `path` or `path#symbol`. `flowfig verify` checks it.\n\n### steps\n\nA step has `label` and `flow`. It can also have `caption` (the line under the figure while no beat has a `say`) and `nodes` (ids of\nboxes to highlight for the whole step).\n\n`flow` is a list of beats. A beat is one of these:\n\n- an edge id (a string);\n- an array of edge ids, which run at the same time;\n- an object `{ edges?, say?, show?, light?, ms? }`. A beat with no `edges` is a pause.\n\nThe beat fields:\n\n- `edges`: an edge id, a hop object, or an array of these. A hop object is `{ edge, back?, data?, async?, source? }`.\n - `back: true` runs the packet from the `to` box to the `from` box.\n - `data` is a small card that moves with the packet.\n - `async: true` marks a message that does not wait for an answer. The rail draws it dashed, with an `async` tag.\n- `say`: the caption for the beat.\n- `show`: `{ boxId: [row, ...] }`. This fills the content card of each named box. The card keeps the rows until the step ends.\n- `light`: ids of boxes to highlight for this beat.\n- `ms`: how long the beat lasts. Default: the time one packet needs to cross an edge.\n\nA row of a content card is `{ tag?, tone?, text, meta?, mark?, mono? }`.\n\n- `tag`: a short colored label at the start of the line.\n- `tone`: the color of the tag: `blue` (default), `purple`, `green`, `orange` or `gray`.\n- `text`: the main text.\n- `meta`: muted text after the main text.\n- `mark`: a mark at the right end of the line, such as a check or \"new\".\n- `mono`: if `true`, the text uses a monospace font.\n\n## Content rules\n\n- Give every box a stable `id`.\n- Use `shape: \"store\"` for data at rest (a database, a cache, a file, a queue). Use a plain box for a part that does work. Use `shape: \"decision\"` for a branch.\n- Keep one clear main path, left to right. Put side parts in a `column` group.\n- Use `quiet: true` on a long edge that crosses the picture. A quiet edge needs a beat that uses it, or the check reports\n `hidden-edge`.\n- In a rail, draw an error reply as a hop back on the same edge, with the error in `data` (`{ \"edge\": \"login\", \"back\": true,\n \"data\": \"401 CREDENTIALS_INVALID\" }`). A call that stays inside one part (a hash check, a guard) is not a message: put it in\n `say` or in a `show` row of that part.\n- Keep an SVG to one or two steps. The SVG plays every step in a loop, with no controls.\n- Use the React player, `<Flow {...props} />` from `flowfig`, for a page that needs tabs, pause or hover.\n\n## Example\n\n```json\n{\n \"props\": {\n \"layout\": {\n \"children\": [\n { \"id\": \"client\", \"label\": \"Browser\" },\n { \"id\": \"api\", \"label\": \"API Server\" },\n { \"id\": \"db\", \"label\": \"Database\", \"shape\": \"store\" }\n ]\n },\n \"edges\": [\n { \"id\": \"req\", \"from\": \"client\", \"to\": \"api\", \"label\": \"GET /users/42\" },\n { \"id\": \"q\", \"from\": \"api\", \"to\": \"db\", \"label\": \"SELECT\" }\n ],\n \"steps\": [\n {\n \"label\": \"read\",\n \"flow\": [\n { \"edges\": { \"edge\": \"req\", \"data\": \"GET /users/42\" }, \"say\": \"The browser asks for user 42.\" },\n { \"edges\": \"q\", \"show\": { \"db\": [{ \"tag\": \"row\", \"tone\": \"gray\", \"text\": \"Ada Lovelace\" }] }, \"say\": \"The API server reads the row.\" },\n { \"edges\": { \"edge\": \"req\", \"back\": true, \"data\": \"200 OK\" }, \"say\": \"The API server sends the user.\" }\n ]\n }\n ]\n }\n}\n```\n\n## Commands\n\n```bash\nnpx flowfig - out.svg < spec.json # render a spec from stdin\nnpx flowfig spec.json out.svg # render a spec file\nnpx flowfig figure.ts out.svg # render a module whose default export is a spec\nnpx flowfig check <input> [--json] # list the faults. The input can also be an SVG that flowfig wrote.\nnpx flowfig verify <input>... # check that the code each source names still exists\nnpx flowfig diff <old> <new> # list what changed in the spec between two figures\nnpx flowfig --spec out.svg # print the spec that an SVG carries\nnpx flowfig docs # print this guide\nnpx flowfig init # write these instructions for the coding agents of a repo\n```\n\nA render checks first and writes nothing on an error. `--strict` makes warnings errors. `--width` sets the page width.\n`--min-text` sets the smallest text that the reader should get. `--no-check` skips the check.\n";
3
3
  /** The short instruction text that `flowfig init` writes for each coding agent. */
4
- export declare const AGENT_TEXT = "# Diagrams with flowfig\n\nUse flowfig when the user wants a diagram of how something works: an architecture, a data flow or pipeline, a request lifecycle,\na call sequence, a state lifecycle, or a flowchart. flowfig also converts a pasted Mermaid flowchart, `stateDiagram` or\n`sequenceDiagram`.\n\nflowfig does not draw class or ER diagrams, Gantt charts or charts of numbers. For those, tell the user so, suggest Mermaid\n(`classDiagram`, `erDiagram`, `gantt`), and draw nothing with flowfig.\n\n1. Run `npx flowfig docs` and read it. If the command fails, stop: tell the user that flowfig is not available, give them\n `npm install --save-dev flowfig`, and draw nothing with another tool.\n2. Pick the scope. If the code has more than one flow, name each flow in the reply, then draw one flow, or draw one structure\n view and call it a structure view. If the user asks for the whole system, draw one top view with groups, or a set of figures\n that together cover every part. Keep each figure to 12 boxes or fewer.\n3. Write a fact list from the code and from its wiring (config, compose, route and queue files). Write each call as\n `caller \u2192 callee: payload (file:line of the call)`, in order. Include each async call and every consumer of a queue, each\n branch and error path, and each end state. Leave out what the code does not show.\n4. Write the spec from the fact list, with the real names and data. Render it with `npx flowfig - out.svg`. Fix every fault by\n changing the layout first (rows, `around`, the gap). Never remove a fact to pass the check; if you must remove one, name it\n in the reply. If `small-text` appears, split the figure; do not change `--width`.\n5. Run `npx flowfig --spec out.svg` and compare it with the fact list: each edge must go from the caller to the callee that the\n fact names. Fix each gap.\n6. If you can open a browser, look at the SVG at two moments of the loop. If you cannot, write \"not looked at\" in the reply.\n Never report a look that you did not do.\n7. Reply with: the SVG path; what it shows, with the counts copied from the `figure:` line and the names copied from the read-back spec; what it leaves out and why;\n the check line exactly as the command printed it.\n";
4
+ export declare const AGENT_TEXT = "# Diagrams with flowfig\n\nUse flowfig when the user wants a diagram of how something works: an architecture, a data flow or pipeline, a request lifecycle,\na call sequence, a state lifecycle, or a flowchart. flowfig also converts a pasted Mermaid flowchart, `stateDiagram` or\n`sequenceDiagram`.\n\nflowfig does not draw class or ER diagrams, Gantt charts or charts of numbers. For those, tell the user so, suggest Mermaid\n(`classDiagram`, `erDiagram`, `gantt`), and draw nothing with flowfig.\n\n1. Run `npx flowfig docs` and read it. If the command fails, stop: tell the user that flowfig is not available, give them\n `npm install --save-dev flowfig`, and draw nothing with another tool.\n2. Pick the scope. If the code has more than one flow, name each flow in the reply, then draw one flow, or draw one structure\n view and call it a structure view. If the user asks for the whole system, draw one top view with groups, or a set of figures\n that together cover every part. Keep each figure to 12 boxes or fewer.\n3. Write a fact list from the code and from its wiring (config, compose, route and queue files). Write each call as\n `caller \u2192 callee: payload (file:line of the call)`, in order. Include each async call and every consumer of a queue, each\n branch and error path, and each end state. Leave out what the code does not show.\n4. Write the spec from the fact list, with the real names and data. Render it with `npx flowfig - out.svg <<'SPEC'` and the\n JSON in the heredoc. Fix every fault by changing the layout first (rows, `around`, the gap). Never remove a fact to pass\n the check; if you must remove one, name it in the reply. If `small-text` appears, use two rows or split the figure; keep\n the real names; do not change `--width`. Give each box and edge that draws code a `source` (`file#symbol`) from the fact list.\n5. Run `npx flowfig --spec out.svg` and compare it with the fact list: each edge must go from the caller to the callee that the\n fact names. Fix each gap. Then run `npx flowfig verify out.svg` from the repo root and fix each fault.\n6. If you can open a browser, look at the SVG at two moments of the loop. If you cannot, write \"not looked at\" in the reply.\n Never report a look that you did not do.\n7. Reply with: the SVG path; what it shows, with the counts copied from the `figure:` line and the names copied from the read-back spec; what it leaves out and why;\n the two lines the render printed, `0 errors, 0 warnings` and `figure: ...`, copied as printed.\n";
5
5
  /** The description of the Claude skill: the words that make the agent load it. */
6
6
  export declare const SKILL_DESCRIPTION = "Use when the user asks for a diagram, figure or animation of how code or a system works: an architecture, a data flow, a pipeline, a request lifecycle, a call sequence, a state lifecycle or a flowchart; when the user pastes a Mermaid flowchart, stateDiagram or sequenceDiagram to convert; or when the user asks for a class, ER or Gantt diagram.";