figdown 0.3.2 → 0.4.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/README.md +2 -0
- package/dist/figdown.js +1301 -54
- package/dist/figdown.mjs +1301 -54
- package/examples/evpn-fabric.svg +1 -1
- package/examples/showcase/arp-resolution.svg +3 -2
- package/examples/showcase/ethernet-frame.svg +1 -1
- package/examples/showcase/l2-forwarding-logic.svg +1 -1
- package/examples/showcase/tcp-handshake.svg +4 -3
- package/examples/showcase/tcp-header.svg +1 -1
- package/examples/showcase/tcp-state-machine.svg +1 -1
- package/guide/expressing.md +33 -6
- package/guide/layout.md +54 -10
- package/guide/showcase.md +51 -15
- package/integrations/mcp-server/README.md +11 -5
- package/integrations/mcp-server/server.js +19 -3
- package/package.json +2 -1
- package/skill/figdown/SKILL.md +20 -9
- package/skill/figdown/build-svg.js +16 -2
- package/skill/figdown/figdown.html +1491 -69
- package/skill/figdown/reference/experimental/flowchart.md +6 -4
- package/skill/figdown/reference/experimental/sequence.md +252 -0
- package/skill/figdown/reference/experimental/statechart.md +5 -3
- package/skill/figdown/reference/experimental/topology.md +6 -4
- package/skill/figdown/reference/reading.md +14 -9
- package/skill/figdown/reference/scene.md +8 -3
package/guide/expressing.md
CHANGED
|
@@ -13,8 +13,10 @@ portable figures: `block`, `bitfield`, `table` (plus the `UNIVERSAL-CORE-KEYWORD
|
|
|
13
13
|
Constructs marked EXPERIMENTAL — `threshold`, `band`, `bundle`,
|
|
14
14
|
`chart`, option keys
|
|
15
15
|
`extend=`/`data=`,
|
|
16
|
-
and genres `topology`, `flowchart`, `timing` (`CONSTRUCT-STATUS-TIERS`, spec §10)
|
|
17
|
-
|
|
16
|
+
and genres `topology`, `flowchart`, `timing` (`CONSTRUCT-STATUS-TIERS`, spec §10), plus the two
|
|
17
|
+
that arrived later on the same footing, `statechart` (`STATECHART-GENRE-SCOPE`, needs
|
|
18
|
+
`figdown 0.2`) and `sequence` (`SEQUENCE-GENRE-VOCABULARY`, needs `figdown 0.4`) — still parse
|
|
19
|
+
and are not deprecated, but they sit **outside** the v0.1 compatibility promise
|
|
18
20
|
and may change without a migration entry. **Do not use them when the figure
|
|
19
21
|
must be portable.** `path` and `routing` (with `points=`, `tailport=`,
|
|
20
22
|
`headport=`, `routing=`) were on that list until 0.1, when `EDGE-GEOMETRY-CONSTRUCTS`
|
|
@@ -49,7 +51,7 @@ is one place to look and nothing to reconcile.
|
|
|
49
51
|
|---|---|---|
|
|
50
52
|
| containment — node belongs inside a box | `group g "Label"` + `node n "…" in=g` | **`block` and `topology` only** — withdrawn from `flowchart` and `statechart` at 0.3 (`SCENE-KEYWORD-MEMBERSHIP`): no figure in the tree wrote one, and UML's word for the concept is *composite state*, not `group`. **The option key `in=` followed at 0.3 (`MEMBERSHIP-KEY-ACCEPTANCE`)** and is a named line error in those two genres: it named a `group` id and nothing else, so the `SCENE-KEYWORD-MEMBERSHIP` withdrawal left every value a dead end. Under `statechart` its spelling is additionally **RESERVED** — `in=` returns there with a `state`-id domain if UML 2.5.1 §14.2.3.4 composite states are earned. One level of nesting only (spec §2.2); for deeper, represent the inner group as a proxy node |
|
|
51
53
|
| set membership / category (color + legend) | `class c "meaning" fill=… style=…` + `class=c` on members | legend derives automatically; bare `fill=` carries no named meaning |
|
|
52
|
-
| one class used on both nodes and edges | `class c "meaning" fill=… stroke=…` — BOTH keys on the one class | Since 0.1 (`INTERIOR-LESS-ELEMENT-PAINT`) the rule is per CHANNEL: `fill=` paints members that have an interior (a node box) and is inapplicable to an edge, which has none; `stroke=` paints the edge line and a node's outline; `style=` applies to both. So one class still carries one meaning for both kinds of member — do NOT split it. A class an edge joins
|
|
54
|
+
| one class used on both nodes and edges | `class c "meaning" fill=… stroke=…` — BOTH keys on the one class | Since 0.1 (`INTERIOR-LESS-ELEMENT-PAINT`) the rule is per CHANNEL: `fill=` paints members that have an interior (a node box) and is inapplicable to an edge, which has none; `stroke=` paints the edge line and a node's outline; `style=` applies to both. So one class still carries one meaning for both kinds of member — do NOT split it. A class an edge joins must not declare `fill=` without `stroke=` (`INTERIOR-LESS-ELEMENT-PAINT`): on a line those two name the SAME channel, so the edge would otherwise lose its colour silently, and a `style=` beside the `fill=` does not answer what the author asked for. Declaring NO paint at all is legal on every member (`CLASS-CHANNEL-REACH`) — the class claims a meaning, the derived legend draws it with no swatch, and the edge keeps its default line. `fill=`, `stroke=` and `style=` are all NORMATIVE since 0.1 (`STROKE-KEY-STATUS`) |
|
|
53
55
|
| hierarchy / tree | directed `edge` chain; `flow down` to orient | the edges carry the tree; group is for spatial containment, not hierarchy |
|
|
54
56
|
| adjacency without a link | `in=` on the same `group`, no `edge` between them | the shared frame communicates co-location |
|
|
55
57
|
| cross-cutting category spanning groups | `class` + `class=` on elements in different groups | one class can mark nodes, edges, and fields across the whole document |
|
|
@@ -85,7 +87,8 @@ is one place to look and nothing to reconcile.
|
|
|
85
87
|
| event → action | `edge src -[event]-> tgt` | mid-label is the trigger; head-label can name the action |
|
|
86
88
|
| precedence / partial order | a DAG of directed edges | absence of an edge means no stated constraint |
|
|
87
89
|
| fan-out / fan-in | edges from/to a common node | AND-vs-XOR join discipline: declare a `class` (`FLOWCHART-GENRE-DESIGN` — no first-class gateway yet) |
|
|
88
|
-
| message exchange between parties |
|
|
90
|
+
| message exchange between parties, in time order | `figdown 0.4 sequence` — one `lifeline` per party, one `message` per exchange; the ladder's row order **is** declaration order | **The genre LANDED at 0.4 (`SEQUENCE-GENRE-VOCABULARY`), and this row's old advice retires with it**: nodes-as-parties plus `1:`/`2:` ordinal labels was the interim, and ordinals are naming, not semantics (`MEANING-RECOVERY-SOURCE`). Order is now structural — every message gets its own row, so no two share a span and nothing rides on a numbering convention. `state` puts a participant's condition on that participant's own lifeline, between two messages; `fragment` + `operand` say what kind of run a group of messages is (twelve UML operators, `type=` mandatory). It is **EXPERIMENTAL** and requires `figdown 0.4`, so a figure that must stay portable still writes the scene interim — and now does so as a *choice* it can state, not as a lack. See [spec/genres/experimental/sequence.md](../spec/genres/experimental/sequence.md) and [examples/sequence/](../examples/sequence/index.md) |
|
|
91
|
+
| a participant's condition at a point in an exchange | under `sequence`: `state <lifeline-id> "BOUND"`, written between the two messages it sits between (add `in=<fragment\|operand>` to put it inside a frame) | slot 1 **references** a lifeline and declares nothing; the quoted state name is mandatory. Row order places it — there is no time coordinate to write. This is the fact a scene genre plus a companion `table` could only carry as prose (`examples/showcase/tcp-handshake.fd`'s state table). One limit to know before you rely on it: a lifeline's state occurrences are **one sequence**, so two mutually exclusive operands cannot both end in a drawn `INIT` |
|
|
89
92
|
| a label too wide for its diamond / ellipse / cylinder | nothing — shapes size themselves from their **inscribed** area | do not declare an extent; see the note below |
|
|
90
93
|
|
|
91
94
|
> **Delete declared extents added to make a shape fit its text.** Non-rectangular
|
|
@@ -105,6 +108,29 @@ is one place to look and nothing to reconcile.
|
|
|
105
108
|
> document that still carries a `size` line gets a named migration diagnostic,
|
|
106
109
|
> not `unrecognized line`.
|
|
107
110
|
|
|
111
|
+
> **Choosing between `sequence` and `statechart` when many lines run between
|
|
112
|
+
> the same two blocks.** The symptom is identical in every scene genre —
|
|
113
|
+
> `block`, `flowchart` and `statechart` all crowd parallel edges into the one
|
|
114
|
+
> span between two boxes, and the reader sees confused overlap. What settles
|
|
115
|
+
> it is *what those lines are*:
|
|
116
|
+
>
|
|
117
|
+
> - **Time-ordered messages between one pair of participants → `sequence`.**
|
|
118
|
+
> The ladder spreads time down the page, so ten exchanges between A and B
|
|
119
|
+
> are ten rows on two lifelines and no two share a span. A figure like that
|
|
120
|
+
> is already a sequence; the scene genre is only where it was forced to
|
|
121
|
+
> live.
|
|
122
|
+
> - **Distinct transitions between states → `statechart`, and fix the
|
|
123
|
+
> layout.** Many edges returning to one state are different triggers with
|
|
124
|
+
> different meanings, not messages in an order. That crowding is a **layout**
|
|
125
|
+
> problem, not a genre problem, and the answer is
|
|
126
|
+
> [layout.md](layout.md), not a new header line.
|
|
127
|
+
>
|
|
128
|
+
> In one sentence: *are these lines messages between one pair over time, or
|
|
129
|
+
> transitions between states?* [`examples/sequence/dhcp-lease.fd`](../examples/sequence/dhcp-lease.fd)
|
|
130
|
+
> and [`examples/statechart/dhcp-client.fd`](../examples/statechart/dhcp-client.fd)
|
|
131
|
+
> are the same protocol answered both ways, which is the cheapest way to see
|
|
132
|
+
> the difference.
|
|
133
|
+
|
|
108
134
|
---
|
|
109
135
|
|
|
110
136
|
## Data & format
|
|
@@ -135,7 +161,7 @@ is one place to look and nothing to reconcile.
|
|
|
135
161
|
| threshold / watermark | `threshold "label" in=g offset=N%` on a `group` or `node` | spelled `guide` until 0.1 (`THRESHOLD-KEYWORD-SPELLING`). Label and the `%` are both mandatory; there is no `value=` and no `ref=` — the reference lives in the label (`THRESHOLD-VALUE-SCOPE`). `threshold` and `band` take the same two scopes (`AUTHORING-INTENT-OVER-RENDERING`). **EXPERIMENTAL** (`CONSTRUCT-STATUS-TIERS`), and **`block` only** since 0.3 (`SCENE-KEYWORD-MEMBERSHIP`). Even there, mind the irony the withdrawal turns on: in QoS a threshold is a queue depth **with a numeric value** (RFC 2309 `minth`/`maxth`, RFC 7567 — the very RFCs `THRESHOLD-KEYWORD-SPELLING` took the spelling from), while this one has no `value=` and its `offset=` is a fraction of the target's rendered extent, not a quantity |
|
|
136
162
|
| quantity comparison | `table` with numeric columns | `▁▃▅▇` Unicode blocks as sparklines in cells (`TABLE-SPARKLINE`) |
|
|
137
163
|
| signal values over time | `timing id "label"` + `signal name chars` (one char = one cycle) | lane alphabet: `0 1 p n x = .` (a strict subset of WaveDrom's; `2`–`9` retired at 0.1). `timing` is an **EXPERIMENTAL** genre (`CONSTRUCT-STATUS-TIERS`, spelled `wave` until 0.1) — the alphabet is settled, the surface around it is not |
|
|
138
|
-
| event ordering without exact times |
|
|
164
|
+
| event ordering without exact times | `figdown 0.4 sequence` — `message` declaration order **is** the order, and there are no times anywhere in the genre | vertical distance on a ladder carries no duration, so a delay or a timer belongs in the message label as prose. Under a scene genre the ordinal mid-label (`-[1: SYN]->`) remains the interim, and it remains a naming convention rather than semantics (`MEANING-RECOVERY-SOURCE`) — number consistently and say in a comment that you did |
|
|
139
165
|
| visual code / legend | `class` — legend derives automatically from declaration order | each `class` line gives swatch + meaning text |
|
|
140
166
|
| annotation explaining why — prose the **human** must see | `note="…"` on the element's OWN line: `node a "A" note="…"`, `group g "G" note="…"`, `edge a -> b note="…"`, `title "T" note="…"` (the figure-level one) | `figdown 0.3` (`DRAWN-ANNOTATION-FORM`). Attachment is by **syntactic position** — no id, no target key, so no ambiguity about which of several identically-labelled elements is meant. Not `description=`: the two divide by AUDIENCE — `description=` reaches the reading agent as an SVG `<title>` and puts **no ink** on the page, `note=` always draws. Both on one element is legal; neither is a fallback for the other. **You do not place the box** (`DOMAIN-CONVENTION-DIRECTIVES`): no `at=`, no `side=`; the engine sits it beside its carrier and takes a leader line only when adjacency fails. Refused on `field` (use `description=`) and on `cell`/`external`/`threshold`/`band`/`bundle`/`class` — zero measured demand (`plane` was on that list until `PAINT-ORDER-CONSTRUCT` withdrew the keyword itself). **Where a typed slot exists, a note is never the right answer**: a category is a `class` meaning, a containment is `in=`, a field's condition is `present=` |
|
|
141
167
|
| cross-references within a scene | `edge` + `class` naming the relation | can't reference a table cell or bitfield field — see Known limits `CELL-EDGE-ANCHORS` / `CROSS-BLOCK-REFERENCES` |
|
|
@@ -149,7 +175,8 @@ is one place to look and nothing to reconcile.
|
|
|
149
175
|
Each entry: what cannot be expressed today · OQ reference · sanctioned interim workaround.
|
|
150
176
|
|
|
151
177
|
- **same entity in two views** — no way to assert two nodes are the same participant; OQ pending; interim: shared `class` + a note stating the identity.
|
|
152
|
-
-
|
|
178
|
+
- ~~**strict message ordering**~~ — **SOLVED (`SEQUENCE-GENRE-VOCABULARY`)** by the `sequence` genre. A message's place in the ladder's row order *is* its place in time, so nothing rides on `1:`/`2:` label ordinals and a reading agent no longer answers *three links join the client and the server* where the truth is one association carrying three segments in time. **`MESSAGE-ORDER-AND-STATE` (spec §9) is CLOSED** — the closing condition it stated, *a genre landing that brings a ladder layout path with it*, is the one that was met, and the question is kept whole under its closure note. What genuinely remains open is not the genre's absence but **which surface a portable figure may use**: `sequence` is EXPERIMENTAL and requires `figdown 0.4`, so the two figures `MESSAGE-ORDER-AND-STATE` cites as evidence (`examples/showcase/tcp-handshake.fd`, `examples/showcase/arp-resolution.fd`) deliberately stay `figdown 0.1 topology` + a companion `table` and keep the ordinal interim behind an honest-limit comment. Interim, unchanged, for any figure that must stay on `figdown 0.1`: number labels consistently (`1: SYN`, `2: SYN-ACK`) and carry per-participant state in a second section.
|
|
179
|
+
- **the residual limits of `sequence` itself**, stated because the genre landing did not make them go away. A `message` is **one instant** — no separate send and receive, so no propagation delay and no two messages crossing on the wire. A `par` cannot re-order one chosen pair. A lifeline's `state` occurrences are **one sequence**, so two mutually exclusive operands cannot both end in a drawn `INIT`; the second carries the fact in `description=` instead. `ignore`/`consider` message sets and `loop` bounds live in the frame's label as prose a reader can quote and a parser cannot read, because `fragment` has no argument slot. And a **meaning-only `class`** — the sanctioned idiom for a message sent and never delivered, after `lost=` was refused (`UNDELIVERED-MESSAGE-MARKING`) — puts no ink on the page: the `.fd` reader learns which message was dropped and the `.svg` reader cannot. Every one of these is stated in the sources under `examples/sequence/`; the drawing-side ones are filed in decisions/registry.md.
|
|
153
180
|
- **cell anchors** — an `edge` cannot target a `table` cell or `bitfield` field; `CELL-EDGE-ANCHORS`; interim: whole-table relation + cell name in the edge label.
|
|
154
181
|
- **cross-block references** — no locator from one typed block to another, and no way to declare a composed region subordinate to a host element ("this table is about node X"); `CROSS-BLOCK-REFERENCES`; interim: prose note or a linking `edge` between the host nodes.
|
|
155
182
|
- **a repeat COUNT that names another field** — `index=` says a `bitfield` field repeats and gives the range, but the last index can only be prose when the count lives in another field (`index="0..Last Entry"`), because no value in the language may name a field; `BITFIELD-REPETITION-CONSTRUCT`'s surviving half, downstream of the locator problem `ANNOTATION-LOCATOR-SPLIT`; interim: write the prose end — the run is then honestly indeterminate, which is the correct reading, and say so in a `description=` or a `class` meaning.
|
package/guide/layout.md
CHANGED
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
> (`block`, `bitfield`, `table`) when the figure must be portable — see
|
|
13
13
|
> [authoring.md](authoring.md).
|
|
14
14
|
>
|
|
15
|
+
> **One genre this guide does not reach: `sequence`.** A ladder's two axes are
|
|
16
|
+
> both declaration order — columns are `lifeline` order, rows are
|
|
17
|
+
> `message` ∪ `state` order — so `flow` and `rank` are not words in that genre
|
|
18
|
+
> at all, and a `pin` parses and moves nothing. There is no rung to climb
|
|
19
|
+
> there: the edit is always to the source order. Everything below is about
|
|
20
|
+
> **scene** sections and the typed blocks beside them.
|
|
21
|
+
>
|
|
15
22
|
> Lessons come from field observation of a downstream authoring pass.
|
|
16
23
|
|
|
17
24
|
## 1. The two-zone mindset
|
|
@@ -58,7 +65,8 @@ define, redefine or extend a keyword inside the zone; `GENRE-VOCABULARY-OBLIGATI
|
|
|
58
65
|
words", does not reach in. That is what makes the default safe *for ever* and
|
|
59
66
|
what makes it usable: ONE enumeration of the members is correct under every
|
|
60
67
|
genre, so a reader may apply it without even resolving the header's genre
|
|
61
|
-
token. That enumeration is **core §10 (a′)**, it is NORMATIVE, and
|
|
68
|
+
token. That enumeration is **core §10 (a′)**, it is NORMATIVE, and
|
|
69
|
+
it has exactly **one** member — `pin`, NORMATIVE. `layout` is not a
|
|
62
70
|
member: it is the zone's OPENER and lives in the universal core of three
|
|
63
71
|
(§10 (a)) alongside `figdown` and `title`. The withdrawal of `path` and
|
|
64
72
|
`routing` left `LAYOUT-ZONE-NAMESPACE` whole and took its only experimental members with it.) If a
|
|
@@ -103,8 +111,11 @@ as the figure reads clearly.** The lowest rung that works is the right choice.
|
|
|
103
111
|
| 3 | `pin … at=(x,y)` / `pin … width= height=` | Topology/spatial where placement IS the message. One directive, three optional keys: `at=` places (nodes, groups, `external` endpoints), `width=`/`height=` extend (**nodes only** — groups, external endpoints and typed blocks size to their content). `size` was retired into `pin` at 0.1 (`ELEMENT-GEOMETRY-DIRECTIVE`). **This is the top rung** |
|
|
104
112
|
|
|
105
113
|
**Rung 1 is where authoring should begin** for **scene** sections (`block`,
|
|
106
|
-
or experimental `topology` / `flowchart`). A pure `bitfield` or
|
|
107
|
-
section usually has no `flow`/`rank`/`pin` — geometry follows content
|
|
114
|
+
or experimental `topology` / `flowchart` / `statechart`). A pure `bitfield` or
|
|
115
|
+
`table` section usually has no `flow`/`rank`/`pin` — geometry follows content,
|
|
116
|
+
and a `sequence` section has no rung at all (see the note at the top of this
|
|
117
|
+
guide): both its axes are declaration order, so the whole ladder is the
|
|
118
|
+
engine's and the only edit available is to the source order. For a
|
|
108
119
|
scene section, a `flow` line costs one line and gives the layout engine the
|
|
109
120
|
single most useful piece of intent it can receive. "Write nothing" (rung 0)
|
|
110
121
|
is reasonable only for a figure small enough that direction is obvious — or
|
|
@@ -329,11 +340,27 @@ like at scale. When the graph has a cycle, go to §9 and arrange it: explicit
|
|
|
329
340
|
arrangement is the expected cost of a cyclic figure, not a workaround for a
|
|
330
341
|
defect.
|
|
331
342
|
|
|
343
|
+
**A crowd of parallel edges may not be a layout problem at all — check the
|
|
344
|
+
genre before you tune.** When many lines run between the SAME pair of blocks, a
|
|
345
|
+
scene genre has one span to fan them into and no rung of the ladder changes
|
|
346
|
+
that: the crowding is in the shape of the figure, not in its arrangement. What
|
|
347
|
+
settles it is what those lines *are*. If they are **time-ordered messages
|
|
348
|
+
between two participants**, the figure is a ladder wearing a scene genre —
|
|
349
|
+
`sequence` (`figdown 0.4`, EXPERIMENTAL) gives every message its own row, so no
|
|
350
|
+
two share a span and nothing has to be tuned. If they are **distinct
|
|
351
|
+
transitions between states** — different triggers with different meanings,
|
|
352
|
+
several of them returning to one state — it is a `statechart` and the crowding
|
|
353
|
+
really is a layout problem, which is what §9 is for.
|
|
354
|
+
`examples/statechart/bfd-session.fd` is the second case and stays where it is;
|
|
355
|
+
`examples/sequence/dhcp-lease.fd` is the first, drawn as the ladder it always
|
|
356
|
+
was. Same protocol as `examples/statechart/dhcp-client.fd`, deliberately, so
|
|
357
|
+
the two answers can be compared on one subject.
|
|
358
|
+
|
|
332
359
|
## 7. Before / after: the same semantics, different layout zones
|
|
333
360
|
|
|
334
361
|
The first two entries are pairs, byte-identical in the content zone. The third is the counter-case: no pair, because the fix was not in the layout zone at all. In both pairs the **tuned** side is the top-level example itself (`examples/evpn-fabric.fd`, `examples/srl-evpn-irb.fd`) — only the *auto* variant needs its own file, since the tuned figure is the one the corpus already ships. 0.1 removed the duplicate copies that used to sit under `layout-compare/`; they were byte-identical to the originals and taught nothing a second time. Score = `cross×2 + thru×3 + novlp×3 + lblcol×2 + coinc×2`.
|
|
335
362
|
|
|
336
|
-
**Lint scores are a smoke alarm, not a judge.** The srl-evpn-irb
|
|
363
|
+
**Lint scores are a smoke alarm, not a judge.** The srl-evpn-irb entry below shows lint getting *worse* (ink/e 109→117) while the figure becomes dramatically more readable — and its auto arm has since stopped rendering at all, which no lint score would ever have told you — because the metrics do not measure group containment, overlap, or reading order. Always look at the render; the ladder ends when a human can read it. One variant of that example scored better on lint but had lost its column alignment — and the alignment was the peer signal; only looking at the render caught it.
|
|
337
364
|
|
|
338
365
|
### Leaf-spine fabric — `+flow down +rank sp1 sp2 +rank lf1 lf2 lf3`
|
|
339
366
|
|
|
@@ -346,16 +373,31 @@ Eight-node VXLAN/EVPN topology. Auto-layout scatters spines and leaves; three se
|
|
|
346
373
|
|
|
347
374
|
[auto .fd](../examples/layout-compare/evpn-fabric-auto.fd) · [auto .svg](../examples/layout-compare/evpn-fabric-auto.svg) · [tuned .fd](../examples/evpn-fabric.fd) · [tuned .svg](../examples/evpn-fabric.svg)
|
|
348
375
|
|
|
349
|
-
### srl-evpn-irb — two-level pins + groups (20 layout lines)
|
|
376
|
+
### srl-evpn-irb — two-level pins + groups (20 layout lines), and an auto arm that does not render
|
|
377
|
+
|
|
378
|
+
Sixteen-node EVPN-VXLAN IRB figure with three leaf groups. The tuned version pins each group as a layout module (spec §3 `PIN-COORDINATE-SCOPE`: a pinned group anchors its local origin in canvas px; members are group-local), giving three clean leaf boxes under the fabric overlay (1228×656 px).
|
|
379
|
+
|
|
380
|
+
**The auto arm has no artifact, and that is the result.** The engine guarantees that a `group` band contains only that group's members — the reader's rule is *inside the box is in the group*, so a band around a non-member states a membership the source never wrote. **Auto-layout cannot place these three groups' members contiguously.** Each leaf group's members land on different ranks, with the other groups' members and five hosts interleaved between them, so the band that has to enclose `leaf4`'s members ends up spanning most of the canvas — and no position the separation pass can reach clears every band at once. Reordering the source does not help: the ranks come from the fabric edges, not from the order the nodes are declared in. The engine therefore refuses the figure rather than drawing it:
|
|
381
|
+
|
|
382
|
+
```
|
|
383
|
+
$ node tools/build-svg.js examples/layout-compare/srl-evpn-irb-auto.fd
|
|
384
|
+
examples/layout-compare/srl-evpn-irb-auto.fd:
|
|
385
|
+
Line 25: group "leaf4" would enclose non-member "h2" and the layout pass could not separate them; the figure is not drawn rather than drawn wrongly. Give "h2" a pin outside the group, or add it with in=leaf4.
|
|
386
|
+
built 0 artifact(s) from 1 path argument(s) — 1 of 1 .fd file(s) FAILED and wrote nothing
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
(Which non-member gets named can differ between runs — several are enclosed, and the message reports the ones the pass gave up on. The refusal itself is the stable part.)
|
|
390
|
+
|
|
391
|
+
So the pair no longer compares two pictures. **It compares a picture with a refusal, and that is the sharper lesson:** for this topology the pinned arm is not a polish pass over a working auto layout, it is the only layout that exists. `pin` is what makes the figure renderable at all, and it stays that way until auto-layout learns **group-aware rank assignment** — placing a group's members in adjacent ranks so contiguous clustering can succeed (engine-backlog item 32). The `.fd` stays in the corpus as the one figure the containment guarantee cannot place; `node tools/artifact-check.js` reports it as `geometry-refused` and treats its *missing* `.svg` as the correct state.
|
|
350
392
|
|
|
351
|
-
|
|
393
|
+
The measurements below are the last ones taken before the refusal, and they are why the ratio mattered: lint ink/e got *worse* (109→117) while the figure became unambiguous, because the metrics never measured group containment or overlap — the very property the engine now enforces outright.
|
|
352
394
|
|
|
353
395
|
| variant | cross | novlp | ink/e | score |
|
|
354
396
|
|---------|-------|-------|-------|-------|
|
|
355
|
-
| auto
|
|
397
|
+
| auto (no longer renders) | 0 | 1 | 109 | 3 |
|
|
356
398
|
| `+pin` ×20 | 0 | 1 | 117 | 3 |
|
|
357
399
|
|
|
358
|
-
[auto .fd](../examples/layout-compare/srl-evpn-irb-auto.fd)
|
|
400
|
+
[auto .fd](../examples/layout-compare/srl-evpn-irb-auto.fd) (refused — no artifact) · [tuned .fd](../examples/srl-evpn-irb.fd) · [tuned .svg](../examples/srl-evpn-irb.svg)
|
|
359
401
|
|
|
360
402
|
### vxlan-encap — when layout tuning is the wrong fix (0 layout lines)
|
|
361
403
|
|
|
@@ -479,8 +521,10 @@ class that mutes the bundle so it recedes — `class discard "Discard reasons"
|
|
|
479
521
|
stroke=#b8b6b0 style=dashed` on the terminal-bound edges, used to good effect
|
|
480
522
|
by a corpus author. An edge is a line with no interior, so `stroke=` is the
|
|
481
523
|
channel that paints it; `fill=` paints only members that have an interior, and
|
|
482
|
-
a
|
|
483
|
-
`INTERIOR-LESS-ELEMENT-PAINT
|
|
524
|
+
a class joined by an edge that declares `fill=` with no `stroke=` is a line
|
|
525
|
+
error (spec/core.md §5, `INTERIOR-LESS-ELEMENT-PAINT`). A class that declares no paint at all is legal
|
|
526
|
+
— it claims a meaning and the edge keeps its default line — so mute the
|
|
527
|
+
bundle by writing the channel you want, not by omitting all of them.
|
|
484
528
|
|
|
485
529
|
**Use `rank` for a lateral bypass, not for the mainline.** Under `flow down` a
|
|
486
530
|
`rank` shares a *row*, so ranking the main chain flattens the figure sideways.
|
package/guide/showcase.md
CHANGED
|
@@ -25,6 +25,17 @@ carries it machine-readably, rather than letting it ride on geometry or
|
|
|
25
25
|
absence. Nested `table` under a single scene header is legacy, not the taught
|
|
26
26
|
path.
|
|
27
27
|
|
|
28
|
+
**A seventh figure family is missing from this page on purpose.** The
|
|
29
|
+
`sequence` genre (`SEQUENCE-GENRE-VOCABULARY`) draws the interaction ladders these
|
|
30
|
+
six cannot: time-ordered messages between participants, with each
|
|
31
|
+
participant's state on its own column and framed runs saying what kind of run
|
|
32
|
+
they are. It is **EXPERIMENTAL** and requires `figdown 0.4`; nothing below
|
|
33
|
+
declares a version later than `figdown 0.2`, which is where a reader deciding
|
|
34
|
+
whether to adopt the language actually has to judge it. Its two worked figures
|
|
35
|
+
are collected in
|
|
36
|
+
[examples/sequence/index.md](../examples/sequence/index.md), and the honest-limit
|
|
37
|
+
notes under §2 and §5 say exactly what they buy.
|
|
38
|
+
|
|
28
39
|
---
|
|
29
40
|
|
|
30
41
|
## 1. TCP header — bit-exact machine-readable layout (`bitfield`)
|
|
@@ -112,7 +123,8 @@ This is a **multi-section hybrid**: a `topology` scene section *and* a
|
|
|
112
123
|
9293 figure carries the per-endpoint state on the lifelines; the topology
|
|
113
124
|
genre has no lifeline-state construct, so the companion table section carries
|
|
114
125
|
those states machine-readably — multi-section composition is the main-standard
|
|
115
|
-
|
|
126
|
+
answer, and it is what a `figdown 0.1` document still writes now that the
|
|
127
|
+
`sequence` genre has landed (see the note below this section).
|
|
116
128
|
|
|
117
129
|
**Human sees:** two endpoints and three numbered segments — SYN out, SYN-ACK
|
|
118
130
|
back, ACK (with piggybacked data) out — with a state table below tracking each
|
|
@@ -129,10 +141,22 @@ endpoint's TCP state segment by segment.
|
|
|
129
141
|
row 2 of the state table (`1: SYN →`, Server state). Answerable only because
|
|
130
142
|
the companion table carries the RFC 9293 lifeline states.
|
|
131
143
|
|
|
132
|
-
> **Honest limit (stated in the source)
|
|
133
|
-
> label ordinals, not a first-class
|
|
134
|
-
>
|
|
135
|
-
>
|
|
144
|
+
> **Honest limit (stated in the source), and what changed under it.** Message
|
|
145
|
+
> *order* here rides on the 1/2/3 label ordinals, not on a first-class
|
|
146
|
+
> construct — `MEANING-RECOVERY-SOURCE` does not treat numbering as semantics — and per-endpoint
|
|
147
|
+
> state rides on the companion table. **The `sequence` genre landed at
|
|
148
|
+
> 0.4 (`SEQUENCE-GENRE-VOCABULARY`) and expresses both structurally**: a `message`'s place
|
|
149
|
+
> in the ladder's row order *is* its place in time, and a `state` occurrence
|
|
150
|
+
> sits on a lifeline between the two messages it falls between. This figure
|
|
151
|
+
> nonetheless stays as written, and the reason is worth stating: `sequence` is
|
|
152
|
+
> **EXPERIMENTAL** and requires `figdown 0.4`, while this figure is a
|
|
153
|
+
> `figdown 0.1` document that any released engine can render. So the
|
|
154
|
+
> interim above is now a **choice this document can defend**, not a gap it is
|
|
155
|
+
> waiting on. What the ladder does with an exchange of this shape is shown by
|
|
156
|
+
> the genre's own figures, collected in
|
|
157
|
+
> [examples/sequence/index.md](../examples/sequence/index.md) — a DHCP lease from
|
|
158
|
+
> acquisition to release, and all twelve interaction operators. See also
|
|
159
|
+
> [expressing.md](expressing.md), "message exchange between parties".
|
|
136
160
|
|
|
137
161
|
---
|
|
138
162
|
|
|
@@ -287,8 +311,12 @@ prose rather than on an edge of its own.
|
|
|
287
311
|
cache table (`before the exchange` → `(no entry for B's IP)`). Answerable only
|
|
288
312
|
because the companion table carries the before-state explicitly.
|
|
289
313
|
|
|
290
|
-
> **Honest limit
|
|
291
|
-
>
|
|
314
|
+
> **Honest limit, and a stated choice rather than a lack:**
|
|
315
|
+
> step order (1/2) rides on label ordinals, which `MEANING-RECOVERY-SOURCE` treats as naming and not
|
|
316
|
+
> as semantics. The `sequence` genre now expresses it structurally — see §2's
|
|
317
|
+
> note and [examples/sequence/](../examples/sequence/index.md) — but it is
|
|
318
|
+
> EXPERIMENTAL and needs `figdown 0.4`, and this figure stays on the frozen
|
|
319
|
+
> `figdown 0.1` surface on purpose.
|
|
292
320
|
|
|
293
321
|
---
|
|
294
322
|
|
|
@@ -419,14 +447,22 @@ arrow carrying its `event / action`.
|
|
|
419
447
|
|
|
420
448
|
## The honest limits, in one paragraph
|
|
421
449
|
|
|
422
|
-
Two figures depend on a convention
|
|
423
|
-
**message/step ordering** in the handshake and ARP is carried by
|
|
424
|
-
labels `1:`/`2:`/`3:`, which `MEANING-RECOVERY-SOURCE` treats as naming, not
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
450
|
+
Two figures depend on a convention `figdown 0.1` does not make first-class:
|
|
451
|
+
**message/step ordering** in the handshake and ARP is carried by
|
|
452
|
+
numbering edge labels `1:`/`2:`/`3:`, which `MEANING-RECOVERY-SOURCE` treats as naming, not
|
|
453
|
+
semantics, and the RFC 9293 handshake's **per-endpoint lifeline state**
|
|
454
|
+
(LISTEN, SYN-SENT, …) rides in a companion `table` section because the topology
|
|
455
|
+
genre has no construct for it. **The language itself has stopped lacking
|
|
456
|
+
both.** The `sequence` genre landed (`SEQUENCE-GENRE-VOCABULARY`) with a ladder
|
|
457
|
+
layout of its own: row order *is* time order, and a `state` occurrence sits on
|
|
458
|
+
its lifeline between the two messages it falls between. What keeps these two
|
|
459
|
+
figures as they are is not absence but **status** — `sequence` is EXPERIMENTAL
|
|
460
|
+
and requires `figdown 0.4`, and both figures are `figdown 0.1` documents any
|
|
461
|
+
released engine can render. So both limits are now stated choices, and the
|
|
462
|
+
genre's own worked figures are at
|
|
463
|
+
[examples/sequence/index.md](../examples/sequence/index.md). `MESSAGE-ORDER-AND-STATE` in spec §9
|
|
464
|
+
is CLOSED — the landing it asked for is the one that happened —
|
|
465
|
+
with the question kept whole under the closure note.
|
|
430
466
|
The Ethernet frame uses the **`BYTE-UNIT-PACKET-BLOCKS`** byte-unit workaround (single-row `table`,
|
|
431
467
|
not `bitfield`) so byte order rides on cell order, and one of its facts — the
|
|
432
468
|
FCS **coverage span** over a contiguous run of columns (DA through Payload) —
|
|
@@ -87,7 +87,7 @@ particular there is no layout-lint tool and no "render to PNG".
|
|
|
87
87
|
|---|---|
|
|
88
88
|
| `figdown_build` | Turn one `.fd` into the deterministic, self-carrying SVG. The artifact embeds its own source, that source's SHA-256 and the engine version (core §7), so it round-trips back to text. |
|
|
89
89
|
| `figdown_check` | Parse one document *or walk a whole tree*, render nothing, write nothing. This is the write → validate → fix loop and the corpus sweep; it exists separately from `build` so that iterating on a broken file costs no render and puts no SVG in the agent's context. |
|
|
90
|
-
| `figdown_read` | The semantic model — participants, relationships and direction, containment, and the **stated meaning** of every class — shipped with the reading contract. Accepts a `.svg` too, recovering the source from its metadata and reporting the artifact stale if the `.fd` has moved on. |
|
|
90
|
+
| `figdown_read` | The semantic model — participants, relationships and direction, containment, and the **stated meaning** of every class — shipped with the reading contract. Accepts a `.svg` too, recovering the source from its metadata and reporting the artifact stale if the `.fd` has moved on. A `sequence` section reports its own collections (lifelines, messages, states, fragments, operands) and gets one extra contract line, because in that genre a message is **not** an edge and array order **is** the meaning. |
|
|
91
91
|
| `figdown_reference` | The per-genre reference, so an agent knows what a genre may and may not say. The grammar is **closed** and the parser **never warns about portability**, so this is the only place the frozen / EXPERIMENTAL split is visible. |
|
|
92
92
|
|
|
93
93
|
### `figdown_build`
|
|
@@ -118,8 +118,11 @@ than guessing at the picture.
|
|
|
118
118
|
### `figdown_reference`
|
|
119
119
|
|
|
120
120
|
`name` — a genre (`block`, `bitfield`, `table`, `topology`, `flowchart`,
|
|
121
|
-
`statechart`, `timing`), a task (`reading`, `transcribe`), or
|
|
122
|
-
for the index.
|
|
121
|
+
`statechart`, `timing`, `sequence`), a task (`reading`, `transcribe`), or
|
|
122
|
+
`skill`. Omit it for the index. The list is not restated here or in the tool's
|
|
123
|
+
schema as the authority — it is **parsed out of `SKILL.md`'s router at call
|
|
124
|
+
time**, so a genre that lands without a router row is invisible to this tool
|
|
125
|
+
and `gate:mcp` fails rather than an agent receiving nothing. `experimental: true` adds the genre's EXPERIMENTAL files, which sit
|
|
123
126
|
outside the v0.1 conformance surface and its compatibility promise.
|
|
124
127
|
|
|
125
128
|
## A parse error is a result, not an exception
|
|
@@ -168,7 +171,10 @@ node integrations/mcp-server/test.js # npm run gate:mcp
|
|
|
168
171
|
It speaks real JSON-RPC to a spawned subprocess (framing is the half that was
|
|
169
172
|
hand-rolled, and an in-process test cannot catch a framing bug), then exercises
|
|
170
173
|
every tool against real figures from `examples/` — a multi-section topology, an
|
|
171
|
-
experimental `figdown 0.2 statechart`, a
|
|
174
|
+
experimental `figdown 0.2 statechart`, a `figdown 0.4 sequence` ladder whose
|
|
175
|
+
model has no nodes and no edges at all, a published artifact read back through
|
|
172
176
|
its own metadata, and a deliberately broken document. Two of its assertions are
|
|
173
177
|
drift guards rather than feature tests: every genre in SKILL.md's router must
|
|
174
|
-
still resolve to files that exist
|
|
178
|
+
still resolve to files that exist — `sequence` by name, since a genre that
|
|
179
|
+
lands without a router row is the failure this guard exists for — and this
|
|
180
|
+
server must contain no engine copy.
|
|
@@ -307,7 +307,12 @@ function toolRead(args) {
|
|
|
307
307
|
const shape = docs.map((d, i) =>
|
|
308
308
|
' section ' + (i + 1) + ': figdown ' + d.version + ' ' + d.genre
|
|
309
309
|
+ (d.title ? ' title ' + JSON.stringify(d.title) : '')
|
|
310
|
-
|
|
310
|
+
// The collection list is per-genre, not scene-only: a `sequence` document
|
|
311
|
+
// carries no nodes and no edges (a message is NOT an edge — SEQUENCE-ORDER-MODEL), so a
|
|
312
|
+
// scene-only list reported a ladder of seventeen messages as "[3 classes]"
|
|
313
|
+
// and the shape line said nothing about the figure at all.
|
|
314
|
+
+ ' [' + ['nodes', 'edges', 'groups', 'classes', 'blocks', 'boundaries',
|
|
315
|
+
'lifelines', 'messages', 'states', 'fragments', 'operands']
|
|
311
316
|
.filter(k => Array.isArray(d[k]) && d[k].length)
|
|
312
317
|
.map(k => d[k].length + ' ' + k).join(', ') + ']').join('\n');
|
|
313
318
|
|
|
@@ -327,7 +332,18 @@ function toolRead(args) {
|
|
|
327
332
|
' parsable — infer no participant, edge or category from them.',
|
|
328
333
|
' * Array order is not ranking or priority (§12.7).',
|
|
329
334
|
' * Everything below `layout` is geometry with no meaning. Skip it.',
|
|
330
|
-
|
|
335
|
+
// One conditional line. `sequence` is the single genre where two of the
|
|
336
|
+
// rules above read as false: it has no nodes and no edges, and its array
|
|
337
|
+
// order IS the figure's meaning. Adding it unconditionally would charge
|
|
338
|
+
// every other reader for a genre they are not holding.
|
|
339
|
+
docs.some(d => d.genre === 'sequence')
|
|
340
|
+
? ' * THIS DOCUMENT HAS A `sequence` SECTION, where two of the rules above invert:\n'
|
|
341
|
+
+ ' participants are `lifelines`, a `message` is an occurrence in time and NOT an\n'
|
|
342
|
+
+ ' edge (which is why `edges` is empty), and `messages` ∪ `states` array order IS\n'
|
|
343
|
+
+ ' the order of the exchange. A `state` names the condition its lifeline is in\n'
|
|
344
|
+
+ ' from that point on. Nothing in the layout zone draws here.'
|
|
345
|
+
: null,
|
|
346
|
+
].filter(Boolean).join('\n');
|
|
331
347
|
|
|
332
348
|
const head = [
|
|
333
349
|
'ok: true ' + docs.length + ' section(s) in ' + s.label,
|
|
@@ -483,7 +499,7 @@ const TOOLS = [
|
|
|
483
499
|
inputSchema: {
|
|
484
500
|
type: 'object',
|
|
485
501
|
properties: {
|
|
486
|
-
name: { type: 'string', description: 'A genre (block, bitfield, table, topology, flowchart, statechart, timing), a task (reading, transcribe), or "skill". Omit for the index.' },
|
|
502
|
+
name: { type: 'string', description: 'A genre (block, bitfield, table, topology, flowchart, statechart, timing, sequence), a task (reading, transcribe), or "skill". Omit for the index.' },
|
|
487
503
|
experimental: { type: 'boolean', description: 'Include the genre\'s EXPERIMENTAL (experimental) files — outside the v0.1 conformance surface and its compatibility promise.' },
|
|
488
504
|
},
|
|
489
505
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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>",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"gate:registry": "node tools/registry-check.js --strict",
|
|
46
46
|
"gate:capability": "node tools/capability-coverage.js --strict",
|
|
47
47
|
"gate:archive": "node tools/archive-check.js --strict",
|
|
48
|
+
"gate:standards": "node tools/standards-check.js --strict",
|
|
48
49
|
"gate:plugin": "node tools/plugin-check.js --strict",
|
|
49
50
|
"gate:mcp": "node integrations/mcp-server/test.js",
|
|
50
51
|
"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]));\"",
|
package/skill/figdown/SKILL.md
CHANGED
|
@@ -113,21 +113,32 @@ so you know which file you need before you need it.
|
|
|
113
113
|
| `flowchart` | `reference/experimental/flowchart.md`, `reference/layout.md` | `reference/experimental/chart.md` |
|
|
114
114
|
| `statechart` | `reference/experimental/statechart.md`, `reference/layout.md` | `reference/experimental/chart.md` |
|
|
115
115
|
| `timing` | — | `reference/experimental/timing.md` |
|
|
116
|
+
| `sequence` — message exchanges over time | — | `reference/experimental/sequence.md` |
|
|
117
|
+
|
|
118
|
+
**`sequence` draws a ladder**, and you place nothing in it: participants are
|
|
119
|
+
columns and time runs down the page, both in the order the source declares
|
|
120
|
+
them, so no key in that genre moves a coordinate — not even a layout-zone one,
|
|
121
|
+
which parses and changes nothing there. Its file is the whole of its
|
|
122
|
+
vocabulary, and it is the one genre where the layout file buys you nothing.
|
|
123
|
+
Three constructs an author arriving from another genre reaches for were
|
|
124
|
+
argued and REFUSED there — a time-gap line, a band over participants, and a
|
|
125
|
+
per-message "sent but never delivered" flag; each is a line error that names
|
|
126
|
+
its ground and the spelling to write instead, so write the source and read
|
|
127
|
+
the message rather than guessing. Its file says how.
|
|
116
128
|
|
|
117
129
|
Two more files answer a **task** rather than a genre:
|
|
118
130
|
|
|
119
131
|
- **Reading a `.fd` someone else wrote, to summarise or answer from it** →
|
|
120
132
|
`reference/reading.md`. It is the contract for what you may conclude and
|
|
121
133
|
what you must not infer, and it is all you need: a reader can skip every
|
|
122
|
-
genre file above. **
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
keyword down is …"
|
|
126
|
-
layout **namespace** — its one member is `pin`
|
|
127
|
-
appears**, because a `pin` may legally sit *before* the
|
|
128
|
-
about half of them do. Membership decides, not position.
|
|
129
|
-
|
|
130
|
-
under "layout and pin".
|
|
134
|
+
genre file above. **The correction that used to be printed here is now in the
|
|
135
|
+
file itself**, so read `reference/reading.md` as it stands. If you ever meet
|
|
136
|
+
an OLDER copy of that contract — one that says *"Ignore the layout zone.
|
|
137
|
+
Everything from the `layout` keyword down is …"* — do not read that as a rule
|
|
138
|
+
about **position**. Ignore the layout **namespace** — its one member is `pin`
|
|
139
|
+
— **wherever a member appears**, because a `pin` may legally sit *before* the
|
|
140
|
+
`layout` line, and about half of them do. Membership decides, not position.
|
|
141
|
+
It is the same rule stated above under "layout and pin".
|
|
131
142
|
- **Transcribing an existing figure** — a drawing, a screenshot, another
|
|
132
143
|
format → `reference/transcribe.md`.
|
|
133
144
|
|
|
@@ -38,9 +38,23 @@ function buildOne(engine, fdPath) {
|
|
|
38
38
|
for (const e of errs) console.error(' ' + e);
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
|
+
// GEOMETRY-TIME DIAGNOSTICS gate the artifact exactly as parse errors do.
|
|
42
|
+
// `parse` cannot see a coordinate, so a figure whose SOURCE is impeccable can
|
|
43
|
+
// still draw a false statement — a `group` band is the bounding box of its
|
|
44
|
+
// members, and a non-member sitting inside it tells the reader it is one.
|
|
45
|
+
// The engine reports those from `render`; writing the `.svg` anyway would
|
|
46
|
+
// publish the picture the engine has just said is wrong, which is the whole
|
|
47
|
+
// failure this gate exists to stop.
|
|
48
|
+
const rendered = docs.map(d => engine.render(d, RENDER_OPTS));
|
|
49
|
+
const gerrs = rendered.reduce((a, r) => a.concat(r.errs || []), []);
|
|
50
|
+
if (gerrs.length) {
|
|
51
|
+
console.error(fdPath + ':');
|
|
52
|
+
for (const e of gerrs) console.error(' ' + e);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
41
55
|
const svg = docs.length > 1
|
|
42
|
-
? engine.stackSectionSvgs(
|
|
43
|
-
:
|
|
56
|
+
? engine.stackSectionSvgs(rendered)
|
|
57
|
+
: rendered[0].svg;
|
|
44
58
|
const hash = crypto.createHash('sha256').update(src, 'utf8').digest('hex');
|
|
45
59
|
// The artifact records three things (spec core §7): the SHA-256 OF THE
|
|
46
60
|
// SOURCE, the ENGINE VERSION that rendered it, and any non-default render
|