figdown 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/dist/figdown.js +1344 -149
- package/dist/figdown.mjs +1344 -149
- package/examples/evpn-fabric.svg +3 -4
- 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/guide/expressing.md +29 -17
- package/guide/layout.md +25 -17
- package/integrations/mcp-server/README.md +174 -0
- package/integrations/mcp-server/server.js +593 -0
- package/package.json +8 -3
- package/skill/figdown/SKILL.md +19 -9
- package/skill/figdown/figdown.html +1350 -152
- package/skill/figdown/reference/experimental/block.md +60 -0
- package/skill/figdown/reference/experimental/chart.md +32 -0
- package/skill/figdown/reference/experimental/flowchart.md +97 -10
- package/skill/figdown/reference/experimental/statechart.md +55 -9
- package/skill/figdown/reference/experimental/timing.md +1 -1
- package/skill/figdown/reference/experimental/topology.md +148 -22
- package/skill/figdown/reference/reading.md +13 -1
- package/skill/figdown/reference/scene.md +75 -22
- package/skill/figdown/reference/experimental/constructs.md +0 -90
package/guide/expressing.md
CHANGED
|
@@ -11,18 +11,31 @@ Purpose: one-line lookup — "I need to show X → use Y." One row per intent. N
|
|
|
11
11
|
portable figures: `block`, `bitfield`, `table` (plus the `UNIVERSAL-CORE-KEYWORDS` core keywords
|
|
12
12
|
`figdown`/`title`/`layout` and the layout namespace's normative `pin`).
|
|
13
13
|
Constructs marked EXPERIMENTAL — `threshold`, `band`, `bundle`,
|
|
14
|
-
`
|
|
15
|
-
`
|
|
14
|
+
`chart`, option keys
|
|
15
|
+
`extend=`/`data=`,
|
|
16
16
|
and genres `topology`, `flowchart`, `timing` (`CONSTRUCT-STATUS-TIERS`, spec §10) — still parse and
|
|
17
17
|
are not deprecated, but they sit **outside** the v0.1 compatibility promise
|
|
18
18
|
and may change without a migration entry. **Do not use them when the figure
|
|
19
19
|
must be portable.** `path` and `routing` (with `points=`, `tailport=`,
|
|
20
|
-
`headport=`, `routing=`) were on that list until
|
|
20
|
+
`headport=`, `routing=`) were on that list until 0.1, when `EDGE-GEOMETRY-CONSTRUCTS`
|
|
21
21
|
**withdrew all six from the language**: removed, not renamed, so there is no
|
|
22
22
|
row for them below and no replacement to point at. Edge geometry is the
|
|
23
|
-
engine's; the need is filed as core §9 **`EDGE-IDENTITY-AND-GEOMETRY`**.
|
|
23
|
+
engine's; the need is filed as core §9 **`EDGE-IDENTITY-AND-GEOMETRY`**. **`plane` went the same way
|
|
24
|
+
(`PAINT-ORDER-CONSTRUCT`)**, taking `z-index=` and the `plane=` option key with it —
|
|
25
|
+
same shape, same absence of a replacement, so no row below either. What the one
|
|
26
|
+
authored figure used it for, `class` already did: stripping both writings from
|
|
27
|
+
`examples/evpn-fabric.fd` and rebuilding changed exactly one drawn markup token
|
|
28
|
+
— a `data-edge` index — and nothing else. The
|
|
29
|
+
parser never warns; this index and §10 are the only
|
|
24
30
|
status sources. Top-level keywords are also **genre-allowlisted (`GENRE-KEYWORD-ALLOWLIST`)**:
|
|
25
|
-
`node` under `figdown 0.1 bitfield` is an error, not a silent hybrid
|
|
31
|
+
`node` under `figdown 0.1 bitfield` is an error, not a silent hybrid — and
|
|
32
|
+
(`SUBJECT-VOCABULARY-SCOPE`) the allowlist runs **per genre all the way down**. Only
|
|
33
|
+
`figdown`/`title`/`layout` are cross-genre; every other keyword belongs to one
|
|
34
|
+
genre's own namespace, and a spelling two genres share is two independent
|
|
35
|
+
declarations that happen to agree today, never one inherited. So the Notes
|
|
36
|
+
column below names the genres each construct is legal in: `group` under
|
|
37
|
+
`flowchart` and `external` under `statechart` are line errors (`SCENE-KEYWORD-MEMBERSHIP`) even
|
|
38
|
+
though both words are live elsewhere.
|
|
26
39
|
|
|
27
40
|
**This file is the single intent index.** authoring.md Step 3 used to carry a
|
|
28
41
|
second, partly-overlapping table; every intent it held now lives here, so there
|
|
@@ -34,21 +47,20 @@ is one place to look and nothing to reconcile.
|
|
|
34
47
|
|
|
35
48
|
| I need to show… | Use | Notes |
|
|
36
49
|
|---|---|---|
|
|
37
|
-
| containment — node belongs inside a box | `group g "Label"` + `node n "…" in=g` | one level of nesting only (spec §2.2); for deeper, represent the inner group as a proxy node |
|
|
50
|
+
| 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 |
|
|
38
51
|
| set membership / category (color + legend) | `class c "meaning" fill=… style=…` + `class=c` on members | legend derives automatically; bare `fill=` carries no named meaning |
|
|
39
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 MUST declare `stroke=` or `style=`: `fill=`-only (`INTERIOR-LESS-ELEMENT-PAINT`) and no-paint-at-all (`CLASS-PAINT-REQUIREMENT`) are both line errors, because the edge would otherwise lose its colour silently. `fill=`, `stroke=` and `style=` are all NORMATIVE since 0.1 (`STROKE-KEY-STATUS`) |
|
|
40
53
|
| hierarchy / tree | directed `edge` chain; `flow down` to orient | the edges carry the tree; group is for spatial containment, not hierarchy |
|
|
41
54
|
| adjacency without a link | `in=` on the same `group`, no `edge` between them | the shared frame communicates co-location |
|
|
42
55
|
| cross-cutting category spanning groups | `class` + `class=` on elements in different groups | one class can mark nodes, edges, and fields across the whole document |
|
|
43
|
-
| system boundary / inside vs outside | `external ext "label"` + edges to/from it; internal nodes in a `group` | `external` is never drawn as a shape — the edge ends open; it names an external I/O *endpoint*, not the frame drawn around a system (that is `group`) |
|
|
56
|
+
| system boundary / inside vs outside | `external ext "label"` + edges to/from it; internal nodes in a `group` | `external` is never drawn as a shape — the edge ends open; it names an external I/O *endpoint*, not the frame drawn around a system (that is `group`). **`block`, `topology` and `flowchart` only**: withdrawn from `statechart` at 0.3 (`SCENE-KEYWORD-MEMBERSHIP`) because UML 2.5.1 §14 already defines `external` as a `TransitionKind` (`external | internal | local`), and that reading is the one a statechart's reader arrives with. It takes **no option key at all** since `PAINT-ORDER-CONSTRUCT` removed `plane=`, its only one |
|
|
44
57
|
| ownership zones / domain coloring | `class zone "Owner" fill=…` on a `group` or its members | assign to the group itself to color the frame |
|
|
45
58
|
| a region the source drew as a **cloud** (the internet, a transit network, an overlay fabric) | `node net "The internet" shape=ellipse` (or a `group` when other elements sit inside it), plus a `class` when the distinction is one a reader must query | `shape=cloud` was **retired at 0.1** (`SHAPE-ENUM-VOCABULARY`) — it was the only value in a geometric enum that named a domain, which `SHAPE-ENUM-VOCABULARY` forbids. `ellipse` is what preserves the drawing; the label is what preserves the meaning, and only you know which one the figure needed |
|
|
46
59
|
| conditional or optional element (standby link, optional component) | `class cond "Condition text" style=dashed` + `class=cond` on the element | `style=dashed` alone states nothing (`PRESENTATION-AS-MEANING-CARRIER`); write the condition in the class label |
|
|
47
60
|
| one-level nesting limit | `in=` on `node`; `group … in=…` is a line error | for two levels: add a proxy `node` representing the inner group |
|
|
48
61
|
| layer stack or spatial arrangement where order is knowledge | `edge` chain + `flow down`, or single-row `table`; state the order in prose | no spatial-arrangement construct yet — see Known limits `MEANINGFUL-ARRANGEMENT` |
|
|
49
|
-
| slot map / position in a spatial grid | state positions in label text + `pin` in the layout zone | `pin` is geometry only, and the layout
|
|
50
|
-
| several links that are one logical thing (LAG, ES, trunk group) | `bundle b1 "LAG" a--c,b--c` — ONE comma-delimited token (the space form was retired at 0.1) | the dashed ring around the members is derived automatically; members resolve **as written** (`a--c` is not `c--a`). **EXPERIMENTAL** (`CONSTRUCT-STATUS-TIERS`)
|
|
51
|
-
| an independent plane a reader can separate (overlay vs underlay, control vs data) | `plane overlay "VXLAN tunnels" z-index=2` + `plane=overlay` on its elements | **EXPERIMENTAL** (`CONSTRUCT-STATUS-TIERS`) — the keyword, `z-index=` and `plane=` alike; works unchanged. The plane's **label is the knowledge**; `z` is paint order (implicit `base` is model `z` = 0; an omitted `z-index=` takes the 1-based declaration index). Honest limit: `z` reorders only the annotation pass — edges, bundle rings, thresholds, bands — nodes and groups paint in document order whatever their plane says (spec §5) |
|
|
62
|
+
| slot map / position in a spatial grid | state positions in label text + `pin` in the layout zone | `pin` is geometry only, and every member of the layout namespace is ignored by default wherever it is written (`GENRE-NAMESPACE`) — use a `table` for anything a reader must count or address |
|
|
63
|
+
| several links that are one logical thing (LAG, ES, trunk group) | `bundle b1 "LAG" a--c,b--c` — ONE comma-delimited token (the space form was retired at 0.1) | the dashed ring around the members is derived automatically; members resolve **as written** (`a--c` is not `c--a`). **EXPERIMENTAL** (`CONSTRUCT-STATUS-TIERS`), and **`topology` only** since 0.3 (`SCENE-KEYWORD-MEMBERSHIP`): every corpus use of it was already in a `topology` document, and only there can it be defined by its referent — an IEEE 802.1AX LAG, an ECMP set, an EVPN Ethernet Segment — instead of as "a ring drawn round these links" |
|
|
52
64
|
| same row / same column (peers, stages, siblings) | `rank a,b,c` — a **semantic** (content) line, before `layout`; ONE comma-delimited token (the space form was retired at 0.1) | under `flow down` a rank shares a row, so rank the lateral peers, not the mainline ([layout.md §8](layout.md#8-cautions-from-a-production-corpus)) |
|
|
53
65
|
| cardinality, port name, or role at each end of a relationship | endpoint labels: `a [1] -[places]-> [N] b` | three label positions on one edge (tail · mid · head); `[flags[3:0]]` nests, `["…"]` for `\n` or unbalanced brackets. `label=`/`taillabel=`/`headlabel=` are retired options, not spellings |
|
|
54
66
|
| a shape in the source figure that carries no text at all (a junction, an unlabelled multiplexer, a bare glyph) | an explicitly empty label: `node j "" shape=circle` | `""` is a WRITTEN value, recorded as `label: ""` and drawn blank. Omitting the label instead makes the renderer display the **id**, which is text the source does not have. Reserve the omitted form for a label you could not read, and say so in a `#` comment (spec §2.1/§12.3, `EMPTY-LABEL-STATE`) |
|
|
@@ -119,13 +131,13 @@ is one place to look and nothing to reconcile.
|
|
|
119
131
|
|
|
120
132
|
| I need to show… | Use | Notes |
|
|
121
133
|
|---|---|---|
|
|
122
|
-
| proportion of a whole / fill level | `band "Headroom" 15..35% in=g fill=…` on a `group` or `node` | the quoted label is **mandatory** and comes **first** (`BAND-LABEL-STATUS`) — a band with no name asserts nothing; `band "X" 15%` = 0..15%. The label's colour is derived from the band's fill (`LABEL-COLOUR-SOURCE`); there is no key for it. **EXPERIMENTAL** (`CONSTRUCT-STATUS-TIERS`) |
|
|
123
|
-
| 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`) |
|
|
134
|
+
| proportion of a whole / fill level | `band "Headroom" 15..35% in=g fill=…` on a `group` or `node` | the quoted label is **mandatory** and comes **first** (`BAND-LABEL-STATUS`) — a band with no name asserts nothing; `band "X" 15%` = 0..15%. The label's colour is derived from the band's fill (`LABEL-COLOUR-SOURCE`); there is no key for it. **EXPERIMENTAL** (`CONSTRUCT-STATUS-TIERS`), and **`block` only** since 0.3 (`SCENE-KEYWORD-MEMBERSHIP`) — under `topology` the word reads as a *frequency* band, which is the collision that withdrew it there |
|
|
135
|
+
| 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 |
|
|
124
136
|
| quantity comparison | `table` with numeric columns | `▁▃▅▇` Unicode blocks as sparklines in cells (`TABLE-SPARKLINE`) |
|
|
125
137
|
| 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 |
|
|
126
138
|
| event ordering without exact times | directed edges with ordinal mid-labels (`-[1: SYN]->`) | number labels consistently; no sequence genre yet |
|
|
127
139
|
| visual code / legend | `class` — legend derives automatically from declaration order | each `class` line gives swatch + meaning text |
|
|
128
|
-
| annotation explaining why | `node
|
|
140
|
+
| 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=` |
|
|
129
141
|
| 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` |
|
|
130
142
|
| units | state them in the label or column header | no machine-readable unit type; interim: text in the label |
|
|
131
143
|
| trends / rates | `table` with a values column; `▁▃▅▇` for sparklines | charts are out of scope (`CHART-SCOPE-BOUNDARY`) — keep the raster + prose |
|
|
@@ -138,20 +150,20 @@ Each entry: what cannot be expressed today · OQ reference · sanctioned interim
|
|
|
138
150
|
|
|
139
151
|
- **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.
|
|
140
152
|
- **strict message ordering** — order carried only by label numbering convention; sequence genre candidate (v0.2, `FLOWCHART-GENRE-DESIGN`); interim: number labels consistently (e.g. `1: SYN`, `2: SYN-ACK`).
|
|
141
|
-
- **first-class callout / annotation** — a callout is structurally a node; `ROW-INDEX-GUTTER`; interim: `node note "…" style=dashed` + dashed `edge`.
|
|
142
153
|
- **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.
|
|
143
154
|
- **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.
|
|
144
155
|
- **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.
|
|
145
156
|
- **byte-unit packet construct** — `bitfield` adds an unwanted bit ruler; `BYTE-UNIT-PACKET-BLOCKS`; interim: single-row `table`.
|
|
146
|
-
- **meaningful arrangement** (layer stacks, floorplans, proportional memory maps) — no declared-arrangement construct, and the layout
|
|
157
|
+
- **meaningful arrangement** (layer stacks, floorplans, proportional memory maps) — no declared-arrangement construct, and the layout namespace is default-ignored wherever its members are written so `pin` is not a substitute (`GENRE-NAMESPACE`); `MEANINGFUL-ARRANGEMENT`; interim: state the arrangement meaning in prose + read the `pin` lines.
|
|
147
158
|
- **timing-diagram span parameters** (setup/hold spans) — no named span between signal events in `timing`; `TIMING-MEASUREMENT-ANNOTATIONS`; interim: keep the original raster + prose.
|
|
148
159
|
- **mode-dependent field variants** — same bit range, different decode per mode; `BITFIELD-DISCRIMINATED-VARIANTS`; interim: separate labelled `bitfield` blocks + `description=`.
|
|
149
160
|
- **multiplicity / machine-readable units** — no count or unit type; pending corpus frequency; interim: state them in label text.
|
|
150
161
|
- **AND vs XOR fan-out** — `decision` (`flowchart` only) settles it for a decision's own exits: exactly one fires. For any OTHER fan-out — a `process` with several successors — the language still cannot say whether all branches fire or one does; `FLOWCHART-GENRE-DESIGN` sub-question; interim: declare a `class` naming the join discipline. `fork`/`join`/`merge` are recorded as excluded (0 corpus uses).
|
|
151
|
-
- **swimlanes / partitions in a flowchart** — no container-with-an-axis construct; recorded as the next flowchart candidate (Mermaid #2028, UML `ActivityPartition`, BPMN `Lane`)
|
|
162
|
+
- **swimlanes / partitions in a flowchart** — no container-with-an-axis construct; recorded as the next flowchart candidate (Mermaid #2028, UML `ActivityPartition`, BPMN `Lane`). A swimlane partitions **across** the flow, by responsibility. The interim used to be `group` + a label naming the lane, which lost the axis; (`SCENE-KEYWORD-MEMBERSHIP`) `group` is not a `flowchart` keyword and (`MEMBERSHIP-KEY-ACCEPTANCE`) neither is `in=`, so the box interim is gone entirely — declare a `class` per partition and put `class=` on each stage, which earns a legend entry and applies to every member at once.
|
|
163
|
+
- **pipeline PHASES in a flowchart — a SIBLING of the row above, not the same gap** (`MEMBERSHIP-KEY-ACCEPTANCE`). **A phase is not a swimlane.** Visio ships the two as separate constructs on orthogonal axes: swimlanes (bands) name functional units — *who is responsible* — while **phases** (separators) cut **across all the lanes** to mark a stage boundary; a cross-functional flowchart routinely has both. UML's `ActivityPartition` is the actor-ish one, so it belongs to the swimlane row. A phase is **an ordered stage-partition ALONG the flow axis**. The demand is real and **was never measured**: the downstream flowchart survey counted 3266 nodes and 2684 edges across 227 deduped figures and **never counted containers at all**, so the evidence is thin on both sides. *Reopens on:* a count of downstream production parser specifications whose phases fail to read once rendered from `class`. Interim: a `class` per phase — complete on meaning, and its one residual loss (members are not guaranteed to be drawn adjacent or in phase order) is a **renderer** question filed in `decisions/registry.md` beside items 26/27, not a missing word.
|
|
152
164
|
- **conditional presence in scene elements** — `present=` is a `bitfield`-only option key; `node a "A" present="..."` is a line error; <!-- fence-check: skip --> v0.2 candidate; interim: `class cond "condition text" style=dashed` + `class=cond`.
|
|
153
165
|
- **mutually exclusive `bitfield` interpretations — a correctness trap, not a cosmetic one.** `break` is presentation-only and never reorders or skips bits, so alternative decodings of the *same* bits stacked with `break` are read by a machine as one long contiguous sequence: an 18-bit register drawn as eight alternative encodings computes as 144 bits. There is no union/case construct and nothing warns. Open question: what a variant/discriminator construct should look like (adjacent to `BITFIELD-DISCRIMINATED-VARIANTS`). Interim: **one `bitfield` block per alternative**, each labelled with the discriminator value that selects it (`bitfield ctl_a "Control — mode=0 (18 bits)" word=18`).
|
|
154
|
-
-
|
|
166
|
+
- ~~**first-class callout / annotation**~~ and ~~**aside / non-participating annotation**~~ — **SOLVED (`DRAWN-ANNOTATION-FORM`)** by `note=`, which is an attribute and therefore not a participant at all: the workaround it replaces (a detached dashed `node`) *was* structurally a node, so a reading agent counted it in the topology and mixed annotation text into architecture descriptions. What remains open is only the **spanning** case — one note about two or more elements, measured at 10.0% (corpus A) and 6.7% (corpus B), which an attribute cannot express. Interim, and it is the corpus's own: a **footnote marker** in the label text (`*`, `**`, `#`, `##`) with the explanation in a `note=` keyed to the same marker. Legal, drawn and readable today; its only loss is that the correspondence is not machine-readable.
|
|
155
167
|
- **repeated-subgraph reuse** — no way to declare a sub-structure once and instantiate it *n* times. Open question: whether declare-once/instantiate-many belongs in the language at all or in the generator above it. Interim: write each instance out.
|
|
156
168
|
- **two-level group nesting** — `group … in=…` is a line error; nesting is one level in v0.1. Open question: whether deeper nesting needs new syntax or only a renderer change. Interim: a proxy `node` for the inner group.
|
|
157
169
|
- **mux / selector shape** — `shape=` is a closed geometric set (`box|rounded|circle|ellipse|diamond|cylinder`); `shape=mux` is a line error. Open question: whether the mux trapezoid is geometry (admissible under `SHAPE-ENUM-VOCABULARY`) or a domain noun (excluded). Interim: `shape=diamond` or a box, with the selector role in the label or a `class`.
|
package/guide/layout.md
CHANGED
|
@@ -28,7 +28,7 @@ removed, not renamed, so there is no spelling to migrate to. See
|
|
|
28
28
|
[spec/migrations.md](../spec/migrations.md) 0.1, core §9 **`EDGE-IDENTITY-AND-GEOMETRY`** for
|
|
29
29
|
the requirement that survived them, and
|
|
30
30
|
the project’s working record for the evidence, which is not published.)
|
|
31
|
-
(The zone opener was spelled `render` before
|
|
31
|
+
(The zone opener was spelled `render` before 0.1; that spelling is now a
|
|
32
32
|
line error, because the zone takes geometry, not presentation.)
|
|
33
33
|
|
|
34
34
|
Key invariant (`GUI-WRITEBACK-STRUCTURE`, `MEANING-RECOVERY-SOURCE`): stripping every `pin` line
|
|
@@ -37,23 +37,31 @@ expresses the identical structure and relationships. No knowledge lives in the
|
|
|
37
37
|
layout zone. If removing a `pin` changes what an AI reader can learn, the
|
|
38
38
|
position was encoding meaning that should live in text instead.
|
|
39
39
|
|
|
40
|
-
**The layout
|
|
40
|
+
**The layout NAMESPACE is DEFAULT-IGNORED (`GENRE-NAMESPACE`, strengthening `CONTENT-LAYOUT-ZONE-SPLIT`; restated
|
|
41
|
+
over the namespace by `GENRE-NAMESPACE`).** It exists
|
|
41
42
|
ONLY to stabilise the rendered `.svg`. Anything that is content, logic or
|
|
42
43
|
concept MUST be expressible in the content zone, so a reading agent's default
|
|
43
|
-
behaviour is to
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
behaviour is to ignore **every member of the layout namespace, wherever in the
|
|
45
|
+
document it appears** — not merely that it may. **Membership decides, never
|
|
46
|
+
position.** Write for that: every `pin` you write is a line an AI reader will
|
|
47
|
+
not see, whether you put it after `layout` or before.
|
|
48
|
+
There is no keyword to opt in or out. (The earlier form of this promise said
|
|
49
|
+
"skip everything from the `layout` line down", and that was literally true and
|
|
50
|
+
practically empty: `pin` is legal before `layout` too, and about half the pins
|
|
51
|
+
in this repository's own corpus are written there, so an agent keeping that
|
|
52
|
+
promise still met half the layout information. `GENRE-NAMESPACE` moved the promise onto the
|
|
53
|
+
namespace, where the container model already was.
|
|
54
|
+
**the layout zone is a namespace of its own** (`LAYOUT-ZONE-NAMESPACE`, clause
|
|
55
|
+
`LAYOUT-ZONE-NAMESPACE`), and every
|
|
50
56
|
member of it is genre-independent. No genre may
|
|
51
57
|
define, redefine or extend a keyword inside the zone; `GENRE-VOCABULARY-OBLIGATION`, "a genre owns its
|
|
52
|
-
words", does not reach in. That is what makes the
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
words", does not reach in. That is what makes the default safe *for ever* and
|
|
59
|
+
what makes it usable: ONE enumeration of the members is correct under every
|
|
60
|
+
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 it has exactly **one** member — `pin`, NORMATIVE. `layout` is not a
|
|
62
|
+
member: it is the zone's OPENER and lives in the universal core of three
|
|
63
|
+
(§10 (a)) alongside `figdown` and `title`. The withdrawal of `path` and
|
|
64
|
+
`routing` left `LAYOUT-ZONE-NAMESPACE` whole and took its only experimental members with it.) If a
|
|
57
65
|
figure needs
|
|
58
66
|
"arrangement carries meaning", the language owes it a content-zone construct
|
|
59
67
|
(`MEANINGFUL-ARRANGEMENT`); until that lands, say the arrangement in prose in the content zone
|
|
@@ -443,9 +451,9 @@ paragraph is for.)
|
|
|
443
451
|
|
|
444
452
|
**Long back-edges and bypasses read as detours — say what they are.** An edge
|
|
445
453
|
that skips several ranks is routed through the gaps and keeps a bend only
|
|
446
|
-
where something is actually in the way (it used to pick up
|
|
454
|
+
where something is actually in the way (`EDGE-BEND-RETENTION` — it used to pick up
|
|
447
455
|
a bend per rank crossed and read as a staircase); but however clean the line,
|
|
448
|
-
a reader who cannot tell *why* a line is out there reads it as noise. Until
|
|
456
|
+
a reader who cannot tell *why* a line is out there reads it as noise. Until 0.1 the taught answer was a hand-written waypoint; `path` is withdrawn
|
|
449
457
|
and there is no replacement, so the answer moves into the content zone, where
|
|
450
458
|
it arguably belonged: name the bypass so the detour is legible as a bypass.
|
|
451
459
|
|
|
@@ -515,7 +523,7 @@ better (backlog item #4) makes a layered drawing tidier; it never turns it into
|
|
|
515
523
|
a ring.
|
|
516
524
|
|
|
517
525
|
There is a second cause, and it is in the language rather than the engine.
|
|
518
|
-
Every flowchart connector today is a plain `flowline` (spelled `edge` before
|
|
526
|
+
Every flowchart connector today is a plain `flowline` (spelled `edge` before 0.2, `GENRE-CONNECTOR-SPELLING` — the rename gave the genre its own word, not a role). The
|
|
519
527
|
renderer cannot tell the mainline from an exception branch from a retry loop — and those distinctions
|
|
520
528
|
are *exactly* what human flowchart routing conventions are made of.
|
|
521
529
|
0.1 (`FLOWCHART-ROLE-KEYWORDS`) gave the genre role vocabulary for its NODES — `process`,
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# figdown-mcp — FigDown over the Model Context Protocol
|
|
2
|
+
|
|
3
|
+
A dependency-free MCP server that gives an AI agent the three things it needs
|
|
4
|
+
to work with FigDown figures: **build** a `.fd` into an SVG, find out **why**
|
|
5
|
+
one does not parse, and **read** a figure's meaning out of the model rather
|
|
6
|
+
than out of the picture.
|
|
7
|
+
|
|
8
|
+
FigDown exists so an agent can read a figure's *meaning* instead of OCR-ing a
|
|
9
|
+
drawing. MCP is how agents reach tools. This is that channel — and it is the
|
|
10
|
+
only one that exposes the parsed semantic model (spec core §12), which is what
|
|
11
|
+
a *reading* agent actually wants and what neither the SVG nor the CLI hands
|
|
12
|
+
over.
|
|
13
|
+
|
|
14
|
+
## No network, no service, no API key
|
|
15
|
+
|
|
16
|
+
Nothing here opens a socket, resolves a name, or reads a credential. There is
|
|
17
|
+
no account, no rate limit and no telemetry. The server has **zero
|
|
18
|
+
dependencies** — including no MCP SDK: the stdio transport is newline-delimited
|
|
19
|
+
JSON-RPC 2.0, which is about eighty lines, and taking a dependency to save them
|
|
20
|
+
would have been the first dependency in the whole project.
|
|
21
|
+
|
|
22
|
+
Its only I/O is stdin/stdout plus, on explicit request, reading a `.fd`/`.svg`
|
|
23
|
+
and writing that source's sidecar. **There is no arbitrary output path:** the
|
|
24
|
+
only file this server ever writes is `X.svg` beside the `X.fd` it was given,
|
|
25
|
+
and only when the call passes `write: true`.
|
|
26
|
+
|
|
27
|
+
## It does not contain a copy of the engine
|
|
28
|
+
|
|
29
|
+
This repository has shipped seven four-copy-drift incidents. There are four
|
|
30
|
+
engine copies already — `editor/figdown.html` (hand-edited) plus the three
|
|
31
|
+
generated from it (`dist/figdown.js`, `dist/figdown.mjs`,
|
|
32
|
+
`skill/figdown/figdown.html`). This server adds **no fifth copy**: it
|
|
33
|
+
`require`s `dist/figdown.js`, exactly as the other two integrations do.
|
|
34
|
+
|
|
35
|
+
`dist/figdown.js` is the right consumption point rather than merely an
|
|
36
|
+
available one. It is the package's own `main` — what `require('figdown')`
|
|
37
|
+
returns — it is the only artifact with a module API, and `npm run gate:dist`
|
|
38
|
+
already holds it to the reference engine *behaviourally*: regenerating must be
|
|
39
|
+
a byte-level no-op, both builds must render identically, and every published
|
|
40
|
+
`.fd` must parse through it with **the same error set** as
|
|
41
|
+
`editor/figdown.html`. That is agreement on behaviour rather than on a version
|
|
42
|
+
string, and this server inherits it for free.
|
|
43
|
+
|
|
44
|
+
The same rule applies one level down. The genre router — *which reference file
|
|
45
|
+
does genre G need* — is **parsed out of `skill/figdown/SKILL.md`** at call
|
|
46
|
+
time, by the same marker comment `tools/skill-coverage.js` gates, rather than
|
|
47
|
+
restated here. A restated router would let a renamed reference file turn into
|
|
48
|
+
an agent silently receiving nothing.
|
|
49
|
+
|
|
50
|
+
## Run it
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
node tools/make-lib.js # regenerate dist/ first, if you edited the engine
|
|
54
|
+
node integrations/mcp-server/server.js # speaks MCP on stdin/stdout
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Configure an MCP client with either the published executable or the path:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"figdown": { "command": "npx", "args": ["-y", "figdown", "figdown-mcp"] }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"figdown": { "command": "node", "args": ["/path/to/figdown/integrations/mcp-server/server.js"] }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Two environment overrides, in the style of `tools/build-svg.js`: `FIGDOWN_LIB`
|
|
76
|
+
(path to `figdown.js`) and `FIGDOWN_SKILL` (path to the `skill/figdown/`
|
|
77
|
+
directory holding `SKILL.md` and `reference/`).
|
|
78
|
+
|
|
79
|
+
## The tools
|
|
80
|
+
|
|
81
|
+
Four, matching the three verbs `skill/figdown/SKILL.md` already teaches —
|
|
82
|
+
author, build, read — plus the diagnostics loop that connects them. A tool
|
|
83
|
+
nobody calls is a maintenance cost with no reader, so there are no others: in
|
|
84
|
+
particular there is no layout-lint tool and no "render to PNG".
|
|
85
|
+
|
|
86
|
+
| Tool | Why an agent needs it |
|
|
87
|
+
|---|---|
|
|
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
|
+
| `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. |
|
|
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
|
+
|
|
93
|
+
### `figdown_build`
|
|
94
|
+
|
|
95
|
+
`source` or `path` (not both); `write`, `with_title`, `return_svg`,
|
|
96
|
+
`include_model`.
|
|
97
|
+
|
|
98
|
+
**What it returns, decided rather than defaulted.** The SVG comes back inline
|
|
99
|
+
*unless* it was just written to disk, in which case the path comes back and the
|
|
100
|
+
caller is spared 5–50 KB of markup it can already open. `return_svg` overrides
|
|
101
|
+
in both directions. The parsed model is **opt-in** here (`include_model`) and
|
|
102
|
+
the default in `figdown_read`: an author who just wrote the source does not
|
|
103
|
+
need it read back, and a reader should not have to render to get it.
|
|
104
|
+
|
|
105
|
+
### `figdown_check`
|
|
106
|
+
|
|
107
|
+
`source`, or `path` to a file **or a directory** (walked recursively — a check
|
|
108
|
+
that does not recurse is a check that lies). It states how many documents it
|
|
109
|
+
looked at, so the number can be compared against `find <dir> -name '*.fd' | wc -l`.
|
|
110
|
+
|
|
111
|
+
### `figdown_read`
|
|
112
|
+
|
|
113
|
+
`source` or `path`, `.fd` or `.svg`. Returns the section shape, the reading
|
|
114
|
+
contract in short form, and the full model as JSON with every element's source
|
|
115
|
+
line. Handed an SVG that FigDown did not produce it refuses and says so, rather
|
|
116
|
+
than guessing at the picture.
|
|
117
|
+
|
|
118
|
+
### `figdown_reference`
|
|
119
|
+
|
|
120
|
+
`name` — a genre (`block`, `bitfield`, `table`, `topology`, `flowchart`,
|
|
121
|
+
`statechart`, `timing`), a task (`reading`, `transcribe`), or `skill`. Omit it
|
|
122
|
+
for the index. `experimental: true` adds the genre's EXPERIMENTAL files, which sit
|
|
123
|
+
outside the v0.1 conformance surface and its compatibility promise.
|
|
124
|
+
|
|
125
|
+
## A parse error is a result, not an exception
|
|
126
|
+
|
|
127
|
+
The diagnostics are the product: each one carries a 1-based line number, the
|
|
128
|
+
reason, and — for a retired spelling — what to write instead. They are what
|
|
129
|
+
makes an automated author → validate → fix loop possible, so they must reach
|
|
130
|
+
the caller intact. Three channels are kept distinct:
|
|
131
|
+
|
|
132
|
+
| Channel | Used for |
|
|
133
|
+
|---|---|
|
|
134
|
+
| JSON-RPC `error` | Protocol faults only: unparseable JSON, unknown method. |
|
|
135
|
+
| `isError: true` | The tool could not run at all — no such file, `source` *and* `path` given, an SVG with no FigDown metadata. The request was malformed; there was nothing to read. |
|
|
136
|
+
| a normal result | **Including a parse failure.** The tool did exactly its job: it reported why the document does not parse. The first line reads `PARSE FAILED — N diagnostic(s)`, the engine's text follows verbatim, and no client can hide it as an error string. |
|
|
137
|
+
|
|
138
|
+
No partial render is ever emitted: `svg` is null whenever there are errors —
|
|
139
|
+
determinism over convenience.
|
|
140
|
+
|
|
141
|
+
## Where MCP fits awkwardly, stated plainly
|
|
142
|
+
|
|
143
|
+
- **A tool that wants to return two things.** A build is an SVG *and* its
|
|
144
|
+
metadata; a read is a model *and* the contract for using it. MCP's result is
|
|
145
|
+
an ordered list of content blocks, which carries them — but the blocks are
|
|
146
|
+
untyped text, so "this block is the SVG and that one is the model" can only
|
|
147
|
+
be said in prose inside the blocks. Structured output (`structuredContent` +
|
|
148
|
+
`outputSchema`) would type it, at the cost of client compatibility across
|
|
149
|
+
protocol revisions; this server puts machine-readable payloads in the text
|
|
150
|
+
blocks as JSON instead, which every revision carries identically.
|
|
151
|
+
- **The diagnostic does not fit the error shape.** MCP has one error flag and
|
|
152
|
+
two meanings to spend it on ("your request was malformed" and "your document
|
|
153
|
+
is malformed"). Those need opposite handling — the first is the caller's bug,
|
|
154
|
+
the second is the caller's *work product* and the whole reason the tool
|
|
155
|
+
exists — so `isError` is reserved for the first and the second is a normal
|
|
156
|
+
result. Anyone reading a transcript should know that a clean-looking result
|
|
157
|
+
can still say `PARSE FAILED`.
|
|
158
|
+
- **Protocol version.** Every payload here is plain text content blocks, which
|
|
159
|
+
every revision carries identically, so there is nothing to negotiate: the
|
|
160
|
+
server echoes the revision the client asked for.
|
|
161
|
+
|
|
162
|
+
## Test
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
node integrations/mcp-server/test.js # npm run gate:mcp
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
It speaks real JSON-RPC to a spawned subprocess (framing is the half that was
|
|
169
|
+
hand-rolled, and an in-process test cannot catch a framing bug), then exercises
|
|
170
|
+
every tool against real figures from `examples/` — a multi-section topology, an
|
|
171
|
+
experimental `figdown 0.2 statechart`, a published artifact read back through
|
|
172
|
+
its own metadata, and a deliberately broken document. Two of its assertions are
|
|
173
|
+
drift guards rather than feature tests: every genre in SKILL.md's router must
|
|
174
|
+
still resolve to files that exist, and this server must contain no engine copy.
|