flowfig 0.7.0 → 0.8.1

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
@@ -22,7 +22,7 @@ A diagram in a repo drifts from the code. Someone renames a function, and the di
22
22
  check fails, so no one sees the drift.
23
23
 
24
24
  flowfig links each part of a diagram to the code that it draws. `flowfig verify` fails when a linked file or symbol is
25
- gone. Run `verify` in CI, and a stale diagram fails the pull request.
25
+ gone, and warns when the code does not make an edge. Run `verify` in CI, and a stale diagram fails the pull request.
26
26
 
27
27
  ## Fail CI when the diagram drifts
28
28
 
@@ -36,12 +36,14 @@ Rename `verifyPassword` to `checkPassword`. Then `verify` fails with exit code 1
36
36
 
37
37
  ```console
38
38
  $ npx flowfig verify login.svg
39
- error missing-symbol login.svg: box "verify" -> src/auth/login.ts#verifyPassword: symbol not found
39
+ error missing-symbol login.svg: box "verify" -> src/auth/login.ts#verifyPassword: symbol not defined
40
40
  1 error, 0 warnings
41
+ login.svg: 0 of 1 boxes defined
41
42
  ```
42
43
 
43
- `verify` checks that each linked file exists and that the symbol name still occurs in it. A link to a Markdown file can
44
- name a heading, so a process diagram can follow its SOP document.
44
+ `verify` checks that each linked file exists and that it defines the symbol. It also checks each edge against the code
45
+ (see [Verify in CI](#verify-in-ci)). A link to a Markdown file can name a heading, so a process diagram can follow its SOP
46
+ document.
45
47
 
46
48
  The GitHub Action runs `verify` on every figure in a pull request. For each SVG that the PR changes, it comments the old
47
49
  image, the new image and the spec changes. It also names each figure whose linked code the PR changes.
@@ -63,7 +65,7 @@ jobs:
63
65
  - uses: actions/setup-node@v4
64
66
  with:
65
67
  node-version: 22
66
- - uses: iamalvisng/flowfig@v0.7.0
68
+ - uses: iamalvisng/flowfig@v0.8.1
67
69
  with:
68
70
  figures: 'docs/**/*.svg' # default **/*.svg
69
71
  ```
@@ -115,7 +117,7 @@ the run.
115
117
 
116
118
  1. **The agent writes JSON, not pictures.** The agent reads the code and writes a spec of the parts, the calls and their
117
119
  order. flowfig does the layout.
118
- 2. **flowfig checks the spec.** `flowfig check` has 22 rules. It finds ids that point nowhere, text wider than its box,
120
+ 2. **flowfig checks the spec.** `flowfig check` has 23 rules. It finds ids that point nowhere, text wider than its box,
119
121
  edges through boxes, overlapping labels and low contrast. A render runs the same check and writes nothing on an error.
120
122
  The agent reads the faults and fixes the spec.
121
123
  3. **The output is one animated SVG with no script.** The SVG plays in a GitHub README, PR or issue. It follows the light
@@ -165,7 +167,7 @@ The options are in [Open and share a figure](#open-and-share-a-figure).
165
167
  | ------------------------------ | --------------------------------- | ------------------------------------ | ---------------- |
166
168
  | Made by an agent from the code | Yes, with the `init` instructions | Yes, as Mermaid text | No |
167
169
  | Linked to the code | Yes, `verify` fails in CI | No | No |
168
- | Checked for faults | Yes, 22 rules in `flowfig check` | Syntax errors only | No |
170
+ | Checked for faults | Yes, 23 rules in `flowfig check` | Syntax errors only | No |
169
171
  | Animated | Yes, one packet per message | No | No |
170
172
  | Shows payloads and order | Yes, on the map and the rail | Order and text in a sequence diagram | No |
171
173
  | Plays in a GitHub README | Yes, as an SVG | Yes, GitHub renders it | Yes, as an image |
@@ -213,7 +215,7 @@ Save this spec as `first.json`. The spec has 3 boxes, 2 edges and 1 step.
213
215
  },
214
216
  "edges": [
215
217
  { "from": "browser", "to": "api", "label": "GET /user" },
216
- { "from": "api", "to": "db", "label": "SELECT" }
218
+ { "from": "api", "to": "db", "label": "read the user" }
217
219
  ],
218
220
  "steps": [
219
221
  {
@@ -233,10 +235,18 @@ Render the spec:
233
235
 
234
236
  ```console
235
237
  $ npx flowfig first.json
238
+ 0 errors, 0 warnings
236
239
  figure: 3 boxes, 0 groups, 2 edges, 1 step, 4 messages
237
- first.svg — 8.8 kB
240
+ browser -> api: GET /user
241
+ api -> db: read the user
242
+ step "Load a user": 4 hops
243
+ first.svg — 9.7 kB
238
244
  ```
239
245
 
246
+ The render prints the check result, the counts, one line per edge and one line per step. Read these lines to check the figure
247
+ without a second command. If a box or an edge has a `source` or a `via`, the render also prints the `verify` counts.
248
+ `--no-verify` skips them.
249
+
240
250
  The command writes `first.svg`:
241
251
 
242
252
  ![A browser loads a user through an API from a database](docs/first.svg)
@@ -252,7 +262,7 @@ Each SVG from the CLI carries its own spec. These commands made the rail-only fi
252
262
  npx flowfig --spec docs/checkout.svg > checkout.json # print the spec in the SVG
253
263
  # edit checkout.json: set "props.rail" to "only"
254
264
  npx flowfig checkout.json docs/checkout-rail-only.svg
255
- npx flowfig verify docs/checkout.svg # check that each source still exists
265
+ npx flowfig verify docs/checkout.svg # check each source and each edge against the code
256
266
  npx flowfig diff old.svg docs/checkout.svg # list what changed in the spec
257
267
  ```
258
268
 
@@ -281,14 +291,14 @@ full reference, run `npx flowfig docs`.
281
291
 
282
292
  `layout` is a group. A group holds boxes and other groups.
283
293
 
284
- | Field | Meaning | Default |
285
- | ----------- | --------------------------------------------------------------- | --------------------------------------------------- |
286
- | `children` | The boxes and groups in the group, in order. | required |
287
- | `id` | The name that an edge can use to reach the whole group. | none |
288
- | `label` | The title of the frame. Only a group with a label has a frame. | none |
289
- | `direction` | `"row"` puts the children side by side. `"column"` stacks them. | `"row"` |
290
- | `gap` | The space between the children in px. | column: 28; row: fits the widest label, at least 56 |
291
- | `align` | `"start"`, `"center"` or `"end"`, across the direction. | `"center"` (a column stretches) |
294
+ | Field | Meaning | Default |
295
+ | ----------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
296
+ | `children` | The boxes and groups in the group, in order. | required |
297
+ | `id` | The name that an edge can use to reach the whole group. | none |
298
+ | `label` | The title of the frame. Only a group with a label has a frame. | none |
299
+ | `direction` | `"row"` puts the children side by side. `"column"` stacks them. | `"row"` |
300
+ | `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 |
301
+ | `align` | `"start"`, `"center"` or `"end"`, across the direction. | `"center"` (a column stretches) |
292
302
 
293
303
  ### Edges
294
304
 
@@ -358,7 +368,7 @@ figure: 5 boxes, 3 groups, 4 edges, 2 steps, 6 messages
358
368
 
359
369
  The last line gives the counts of the parts of the figure. Compare the counts with the parts that you planned.
360
370
 
361
- `flowfig check` has 22 rules: 11 errors and 11 warnings.
371
+ `flowfig check` has 23 rules: 11 errors and 12 warnings.
362
372
 
363
373
  | Rule | Severity | What it finds |
364
374
  | --------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
@@ -384,6 +394,7 @@ The last line gives the counts of the parts of the figure. Compare the counts wi
384
394
  | `timeline-and-lanes` | warning | The figure sets `timeline` and `lanes`. The renderers draw the timeline and ignore `lanes`. |
385
395
  | `font-estimated` | warning | `theme.font` is set. The SVG check estimates text width for the system font. |
386
396
  | `color-not-checked` | warning | A color that the check cannot read, so its contrast is not checked. |
397
+ | `plain-text` | warning | Text with a code name, a filler word, or a `say` or caption over 20 words. |
387
398
 
388
399
  | Option | Effect |
389
400
  | ----------------- | ---------------------------------------------------------------------------------------------------------- |
@@ -431,6 +442,23 @@ fails when the file or the symbol is gone. The action runs `verify` on every fig
431
442
  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
432
443
  changes.
433
444
 
445
+ `verify` prints one count line for each figure, and gives each edge one result:
446
+
447
+ ```console
448
+ docs/login.svg: 7 of 7 boxes defined; edges: 9 found, 0 not found, 1 unsure, 1 not checked
449
+ ```
450
+
451
+ - found: the caller code calls or references the callee through an import, the same file or a typed receiver.
452
+ - not found: the caller code does not. `verify` warns, and `--strict` makes it an error.
453
+ - unsure: `verify` cannot decide, for example when a receiver has no declared type. `verify` lists it with the reason. It never fails CI.
454
+ - not checked: the edge has no `source`, or the language is not supported.
455
+
456
+ A method source is `path#Owner.name`. An edge `source` names the function that makes the call.
457
+
458
+ `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.
459
+
460
+ Edge checks support TypeScript/JavaScript, Python, Go, Java, C# and Rust. Other files keep the name check for boxes.
461
+
434
462
  A process figure for a team links its boxes to the SOP document, not to code: `"source": "docs/sop/refunds.md#step-3-approve-or-reject"`. The symbol is the heading as a GitHub anchor. If the heading is gone, `verify` fails.
435
463
 
436
464
  ```yaml
@@ -450,7 +478,7 @@ jobs:
450
478
  - uses: actions/setup-node@v4
451
479
  with:
452
480
  node-version: 22
453
- - uses: iamalvisng/flowfig@v0.7.0
481
+ - uses: iamalvisng/flowfig@v0.8.1
454
482
  with:
455
483
  figures: 'docs/**/*.svg' # default **/*.svg
456
484
  ```
@@ -458,7 +486,8 @@ jobs:
458
486
  ## MCP server
459
487
 
460
488
  `npx flowfig mcp` serves the tools `docs`, `check`, `render`, `verify` and `diff` over stdio, for an agent with no shell.
461
- `render` writes the SVG file and returns the check lines and the path, so no SVG text goes through the model.
489
+ `render` writes the SVG file and returns the check lines and the path, so no SVG text goes through the model. `docs` takes an
490
+ optional `topic`.
462
491
 
463
492
  `npx flowfig init` registers the server for Claude Code (`.mcp.json`), Cursor (`.cursor/mcp.json`), GitHub Copilot
464
493
  (`.vscode/mcp.json`), Gemini CLI (`.gemini/settings.json`) and Kiro (`.kiro/settings/mcp.json`). It merges one entry into
@@ -484,6 +513,7 @@ The player has these controls:
484
513
  - A pause button, and a speed button that changes between 1× and 2×.
485
514
  - A full screen button. Esc closes full screen.
486
515
  - 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.
516
+ - 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.
487
517
 
488
518
  The player props are the spec fields. `theme` sets the colors, `speed` sets the packet time, and `autoplay={false}` stops the
489
519
  auto start. `check` runs the check rules. If a figure is wider than its container, the player shrinks the figure to half size at
@@ -523,7 +553,8 @@ writeFileSync('first.svg', toSvg(spec));
523
553
  - `check(spec, options)` returns the findings. It also takes `width` and `minText`.
524
554
  - `render(spec, options)` returns `{ svg, scene }`. The scene is the layout that the check reads. Its shape can change.
525
555
 
526
- `flowfig/verify` is for Node only. It exports `verify`, `links`, `owners` and `parseSource`, the same checks as `flowfig verify`.
556
+ `flowfig/verify` is for Node only. It exports `verify`, `verifyReport` (the findings and the counts), `links`, `owners` and `parseSource`, the same checks
557
+ as `flowfig verify`.
527
558
 
528
559
  `toSvg` does not put the spec in the SVG. Only the CLI adds the spec, which `--spec` reads back.
529
560
 
@@ -566,7 +597,8 @@ A section sits between `<!-- flowfig:start -->` and `<!-- flowfig:end -->`. A se
566
597
  of the file. A second run also replaces a whole file that `init` wrote. If a whole file exists and did not come from `init`,
567
598
  `init` skips the file. `init` also skips a section file that has a start marker and no end marker.
568
599
 
569
- `npx flowfig docs` prints the full guide as Markdown.
600
+ `npx flowfig docs` prints the core guide as Markdown. `npx flowfig docs <topic>` prints one topic: `lanes`, `timeline`, `rail`,
601
+ `marks` or `verify`.
570
602
 
571
603
  ## What flowfig does not draw
572
604
 
package/dist/check.js CHANGED
@@ -1,11 +1,68 @@
1
1
  import { DARK, LIGHT, ON_ACCENT, TONES, edgeId, isGroup, dayOf, isLanesLayout, lanePlan, validAt, laneColumns, nodes, str, toBeat, } from './model.js';
2
2
  import { layoutRail } from './rail.js';
3
- import { crosses } from './geometry.js';
3
+ import { crosses, hit as curveHit } from './geometry.js';
4
4
  import { textWidth } from './text.js';
5
5
  import { owners, parseSource } from './source.js';
6
6
  const err = (rule, ids, message) => ({ rule, severity: 'error', ids, message });
7
7
  const warn = (rule, ids, message) => ({ rule, severity: 'warning', ids, message });
8
8
  const groupIds = (g) => [...(g.id ? [g.id] : []), ...g.children.flatMap((c) => (isGroup(c) ? groupIds(c) : []))];
9
+ const FILLER = /\b(seamless(ly)?|robust|powerful|leverag(e|es|ed|ing)|effortless(ly)?|cutting-edge|state-of-the-art|holistic|synergy|empower(s|ed|ing)?|elegant(ly)?)\b/i;
10
+ const codeShape = (t) => /^[^A-Za-z]*[a-z][A-Za-z0-9]*[A-Z]|[A-Za-z]_[A-Za-z]|\(\)|::|\w\.\w+\(/.test(t);
11
+ const DB_COMMAND = /^(SELECT|INSERT|UPDATE|DELETE|UPSERT|MERGE|SET|GET|DEL|HSET|HGET|HDEL|EXPIRE|INCR|DECR|LPUSH|RPUSH|LPOP|RPOP|SADD|ZADD|PUBLISH|XADD)\b/;
12
+ const HTTP_LINE = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) \//;
13
+ const codeToken = (t) => codeShape(t) || t.split(/[./]/).some(codeShape);
14
+ function plainText(fig) {
15
+ const out = [];
16
+ const check = (owner, field, text, o = {}) => {
17
+ const t = str(text).trim();
18
+ if (!t)
19
+ return;
20
+ const tokens = t.split(/\s+/);
21
+ const symbol = o.source
22
+ ? parseSource(o.source)
23
+ ?.symbol?.split(/[.#:]+/)
24
+ .pop()
25
+ : undefined;
26
+ const code = o.code === false ? undefined : tokens.find((w) => codeToken(w) || (w === symbol && /[a-z][A-Z]/.test(w)));
27
+ const filler = FILLER.exec(t)?.[0];
28
+ const command = o.command && DB_COMMAND.test(t) && !HTTP_LINE.test(t);
29
+ const why = [
30
+ command && `${field} "${t}" looks like code; use plain words`,
31
+ code &&
32
+ (tokens.length === 1 && field === 'label'
33
+ ? `${field} "${t}" looks like code; use plain words, the code name goes in source; if this is a product name, keep it`
34
+ : `${field} has the code name "${code}"; use plain words; if this is a product name, keep it`),
35
+ symbol && t === symbol && !code && !/^[A-Za-z]+$/.test(t) && `${field} "${t}" repeats its source; use plain words`,
36
+ filler && `${field} "${t}" has the filler word "${filler}"`,
37
+ o.words && tokens.length > 20 && `${field} has ${tokens.length} words; keep it to 20`,
38
+ ].filter(Boolean);
39
+ if (why.length)
40
+ out.push(warn('plain-text', [], `${owner}: ${why.join('; ')}`));
41
+ };
42
+ for (const n of nodes(fig.layout)) {
43
+ check(`box "${n.id}"`, 'label', n.label, { source: n.source, command: true });
44
+ check(`box "${n.id}"`, 'sub', n.sub);
45
+ }
46
+ const groupLabels = (g) => {
47
+ if (g.label != null)
48
+ check(`group "${g.id ?? str(g.label)}"`, 'label', g.label);
49
+ for (const c of g.children)
50
+ if (isGroup(c))
51
+ groupLabels(c);
52
+ };
53
+ groupLabels(fig.layout);
54
+ fig.edges.forEach((e) => check(`edge "${edgeId(e)}"`, 'label', e.label, { source: e.source, command: true }));
55
+ for (const s of fig.steps ?? []) {
56
+ const name = str(s.label);
57
+ check(`step "${name}"`, 'label', s.label);
58
+ check(`step "${name}"`, 'caption', s.caption, { words: true });
59
+ s.flow.map(toBeat).forEach((b, i) => {
60
+ check(`step "${name}"`, `say ${i + 1}`, b.say, { words: true });
61
+ b.hops.forEach((h) => check(`step "${name}"`, `data ${i + 1}`, h.data, { code: false }));
62
+ });
63
+ }
64
+ return out;
65
+ }
9
66
  /** Faults in the spec: dangling ids, duplicate ids, empty steps. The renderers skip them silently. */
10
67
  export function checkSpec(fig) {
11
68
  const out = [];
@@ -44,9 +101,12 @@ export function checkSpec(fig) {
44
101
  if (e.quiet && !used.has(edges[i]))
45
102
  out.push(err('hidden-edge', [edges[i]], `edge "${edges[i]}" is quiet and no beat uses it, so the figure never shows it`));
46
103
  });
47
- for (const [who, source] of owners(fig))
104
+ for (const [who, source, via] of owners(fig)) {
48
105
  if (source != null && !parseSource(source))
49
106
  out.push(warn('bad-source', [], `${who}: source "${source}" is not path or path#symbol`));
107
+ if (via != null && (typeof via !== 'string' || !/^\S+$/.test(via)))
108
+ out.push(warn('bad-source', [], `${who}: via ${JSON.stringify(via)} is not a string of one word`));
109
+ }
50
110
  const marks = nodes(fig.layout).map((n) => n.mark);
51
111
  const starts = marks.filter((m) => m === 'start').length;
52
112
  const ends = marks.filter((m) => m === 'end').length;
@@ -101,6 +161,7 @@ export function checkSpec(fig) {
101
161
  for (const n of nodes(fig.layout))
102
162
  if (n.at != null && !validAt(n.at))
103
163
  out.push(err('bad-at', [n.id], `box "${n.id}": at ${n.at} is not an integer of 0 or more`));
164
+ out.push(...plainText(fig));
104
165
  return out;
105
166
  }
106
167
  const at = ([p0, p1, p2, p3], t) => {
@@ -125,11 +186,9 @@ export function checkScene(scene, { width = 830, minText = 10 } = {}) {
125
186
  if (e.behind && !e.elbow)
126
187
  continue;
127
188
  const line = e.elbow ?? e.pts;
128
- const pts = line
129
- ? line.slice(1).flatMap((q, k) => Array.from({ length: 17 }, (_, i) => lerp(line[k], q, i / 16)))
130
- : Array.from({ length: 33 }, (_, i) => at(e.curve, i / 32));
189
+ const pts = line?.slice(1).flatMap((q, k) => Array.from({ length: 17 }, (_, i) => lerp(line[k], q, i / 16)));
131
190
  for (const b of scene.boxes)
132
- if (b.id !== e.from && b.id !== e.to && pts.some((p) => inside(p, b.rect, 2)))
191
+ if (b.id !== e.from && b.id !== e.to && (pts ? pts.some((p) => inside(p, b.rect, 2)) : curveHit(e.curve, [b.rect], -2).length))
133
192
  out.push(err('edge-crosses-box', [e.id, b.id], `edge "${e.id}" passes through box "${b.id}"`));
134
193
  }
135
194
  const pills = scene.edges.filter((e) => e.pts && e.label);
@@ -178,6 +237,9 @@ export function checkScene(scene, { width = 830, minText = 10 } = {}) {
178
237
  for (const b of scene.boxes)
179
238
  if (overlap(e.label, b.rect, 1))
180
239
  out.push(err('label-overlap', [e.id, b.id], `the label of edge "${e.id}" covers box "${b.id}"`));
240
+ const [p, a] = [e.label, scene.area];
241
+ if (a && e.step == null && (p.x < a.x - 0.5 || p.y < a.y - 0.5 || p.x + p.w > a.x + a.w + 0.5 || p.y + p.h > a.y + a.h + 0.5))
242
+ out.push(err('label-overlap', [e.id], `the label of edge "${e.id}" is outside the figure`));
181
243
  });
182
244
  const shown = scene.minFont * Math.min(1, width / scene.width);
183
245
  if (shown < minText)
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { basename, dirname, join, resolve } from 'node:path';
14
14
  import { pathToFileURL } from 'node:url';
15
15
  import { captureFrames, findBrowser, launch } from './browser.js';
16
16
  import { delays, encodeGif } from './gif.js';
17
- import { GUIDE } from './guide.js';
17
+ import { GUIDE, TOPICS } from './guide.js';
18
18
  import { runDraw } from './draw.js';
19
19
  import { runInit } from './init.js';
20
20
  import { serve } from './mcp.js';
@@ -22,22 +22,22 @@ import { openedLine, openSvg, runOpen } from './open.js';
22
22
  import { pageHtml } from './page.js';
23
23
  import { decodePng } from './png.js';
24
24
  import { diff, formatDiff } from './diff.js';
25
- import { loadSpec, reportLines, sortFindings, specOf, svgWithSpec } from './load.js';
25
+ import { loadSpec, reportLines, sortFindings, specOf, summaryLines, svgWithSpec, verifyLines } from './load.js';
26
26
  import { check, render, toSvg } from './svg.js';
27
27
  import { checkRendered } from './check.js';
28
- import { links, verify } from './verify.js';
28
+ import { coverageLine, links, unsureLines, verifyReport } from './verify.js';
29
29
  const USAGE = `usage: flowfig <-|spec.json|figure.ts> [out.svg] [--open] render a figure; a spec on stdin with -
30
30
  flowfig check <-|spec.json|figure.ts|figure.svg> list the faults; the input can be an SVG this wrote
31
31
  flowfig --spec figure.svg print the spec the SVG carries
32
32
  flowfig verify <input>... [--root <dir>] [--json] [--strict] check the code links of one or more figures
33
33
  flowfig diff <old> <new> [--json|--md] list the spec changes between two figures
34
- flowfig docs print the guide (Markdown)
34
+ flowfig docs [topic] print the guide, or one topic (Markdown)
35
35
  flowfig mcp serve check, render, verify, diff and docs over MCP (stdio)
36
36
  flowfig init [dir] [--agents <ids>] [-y] [--global] [--dry-run] [--no-mcp] write flowfig instructions for the coding agents of a repo
37
37
  flowfig draw "<question>" [--out <path>] [--model <alias>] [--max-turns <n>] [--json] [--open] ask Claude Code for a figure, then check it
38
38
  flowfig open <figure.svg> [--html <path>] show the figure in the default browser
39
39
  flowfig gif <figure.svg> [out.gif] [--step <n>] [--dark] [--fps <n>] [--scale <n>] [--mp4] write an animated GIF
40
- flags for render and check: --strict (warnings are errors), --json, --width <px>, --min-text <px>, --no-check (render only)`;
40
+ flags for render and check: --strict (warnings are errors), --json, --width <px>, --min-text <px>, --no-check (render only), --no-verify (render only)`;
41
41
  function usage(message) {
42
42
  console.error(message);
43
43
  process.exit(2);
@@ -62,7 +62,10 @@ if (!args[0] || ['help', '--help', '-h'].includes(args[0])) {
62
62
  process.exit(0);
63
63
  }
64
64
  if (args[0] === 'docs') {
65
- process.stdout.write(GUIDE);
65
+ const topic = args[1];
66
+ if (topic && !Object.hasOwn(TOPICS, topic))
67
+ usage(`unknown topic ${topic}; topics: ${Object.keys(TOPICS).join(', ')}`);
68
+ process.stdout.write(topic ? TOPICS[topic] : GUIDE);
66
69
  process.exit(0);
67
70
  }
68
71
  if (args[0] === 'mcp') {
@@ -120,20 +123,28 @@ if (args[0] === 'verify') {
120
123
  usage('verify needs at least one figure');
121
124
  const findings = [];
122
125
  const all = [];
126
+ const coverage = [];
123
127
  for (const input of args) {
124
128
  const props = await load(input);
125
- const found = sortFindings([...check(props), ...verify(props, { root })], strict).map((f) => ({ ...f, figure: input }));
129
+ const report = verifyReport(props, { root });
130
+ const found = sortFindings([...check(props), ...report.findings], strict).map((f) => ({ ...f, figure: input }));
126
131
  findings.push(...found);
132
+ coverage.push({ figure: input, ...report.coverage });
127
133
  all.push(...links(props).map((l) => ({ ...l, figure: input })));
128
134
  }
129
135
  const errors = findings.filter((f) => f.severity === 'error').length;
130
136
  if (json)
131
- console.log(JSON.stringify({ findings, links: all }, null, 2));
137
+ console.log(JSON.stringify({ findings, links: all, coverage }, null, 2));
132
138
  else {
133
139
  const n = (k, word) => `${k} ${word}${k === 1 ? '' : 's'}`;
134
140
  for (const f of findings)
135
141
  console.log(`${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.figure}: ${f.message}`);
136
142
  console.log(`${n(errors, 'error')}, ${n(findings.length - errors, 'warning')}`);
143
+ for (const c of coverage) {
144
+ console.log(coverageLine(c.figure, c));
145
+ for (const l of unsureLines(c))
146
+ console.log(l);
147
+ }
137
148
  }
138
149
  process.exit(errors ? 1 : 0);
139
150
  }
@@ -308,7 +319,7 @@ if (args[0] === 'gif') {
308
319
  process.exit(stopped || code);
309
320
  }
310
321
  const command = args[0] === 'check' ? args.shift() : 'render';
311
- const json = flag('--json'), strict = flag('--strict'), skip = flag('--no-check'), open = flag('--open');
322
+ const json = flag('--json'), strict = flag('--strict'), skip = flag('--no-check'), skipVerify = flag('--no-verify'), open = flag('--open');
312
323
  if (command === 'check' && open)
313
324
  usage('--open works with a render, not with check');
314
325
  const opts = { width: value('--width'), minText: value('--min-text') };
@@ -322,9 +333,9 @@ const props = await load(input);
322
333
  const rendered = skip ? undefined : render(props, opts);
323
334
  const findings = rendered ? sortFindings(checkRendered(props, opts, rendered.scene), strict) : [];
324
335
  const errors = findings.filter((f) => f.severity === 'error').length;
325
- const report = (print, tty) => {
336
+ const report = (print, tty, extra = []) => {
326
337
  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;
327
- for (const line of reportLines(props, findings, !skip))
338
+ for (const line of [...reportLines(props, findings, !skip), ...extra])
328
339
  print(paint(line));
329
340
  };
330
341
  if (command === 'check') {
@@ -334,10 +345,10 @@ if (command === 'check') {
334
345
  report(console.log, process.stdout.isTTY);
335
346
  process.exit(errors ? 1 : 0);
336
347
  }
337
- report(console.error, process.stderr.isTTY);
348
+ const dest = out ?? (input === '-' ? 'figure.svg' : input.replace(/\.[^./\\]+$/, '') + '.svg');
349
+ report(console.error, process.stderr.isTTY, errors ? [] : [...summaryLines(props), ...(skipVerify ? [] : verifyLines(props, dest))]);
338
350
  if (errors)
339
351
  process.exit(1);
340
- const dest = out ?? (input === '-' ? 'figure.svg' : input.replace(/\.[^./\\]+$/, '') + '.svg');
341
352
  const withSpec = svgWithSpec(props, opts, rendered?.svg);
342
353
  writeFileSync(dest, withSpec);
343
354
  console.log(`${dest} — ${(withSpec.length / 1024).toFixed(1)} kB`);
package/dist/code.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ export type Lang = 'ts' | 'py' | 'go' | 'java' | 'cs' | 'rs';
2
+ export type Read = {
3
+ (full: string): string | null;
4
+ files?: (root: string) => string[];
5
+ };
6
+ export type CodeFile = {
7
+ path: string;
8
+ lang: Lang;
9
+ text: string;
10
+ code: string;
11
+ keep: string;
12
+ };
13
+ export declare const langOf: (path: string) => Lang | null;
14
+ export declare const readFile: Read;
15
+ type Extent = [number, number];
16
+ export declare const esc: (s: string) => string;
17
+ export declare function matchClose(code: string, open: number): number;
18
+ export declare function pyBody(code: string, start: number): Extent;
19
+ export declare function outside(root: string, path: string): boolean;
20
+ export declare function codeFile(root: string, path: string, read?: Read, cache?: Map<string, CodeFile | null>): CodeFile | null;
21
+ export declare function locate(file: CodeFile, symbol: string): {
22
+ start: number;
23
+ end: number;
24
+ } | null;
25
+ export declare const isDefined: (file: CodeFile, symbol: string) => boolean;
26
+ export {};