figdown 0.1.0-rc.1 → 0.1.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.
@@ -0,0 +1,173 @@
1
+ ---
2
+ name: figdown
3
+ description: Create and maintain documentation figures as FigDown .fd text files with deterministic SVG artifacts embedded in Markdown. Use when asked to create, edit, fix, or read diagrams/figures in docs — block and architecture diagrams, topologies, flowcharts, bit-level layouts (packet headers, hardware registers), tables, timing waveforms — or when a .md contains an SVG with a "source: *.fd" footer.
4
+ ---
5
+
6
+ # FigDown — figures as text, one source, two readers
7
+
8
+ A figure lives as a `.fd` text file (single source of truth). The SVG is a
9
+ deterministic build artifact for human eyes; **you read the `.fd` for meaning —
10
+ never OCR the SVG**. Structure (nodes, edges, fields, rows) is the content;
11
+ placement and colour are presentation whose only job is rendering stability.
12
+
13
+ **Choose the form that communicates best.** Three modes: (1) prose, list or
14
+ table alone for linear facts, values or procedures — do not draw; (2) a figure
15
+ alone when it is self-contained; (3) figure + prose, the common case — the
16
+ figure carries structure and relationships, the surrounding prose carries the
17
+ detail. When the answer involves a figure, author one. When it does not, do
18
+ not draw out of habit. The `.fd` must carry everything the rendered figure
19
+ conveys, and no more is owed.
20
+
21
+ **Mode 3 has an authoring consequence:** if the figure is the outline, do not
22
+ cram the whole story into it. Keep labels short and leave the detail to the
23
+ host document. Overstuffed labels are the visible symptom of an author making
24
+ the figure do prose's job.
25
+
26
+ ## Workflow
27
+
28
+ 1. Edit or create `X.fd` (never edit an `.svg`). A new file opens with the
29
+ spec provenance line, verbatim, then the header — see *The document
30
+ skeleton* below.
31
+ 2. Build + validate: `node <this-skill-dir>/build-svg.js X.fd`
32
+ Errors come as `Line N: message` — fix and rerun until `OK`. Titles are not
33
+ drawn by default (Markdown supplies the caption); `--with-title` opts in.
34
+ 3. Embed in Markdown — the SVG only, never `.fd` content:
35
+
36
+ ```markdown
37
+ ![Ingress datapath](figures/ingress.svg)
38
+
39
+ <sub>source: [figures/ingress.fd](figures/ingress.fd)</sub>
40
+ ```
41
+
42
+ 4. Keep `X.fd` and `X.svg` side by side, same basename; commit both.
43
+
44
+ If a `.fd` is missing, its `.svg` carries the source verbatim, the SHA-256 of
45
+ that source and the engine version that rendered it, in
46
+ `<metadata id="figdown-source" data-sha256="…" data-engine-version="…">`.
47
+ Recover from there. On hash mismatch the `.fd` is truth: rebuild. If the hash
48
+ matches but your drawing differs, compare `data-engine-version` — that is the
49
+ other half of "same source → same SVG".
50
+
51
+ ## Load only what you need
52
+
53
+ This file is the whole genre-independent language. **Everything else is one
54
+ lookup away, and the lookup is mechanical:** line 1 of a `.fd` names its genre,
55
+ so you know which file you need before you need it.
56
+
57
+ <!-- skill-coverage: router -->
58
+
59
+ | Genre on line 1 | Load | Add only for EXPERIMENTAL constructs |
60
+ |---|---|---|
61
+ | `block` — architecture, dataflow, hierarchy | `reference/scene.md`, `reference/layout.md` | `reference/experimental/constructs.md` |
62
+ | `bitfield` — packet headers, register layouts | `reference/bitfield.md` | — |
63
+ | `table` — config, state, memory maps | `reference/table.md` | `reference/experimental/constructs.md` |
64
+ | `topology` | `reference/scene.md`, `reference/layout.md` | `reference/experimental/topology.md`, `reference/experimental/constructs.md` |
65
+ | `flowchart` | `reference/scene.md`, `reference/layout.md` | `reference/experimental/flowchart.md`, `reference/experimental/constructs.md` |
66
+ | `timing` | — | `reference/experimental/timing.md` |
67
+
68
+ Two more files answer a **task** rather than a genre:
69
+
70
+ - **Reading a `.fd` someone else wrote, to summarise or answer from it** →
71
+ `reference/reading.md`. It is the contract for what you may conclude and
72
+ what you must not infer, and it is all you need: a reader can skip every
73
+ genre file above.
74
+ - **Transcribing an existing figure** — a drawing, a screenshot, another
75
+ format → `reference/transcribe.md`.
76
+
77
+ Pick the genre by what the figure IS, not by its subject: the left-hand
78
+ column above says what each is for. Prefer the first three — they are the
79
+ portable ones.
80
+
81
+ ## The document skeleton
82
+
83
+ The grammar is **CLOSED**: an unknown line is an error, never ignored. Five
84
+ keywords — `figdown`, `title`, `class`, `layout`, `pin` — mean the same thing
85
+ under every genre and no genre may redefine them. Everything else belongs to a
86
+ genre.
87
+
88
+ ```figdown
89
+ # FigDown — figures as text. Spec: https://github.com/FigDown/figdown
90
+ # ^ FIRST LINE of every document you write.
91
+ # Verbatim, em dash included. It is a comment:
92
+ # the parser ignores it. It is there because the
93
+ # file travels — into a wiki, a ticket, a pasted
94
+ # message — and tells whoever meets it there what
95
+ # the format is and where it is defined.
96
+ figdown 0.1 block # REQUIRED first significant line; comments and
97
+ # blanks may precede it. The genre is REQUIRED.
98
+ # A later `figdown 0.1 <genre>` starts a new
99
+ # section with its own genre; one file still
100
+ # renders to one SVG.
101
+ title "Some Title" # optional; the quotes are REQUIRED
102
+ # comments start with '#'; inside quotes the only escapes are \n \" \\
103
+ class hot "Congested path" stroke=#dc2626 # meaning + style, declared once
104
+ ```
105
+
106
+ **Quotes mark a STRING, and nothing else.** Every label is quoted, at every
107
+ position that has one — `title "TCP Header"`, `class c "meaning"`, and the
108
+ label of every block opener — because whitespace also separates arguments, so
109
+ a bare token cannot hold a phrase. Everything that is *not* a string is bare:
110
+ ids and references (`class=hot`), and every value drawn from a fixed list of
111
+ spellings (`style=dashed`, and the header's own `figdown 0.1 <genre>`).
112
+ Quoting one of those is a line error. Quoting a NUMBER or a point is not:
113
+ `width="90"` and `at="(1,2)"` are legal and mean exactly what the bare forms
114
+ mean.
115
+
116
+ **`class` is how meaning survives.** Colour and shape alone assert nothing a
117
+ reader can recover: a `class` states the meaning in text and the legend draws
118
+ itself from it. Anything you want a reader to *conclude* goes in a label or a
119
+ class meaning — never in a colour alone. A class with an empty meaning
120
+ (`class c ""`) deliberately claims nothing.
121
+
122
+ **Two paint channels, SVG's own**, legal on any element that has them:
123
+
124
+ | Key | What it paints |
125
+ |---|---|
126
+ | `fill=` | the **interior** of a shape — `#rgb`, `#rrggbb`, a CSS colour name, or `transparent` |
127
+ | `stroke=` | the **outline** of a shape, and the **whole** of a line |
128
+ | `style=` | `solid` \| `dashed` \| `dotted` |
129
+ | `class=` | one or more declared class ids, comma-separated: `class=hot,legacy` |
130
+
131
+ A line — an edge, a marker, a ring — has no interior, so `fill=` on one is an
132
+ error naming `stroke=`. There is **no label-colour key**: a label's colour is
133
+ derived from the background it sits on, so light text lands on dark fills by
134
+ itself.
135
+
136
+ **`layout` and `pin`** are the layout zone. Everything from a `layout` line
137
+ down is geometry that exists only to stabilise the `.svg`: it carries no
138
+ meaning, no genre may put its own semantics inside it, and `pin` is the only
139
+ directive legal there. Skip the zone when reading; see `reference/layout.md`
140
+ when writing.
141
+
142
+ ## Portability: two statuses, and the parser tells you nothing
143
+
144
+ Every keyword, option key and genre is either **NORMATIVE** (NORMATIVE — inside the
145
+ v0.1 conformance surface and its compatibility promise) or **EXPERIMENTAL**
146
+ (EXPERIMENTAL — the engine accepts it and your document keeps working, but it is
147
+ outside both, and may change or be withdrawn in a later `0.x`).
148
+
149
+ **The parser never warns**, so a line that parses tells you nothing about its
150
+ status — this is the only signal you get. Everything reachable *only* through
151
+ the right-hand column of the router is EXPERIMENTAL: use it when the figure needs
152
+ it, and say so beside the figure.
153
+
154
+ **Hybrid figures** (a scene and a table in one artifact): start a second
155
+ section with its own `figdown 0.1 <genre>` header rather than nesting one
156
+ genre's block inside another's.
157
+
158
+ ## Do not invent syntax
159
+
160
+ If something seems missing, compose it from what exists or tell the user.
161
+ Retired spellings are hard errors whose message names the replacement — read
162
+ the error instead of guessing around it.
163
+
164
+ ## Maintaining a document written against an older version
165
+
166
+ Line 1 pins the version, so never mix syntax generations in one file. When the
167
+ language moves, a **NORMATIVE** construct ships a mechanical rewrite rule and
168
+ you upgrade by applying the rules in order. An **EXPERIMENTAL** one carries no
169
+ such promise: it may be withdrawn with nothing that does what it did, in which
170
+ case the tooling reports and cannot rewrite, and a human decides what the
171
+ figure should say instead.
172
+
173
+ Full spec and docs: https://github.com/FigDown/figdown
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // FigDown Stage-0 sidecar generator (R14): X.fd -> X.svg
2
+ // FigDown sidecar generator: X.fd -> X.svg
3
3
  // Deterministic: same source -> same SVG, bit-level. The artifact embeds
4
- // its own source and a SHA-256 of it (spec draft section 7).
4
+ // its own source and a SHA-256 of it (spec core §7).
5
5
  // The engine is never forked: parser+renderer are extracted from the editor
6
6
  // single file at build time (the "regenerate, don't fork" rule).
7
7
  'use strict';
@@ -24,24 +24,32 @@ function loadEngine() {
24
24
  const start = h.indexOf('const SHAPES');
25
25
  const end = h.indexOf('// 3. UI');
26
26
  if (start < 0 || end < 0) throw new Error('cannot locate engine in ' + ENGINE);
27
- const factory = new Function(h.slice(start, end) + '\nreturn {parse, render};');
27
+ const factory = new Function(h.slice(start, end) + '\nreturn {parse, render, stackSectionSvgs, FIGDOWN_VERSION};');
28
28
  return factory();
29
29
  }
30
30
 
31
31
  function buildOne(engine, fdPath) {
32
32
  const src = fs.readFileSync(fdPath, 'utf8');
33
- const { doc, errs } = engine.parse(src);
33
+ const parsed = engine.parse(src);
34
+ const errs = parsed.errs || parsed.errors || [];
35
+ const docs = parsed.docs && parsed.docs.length ? parsed.docs : (parsed.doc ? [parsed.doc] : []);
34
36
  if (errs.length) {
35
37
  console.error(fdPath + ':');
36
38
  for (const e of errs) console.error(' ' + e);
37
39
  return false;
38
40
  }
39
- const svg = engine.render(doc, RENDER_OPTS).svg;
41
+ const svg = docs.length > 1
42
+ ? engine.stackSectionSvgs(docs.map(d => engine.render(d, RENDER_OPTS)))
43
+ : engine.render(docs[0], RENDER_OPTS).svg;
40
44
  const hash = crypto.createHash('sha256').update(src, 'utf8').digest('hex');
41
- // render options are recorded in the artifact so a third-party rebuild
42
- // (same source + same recorded options) stays bit-identical
45
+ // The artifact records three things (spec core §7): the SHA-256 OF THE
46
+ // SOURCE, the ENGINE VERSION that rendered it, and any non-default render
47
+ // option — so a third-party rebuild (same source + same engine version +
48
+ // same recorded options) stays bit-identical, and a diff between two
49
+ // renderings of one source has somewhere to point.
43
50
  const optAttr = RENDER_OPTS.title === true ? ' data-render-options="with-title"' : '';
44
- const meta = '<metadata id="figdown-source" data-sha256="' + hash + '"' + optAttr + '><![CDATA[\n'
51
+ const meta = '<metadata id="figdown-source" data-sha256="' + hash + '"'
52
+ + ' data-engine-version="' + engine.FIGDOWN_VERSION + '"' + optAttr + '><![CDATA[\n'
45
53
  + src.replace(/]]>/g, ']]]]><![CDATA[>') + '\n]]></metadata>';
46
54
  const artifact = svg.replace(/<\/svg>$/, meta + '</svg>');
47
55
  const out = fdPath.replace(/\.fd$/, '') + '.svg';
@@ -54,20 +62,47 @@ const argv = process.argv.slice(2);
54
62
  const RENDER_OPTS = {};
55
63
  const args = argv.filter(a => {
56
64
  if (a === '--with-title') { RENDER_OPTS.title = true; return false; }
57
- if (a === '--no-title') { return false; } // the default since 0.1-dev.12 (kept for compat)
65
+ if (a === '--no-title') { return false; } // already the default; accepted so old command lines keep working
58
66
  return true;
59
67
  });
60
68
  if (!args.length) {
61
69
  console.error('usage: node tools/build-svg.js [--with-title] <file.fd | dir> ...');
62
70
  process.exit(2);
63
71
  }
72
+ // RECURSIVE. "A gate that does not recurse is a gate that lies" was written
73
+ // for the read-only CHECKS, which read only the top level of a directory.
74
+ // The GENERATOR had the same defect and was missed:
75
+ // handed `examples/ figures/` it rebuilt 28 of the 66 artifacts and reported
76
+ // success on all 28, so a version bump left 38 artifacts recording an engine
77
+ // that no longer existed. `artifact-check.js` (which does recurse) caught it,
78
+ // which is the division of labour working — but the generator lying is how the
79
+ // stale artifacts got there in the first place, twice (§3.1(b)).
80
+ function collect(a, acc) {
81
+ const st = fs.statSync(a);
82
+ if (!st.isDirectory()) { acc.push(a); return acc; }
83
+ for (const name of fs.readdirSync(a).sort()) {
84
+ if (name === 'node_modules' || name === '.git') continue;
85
+ const p = path.join(a, name);
86
+ if (fs.statSync(p).isDirectory()) collect(p, acc);
87
+ else if (name.endsWith('.fd')) acc.push(p);
88
+ }
89
+ return acc;
90
+ }
91
+
64
92
  const engine = loadEngine();
65
- let fail = false;
93
+ let built = 0;
94
+ let failed = 0;
66
95
  for (const a of args) {
67
- const st = fs.statSync(a);
68
- const files = st.isDirectory()
69
- ? fs.readdirSync(a).filter(f => f.endsWith('.fd')).sort().map(f => path.join(a, f))
70
- : [a];
71
- for (const f of files) if (!buildOne(engine, f)) fail = true;
96
+ for (const f of collect(a, [])) { if (buildOne(engine, f)) built++; else failed++; }
72
97
  }
73
- process.exit(fail ? 1 : 0);
98
+ // State the count. §3.1(d): coverage is verified as a number, not assumed —
99
+ // compare against `find <dir> -name '*.fd' | wc -l`.
100
+ // `built` counts artifacts WRITTEN, never files attempted: a file whose source
101
+ // does not parse writes nothing, and counting it as built made the summary line
102
+ // contradict the errors printed above it and the exit code below it — in CI
103
+ // logs, and in the write->validate->fix loop this message exists to serve.
104
+ // A failure is therefore reported as its own number, and the two together are
105
+ // the file count to compare against.
106
+ console.log('built ' + built + ' artifact(s) from ' + args.length + ' path argument(s)'
107
+ + (failed ? ' — ' + failed + ' of ' + (built + failed) + ' .fd file(s) FAILED and wrote nothing' : ''));
108
+ process.exit(failed ? 1 : 0);