figdown 0.4.1 → 0.5.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/.claude-plugin/plugin.json +1 -1
- package/dist/README.md +85 -0
- package/dist/figdown.js +1449 -385
- package/dist/figdown.mjs +1449 -385
- package/examples/evpn-fabric.svg +1 -1
- package/examples/showcase/arp-resolution.svg +1 -1
- package/examples/showcase/ethernet-frame.svg +1 -1
- package/examples/showcase/l2-forwarding-logic.svg +1 -1
- package/examples/showcase/tcp-handshake.svg +1 -1
- package/examples/showcase/tcp-header.svg +1 -1
- package/examples/showcase/tcp-state-machine.svg +1 -1
- package/integrations/mcp-server/server.js +50 -4
- package/package.json +4 -2
- package/skill/README.md +130 -0
- package/skill/figdown/SKILL.md +20 -0
- package/skill/figdown/figdown.html +4118 -897
- package/skill/figdown/reference/experimental/flowchart.md +23 -0
- package/skill/figdown/reference/experimental/sequence.md +28 -0
- package/skill/figdown/reference/experimental/statechart.md +29 -0
- package/skill/figdown/reference/experimental/topology.md +42 -0
- package/skill/figdown/reference/scene.md +23 -0
- package/skill/figdown/reference/transcribe.md +4 -1
|
@@ -149,6 +149,36 @@ function parseFailed(label, errors) {
|
|
|
149
149
|
);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// GEOMETRY-TIME REFUSAL (spec core §8, §8.4). A document whose source is
|
|
153
|
+
// impeccable line by line can still describe a drawing that states something
|
|
154
|
+
// it does not — a `group` band enclosing a non-member, a `pin` covering a node
|
|
155
|
+
// completely. Those come from `render`, not from `parse`, and core §8 makes
|
|
156
|
+
// them cost exactly what a parse error costs: nothing is drawn and no artifact
|
|
157
|
+
// is written. Reporting them under the PARSE FAILED banner would send the
|
|
158
|
+
// author to look for a typo on a line that has none, so they get their own
|
|
159
|
+
// heading and their own remedy sentence.
|
|
160
|
+
function figureRefused(label, errors) {
|
|
161
|
+
return text(
|
|
162
|
+
'FIGURE REFUSED — ' + errors.length + ' geometry-time diagnostic(s)' + (label ? ' in ' + label : '') + '.\n' +
|
|
163
|
+
'The source parses clean; the DRAWING would state something the source does not,\n' +
|
|
164
|
+
'so nothing was rendered and no artifact was written (spec core §8). Each line names\n' +
|
|
165
|
+
'the line you can act on — usually the `pin` that fixed the position.',
|
|
166
|
+
errors.join('\n')
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// WHICH CHANNEL REFUSED. `render()` reports parse errors when there are any and
|
|
171
|
+
// geometry-time diagnostics ONLY when there are none — the two are mutually
|
|
172
|
+
// exclusive by construction, because nothing is rendered for a document that
|
|
173
|
+
// does not parse. So asking the parser is enough to name the channel, and it
|
|
174
|
+
// is asked directly rather than inferred from the message text: a banner keyed
|
|
175
|
+
// on wording would go wrong the first time a message is reworded.
|
|
176
|
+
function refused(label, src, errors) {
|
|
177
|
+
return figdown.parse(src).errors.length
|
|
178
|
+
? parseFailed(label, errors)
|
|
179
|
+
: figureRefused(label, errors);
|
|
180
|
+
}
|
|
181
|
+
|
|
152
182
|
// `source` or `path`, never both, never neither. Returns {src, label, file} or
|
|
153
183
|
// throws a message string.
|
|
154
184
|
function takeSource(args, exts) {
|
|
@@ -172,8 +202,12 @@ function toolBuild(args) {
|
|
|
172
202
|
try { s = takeSource(args, ['.fd']); } catch (m) { return toolError(m); }
|
|
173
203
|
|
|
174
204
|
const opts = args.with_title === true ? { title: true } : undefined;
|
|
205
|
+
// `artifact` refuses on EITHER channel (dist 0.4): a parse error, or
|
|
206
|
+
// a geometry-time diagnostic that only `render` can see. Both give svg=null
|
|
207
|
+
// and a non-empty `errors`, so nothing below can write a picture the engine
|
|
208
|
+
// has already said is wrong.
|
|
175
209
|
const { svg, errors } = figdown.artifact(s.src, opts);
|
|
176
|
-
if (errors.length) return
|
|
210
|
+
if (errors.length) return refused(s.label, s.src, errors);
|
|
177
211
|
|
|
178
212
|
const wrote = args.write === true && s.file
|
|
179
213
|
? s.file.replace(/\.fd$/, '') + '.svg'
|
|
@@ -247,9 +281,13 @@ function toolCheck(args) {
|
|
|
247
281
|
|
|
248
282
|
if (!units.length) return text('ok: true 0 .fd file(s) found under ' + args.path + ' — nothing to check.');
|
|
249
283
|
|
|
284
|
+
// BOTH CHANNELS, because a checker that clears a document `figdown_build`
|
|
285
|
+
// then refuses is a checker that lies. `render` reports parse errors when
|
|
286
|
+
// there are any and geometry-time diagnostics (core §8) otherwise, which is
|
|
287
|
+
// exactly the set that decides whether an artifact can be written.
|
|
250
288
|
const bad = [];
|
|
251
289
|
for (const u of units) {
|
|
252
|
-
const errs = figdown.
|
|
290
|
+
const errs = figdown.render(u.src).errors;
|
|
253
291
|
if (errs.length) bad.push(u.label + ':\n' + errs.map(e => ' ' + e).join('\n'));
|
|
254
292
|
}
|
|
255
293
|
|
|
@@ -291,9 +329,17 @@ function toolRead(args) {
|
|
|
291
329
|
}
|
|
292
330
|
const sidecar = s.file ? s.file.replace(/\.svg$/, '.fd') : null;
|
|
293
331
|
if (sidecar && fs.existsSync(sidecar) && recordedSha) {
|
|
294
|
-
const
|
|
332
|
+
const rebuilt = figdown.artifact(fs.readFileSync(sidecar, 'utf8'));
|
|
333
|
+
const live = rebuilt.svg;
|
|
295
334
|
const liveSha = live && (live.match(/data-sha256="([0-9a-f]{64})"/) || [])[1];
|
|
296
|
-
|
|
335
|
+
// A sidecar the engine now REFUSES cannot be compared, and calling that
|
|
336
|
+
// "stale" would name the wrong defect: the .fd did not drift, it stopped
|
|
337
|
+
// being drawable. Say which it is.
|
|
338
|
+
notes.push(rebuilt.errors.length
|
|
339
|
+
? 'SIDECAR REFUSED: ' + path.basename(sidecar) + ' no longer builds — '
|
|
340
|
+
+ rebuilt.errors.length + ' diagnostic(s), so the recorded hash could not be checked:\n '
|
|
341
|
+
+ rebuilt.errors.join('\n ')
|
|
342
|
+
: liveSha === recordedSha
|
|
297
343
|
? 'sidecar ' + path.basename(sidecar) + ' matches the artifact\'s recorded hash'
|
|
298
344
|
: 'STALE ARTIFACT: ' + path.basename(sidecar) + ' has changed since this .svg was built. '
|
|
299
345
|
+ 'The .fd is truth — rebuild.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A missing edge still looks fine. Text doesn't. A closed, deterministic figure language for Markdown whose source states the meaning, so the next reader can check the figure instead of only looking at it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "FigDown <hello@figdown.org>",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"gate:page": "node tools/page-check.js",
|
|
33
33
|
"gate:shape": "node tools/shape-check.js --strict",
|
|
34
34
|
"gate:strip": "node tools/strip-check.js --strict",
|
|
35
|
-
"gate:layout": "node tools/layout-lint.js --strict",
|
|
35
|
+
"gate:layout": "node tools/layout-lint.js --strict examples figures tools/layout-lint-fixtures/b66-typed-title",
|
|
36
36
|
"gate:namespace": "node tools/namespace-check.js --strict",
|
|
37
37
|
"gate:artifact": "node tools/artifact-check.js --strict",
|
|
38
38
|
"gate:dist": "node tools/dist-check.js --strict",
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
"gate:standards": "node tools/standards-check.js --strict",
|
|
49
49
|
"gate:plugin": "node tools/plugin-check.js --strict",
|
|
50
50
|
"gate:mcp": "node integrations/mcp-server/test.js",
|
|
51
|
+
"gate:proof": "node tools/make-proof.js --check",
|
|
52
|
+
"gate:safesvg": "node tools/safe-svg-check.js --strict",
|
|
51
53
|
"gates:list": "node -e \"const s=require(process.cwd()+'/package.json').scripts||{};const g=Object.keys(s).filter(k=>k.startsWith('gate:'));if(!g.length){console.error('gates:list — no gate:* scripts found');process.exit(2);}if(process.env.GATES_JSON)console.log(JSON.stringify(g));else g.forEach(k=>console.log(k+' -> '+s[k]));\"",
|
|
52
54
|
"test": "node -e \"const s=require(process.cwd()+'/package.json').scripts||{};const sp=require('child_process').spawnSync;const g=Object.keys(s).filter(k=>k.startsWith('gate:'));if(!g.length){console.error('npm test — no gate:* scripts found; refusing to report success');process.exit(2);}const bad=[];g.forEach(k=>{const a=s[k].split(' ');const bin=a[0]==='node'?process.execPath:a[0];console.log('');console.log('=== GATE '+k+' ['+s[k]+'] ===');const r=sp(bin,a.slice(1),{stdio:'inherit'});if(r.error)console.error(' spawn error: '+r.error.message);const c=(r.status===null||r.status===undefined)?1:r.status;console.log('--- '+k+': '+(c===0?'PASS':'FAIL (exit '+c+')')+' ---');if(c!==0)bad.push(k+' ['+s[k]+'] exit '+c);});console.log('');console.log('=== GATE SUMMARY: '+(g.length-bad.length)+'/'+g.length+' passed ===');if(bad.length){console.log('FAILED GATES:');bad.forEach(b=>console.log(' '+b));process.exit(1);}console.log('ALL GATES GREEN');\"",
|
|
53
55
|
"gate:reference": "node tools/reference-gate.js"
|
package/skill/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# FigDown agent skill
|
|
2
|
+
|
|
3
|
+
An installable skill that teaches an AI coding agent (Claude Code and
|
|
4
|
+
compatible agent frameworks) to maintain documentation figures with
|
|
5
|
+
FigDown: edit `.fd` sources, build deterministic `.svg` artifacts, and
|
|
6
|
+
embed them in Markdown with the `source:` footer convention.
|
|
7
|
+
|
|
8
|
+
The bundle is self-contained: `SKILL.md`, a `reference/` directory,
|
|
9
|
+
`build-svg.js` (validator/renderer CLI) and `figdown.html` (the engine
|
|
10
|
+
it extracts at runtime — also the editor: open it in any browser).
|
|
11
|
+
|
|
12
|
+
**It is built for progressive disclosure.** `SKILL.md` is the only file
|
|
13
|
+
always loaded, and it holds the workflow, the genre-independent
|
|
14
|
+
language, and a **router**. Everything else is pulled on demand, and
|
|
15
|
+
the routing rule is mechanical: line 1 of a `.fd` names its genre, so
|
|
16
|
+
the agent knows which file it needs before it needs it.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
SKILL.md always loaded
|
|
20
|
+
reference/scene.md genre `block` — its OWN vocabulary
|
|
21
|
+
reference/bitfield.md
|
|
22
|
+
reference/table.md
|
|
23
|
+
reference/layout.md arranging a scene
|
|
24
|
+
reference/reading.md reading a .fd for meaning
|
|
25
|
+
reference/transcribe.md transcribing an existing figure
|
|
26
|
+
reference/experimental/block.md block's EXPERIMENTAL markers and zones
|
|
27
|
+
reference/experimental/chart.md a chart from a table (any genre
|
|
28
|
+
that can host one)
|
|
29
|
+
reference/experimental/topology.md the five EXPERIMENTAL genres, each
|
|
30
|
+
reference/experimental/flowchart.md declaring its OWN vocabulary in
|
|
31
|
+
reference/experimental/statechart.md its own file, so an agent can
|
|
32
|
+
reference/experimental/timing.md ignore the ones it is not using
|
|
33
|
+
reference/experimental/sequence.md — the ladder genre, and the one
|
|
34
|
+
whose load set has no layout file
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`sequence` is the exception to the "genre file + layout file" pairing above:
|
|
38
|
+
both of its axes are declaration order, so nothing in the layout namespace
|
|
39
|
+
moves a mark there and its own file is the whole of what an author needs.
|
|
40
|
+
|
|
41
|
+
**Every scene genre's file is self-sufficient (`SUBJECT-VOCABULARY-SCOPE`).** There is
|
|
42
|
+
no shared scene-vocabulary file and there was never meant to be one: only
|
|
43
|
+
`figdown`, `title` and `layout` are cross-genre by definition, and a word two
|
|
44
|
+
genres spell the same is two independent declarations that happen to agree
|
|
45
|
+
today. So an agent authoring a `topology` figure loads `topology.md` and does
|
|
46
|
+
not have to follow a link into `block`'s file to find out what `bundle` means
|
|
47
|
+
— which is what it used to have to do, and what it found there was written for
|
|
48
|
+
a different domain. The file this list used to call
|
|
49
|
+
`reference/experimental/constructs.md` was that shared file; it was removed
|
|
50
|
+
with the ruling, and its contents went to the genres that own them.
|
|
51
|
+
|
|
52
|
+
The `reference/` files are a generated copy of [`read/0.5/`](../read/0.5/reading.md),
|
|
53
|
+
which is the source of truth; see "Keeping the bundle fresh" below. If you only
|
|
54
|
+
want to READ a `.fd` from this repository, go to `read/0.5/` and install
|
|
55
|
+
nothing.
|
|
56
|
+
|
|
57
|
+
An agent *reading* a figure loads `SKILL.md` + `reference/reading.md`
|
|
58
|
+
and nothing else; an agent authoring a `bitfield` never pays for any
|
|
59
|
+
scene genre's vocabulary. `node tools/skill-coverage.js --strict` is the gate:
|
|
60
|
+
it checks that every registered keyword, option key and enum value is
|
|
61
|
+
taught in its genre's load set, that no retired spelling is taught, and
|
|
62
|
+
that the always-loaded file stays genre-independent.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
Two paths, one bundle. The plugin is a **wrapper**, not a second copy:
|
|
67
|
+
`.claude-plugin/plugin.json` at the repository root declares
|
|
68
|
+
`"skills": "./skill/"`, so the plugin ships exactly the directory documented
|
|
69
|
+
here. Nothing below is duplicated anywhere.
|
|
70
|
+
|
|
71
|
+
**Claude Code plugin (recommended — installs and updates itself):**
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
/plugin marketplace add FigDown/figdown
|
|
75
|
+
/plugin install figdown@figdown
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`/plugin marketplace update figdown` refreshes the catalogue; the plugin is
|
|
79
|
+
pinned to the `version` in its manifest, so a new release arrives when that
|
|
80
|
+
number changes. No submission and no review are involved: the marketplace is
|
|
81
|
+
[`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json) in this
|
|
82
|
+
repository.
|
|
83
|
+
|
|
84
|
+
**Copy the directory (no plugin mechanism, every project):**
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
cp -r skill/figdown ~/.claude/skills/figdown
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Copy the directory (no plugin mechanism, one project only):**
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
cp -r skill/figdown <your-repo>/.claude/skills/figdown
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The copy paths remain fully supported: the bundle is self-contained, so a
|
|
97
|
+
`cp` of `skill/figdown` into any skills directory works with no network, no
|
|
98
|
+
repository and no manifest.
|
|
99
|
+
|
|
100
|
+
Then ask the agent for a figure ("draw the ingress datapath as a
|
|
101
|
+
figure in docs/arch.md") — the skill triggers on diagram/figure work
|
|
102
|
+
and follows the FigDown workflow automatically. Requires Node.js for
|
|
103
|
+
the build step; no other dependencies, no network.
|
|
104
|
+
|
|
105
|
+
**Other agent frameworks:** nothing here is Claude-specific except the
|
|
106
|
+
directory convention. `SKILL.md` and `reference/` are ordinary Markdown —
|
|
107
|
+
point the agent at `skill/figdown/SKILL.md` and it follows the same router.
|
|
108
|
+
[guide/agents.md](../guide/agents.md) is the orientation page that leads here;
|
|
109
|
+
it teaches no syntax, because this bundle owns that.
|
|
110
|
+
|
|
111
|
+
## Keeping the bundle fresh
|
|
112
|
+
|
|
113
|
+
`figdown/figdown.html`, `figdown/build-svg.js` and everything under
|
|
114
|
+
`figdown/reference/` are **build artifacts** regenerated from their single
|
|
115
|
+
sources (`editor/figdown.html`, `tools/build-svg.js`, and — since `GENRE-REFERENCE-ADDRESS` —
|
|
116
|
+
[`read/0.5/`](../read/0.5/reading.md)) — never edited by hand:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
node tools/make-skill.js
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`SKILL.md` is the one hand-maintained source left in the bundle;
|
|
123
|
+
`make-skill.js` does not own it and does not delete it.
|
|
124
|
+
|
|
125
|
+
**Why `reference/` is vendored rather than linked.** `read/0.5/` is where a
|
|
126
|
+
reader of this repository is sent — nothing to install. This directory is
|
|
127
|
+
copied out of the repository into `~/.claude/skills/`, so a path pointing back
|
|
128
|
+
at `read/0.5/` would dangle the moment it is installed. The copy is therefore
|
|
129
|
+
generated, and `node tools/skill-coverage.js --strict` fails on any byte of
|
|
130
|
+
difference (check 0, VENDOR). Edit `read/0.5/`, then re-run `make-skill.js`.
|
package/skill/figdown/SKILL.md
CHANGED
|
@@ -52,6 +52,26 @@ Recover from there. On hash mismatch the `.fd` is truth: rebuild. If the hash
|
|
|
52
52
|
matches but your drawing differs, compare `data-engine-version` — that is the
|
|
53
53
|
other half of "same source → same SVG".
|
|
54
54
|
|
|
55
|
+
## Repair loop, truthfulness and delivery order
|
|
56
|
+
|
|
57
|
+
**Bounded repair loop.** After step 2 fails, keep fixing and rerunning only
|
|
58
|
+
while each attempt's error count is a new minimum — strictly lower than
|
|
59
|
+
every attempt before it. Two consecutive rounds with no improvement mean
|
|
60
|
+
another identical round will not help either: stop, and report the
|
|
61
|
+
remaining errors truthfully instead of trying a third time.
|
|
62
|
+
|
|
63
|
+
**Truthfulness.** A run that still reports errors is never described as a
|
|
64
|
+
success, in any wording. Never delete or weaken meaningful declared content
|
|
65
|
+
— a node, an edge, a field, a row, a signal, whatever the genre calls it —
|
|
66
|
+
merely to make the error count reach zero; removing what the figure states
|
|
67
|
+
is not a repair, it is a smaller, different figure wearing the same
|
|
68
|
+
filename. Once a run passes cleanly, the document is frozen: no further
|
|
69
|
+
edits chasing "one more improvement."
|
|
70
|
+
|
|
71
|
+
**Artifact first.** Write the file, or the fenced block, before explaining
|
|
72
|
+
it. Commentary, reasoning and a description of what changed come after the
|
|
73
|
+
artifact exists — never before it, and never interleaved with it.
|
|
74
|
+
|
|
55
75
|
## When the figure comes from a source
|
|
56
76
|
|
|
57
77
|
Drawing is step 5 of 6. The steps before it decide what is true; the step after
|