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 +55 -23
- package/dist/check.js +68 -6
- package/dist/cli.js +24 -13
- package/dist/code.d.ts +26 -0
- package/dist/code.js +568 -0
- package/dist/diff.js +12 -5
- package/dist/draw.js +1 -1
- package/dist/edges.d.ts +7 -0
- package/dist/edges.js +557 -0
- package/dist/geometry.d.ts +11 -2
- package/dist/geometry.js +255 -67
- package/dist/guide.d.ts +22 -2
- package/dist/guide.js +282 -219
- package/dist/imports.d.ts +11 -0
- package/dist/imports.js +235 -0
- package/dist/index.js +36 -28
- package/dist/load.d.ts +2 -0
- package/dist/load.js +24 -1
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +22 -9
- package/dist/model.d.ts +13 -2
- package/dist/model.js +54 -11
- package/dist/scene.d.ts +2 -0
- package/dist/source.d.ts +1 -1
- package/dist/source.js +3 -3
- package/dist/svg.js +49 -32
- package/dist/type-index.d.ts +55 -0
- package/dist/type-index.js +383 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +733 -0
- package/dist/verify.d.ts +25 -3
- package/dist/verify.js +67 -22
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
44
|
-
name a heading, so a process diagram can follow its SOP
|
|
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.
|
|
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
|
|
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,
|
|
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": "
|
|
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
|
-
|
|
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
|

|
|
@@ -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
|
|
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
|
|
285
|
-
| ----------- |
|
|
286
|
-
| `children` | The boxes and groups in the group, in order.
|
|
287
|
-
| `id` | The name that an edge can use to reach the whole group.
|
|
288
|
-
| `label` | The title of the frame. Only a group with a label has a frame.
|
|
289
|
-
| `direction` | `"row"` puts the children side by side. `"column"` stacks them.
|
|
290
|
-
| `gap` | The space between the children in px.
|
|
291
|
-
| `align` | `"start"`, `"center"` or `"end"`, across the direction.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {};
|