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.
@@ -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 parseFailed(s.label, errors);
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.parse(u.src).errors;
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 live = figdown.artifact(fs.readFileSync(sidecar, 'utf8')).svg;
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
- notes.push(liveSha === recordedSha
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.4.1",
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"
@@ -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`.
@@ -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