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.
@@ -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) still parse and
17
- are not deprecated, but they sit **outside** the v0.1 compatibility promise
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 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`) |
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 | nodes as parties + labelled directed edges; ordinal labels for ordering | strict ordering: number labels consistently (`FLOWCHART-GENRE-DESIGN`) — see Known limits |
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 | directed edges with ordinal mid-labels (`-[1: SYN]->`) | number labels consistently; no sequence genre yet |
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
- - **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`).
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 it has exactly **one** member — `pin`, NORMATIVE. `layout` is not a
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 `table`
107
- section usually has no `flow`/`rank`/`pin` — geometry follows content. For a
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 pair below shows lint getting *worse* (ink/e 109→117) while the figure becomes dramatically more readable — 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.
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
- Sixteen-node EVPN-VXLAN IRB figure with three leaf groups. Auto-layout lets group boxes overlap each other and member nodes escape their frames, producing a tall chaotic column (691×955 px). 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). Aspect ratio flips from portrait to landscape. Lint ink/e gets *worse* (109→117) while the figure becomes unambiguous the metrics do not measure group containment or overlap.
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 | 0 | 1 | 109 | 3 |
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) · [auto .svg](../examples/layout-compare/srl-evpn-irb-auto.svg) · [tuned .fd](../examples/srl-evpn-irb.fd) · [tuned .svg](../examples/srl-evpn-irb.svg)
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 `fill=`-only class joined by an edge is a line error (spec/core.md §5,
483
- `INTERIOR-LESS-ELEMENT-PAINT`/`CLASS-PAINT-REQUIREMENT`).
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
- interim until the sequence genre lands.
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):** message *order* rides on the 1/2/3
133
- > label ordinals, not a first-class construct — `MEANING-RECOVERY-SOURCE` does not treat numbering as
134
- > semantics. The v0.2 sequence genre (`FLOWCHART-GENRE-DESIGN`) closes this; see
135
- > [expressing.md](expressing.md), "strict message ordering".
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:** step order (1/2) rides on label ordinals, pending the v0.2
291
- > sequence genre (`FLOWCHART-GENRE-DESIGN`).
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 the language does not yet make first-class:
423
- **message/step ordering** in the handshake and ARP is carried by numbering edge
424
- labels `1:`/`2:`/`3:`, which `MEANING-RECOVERY-SOURCE` treats as naming, not semantics — the v0.2
425
- sequence genre (`FLOWCHART-GENRE-DESIGN`) will fix it, and the sources say so. Relatedly, the RFC
426
- 9293 handshake carries **per-endpoint lifeline state** (LISTEN, SYN-SENT, …)
427
- that the topology genre has no construct for; the companion state table section
428
- carries it as multi-section composition (`MULTI-FIGURE-DOCUMENTS` main-standard interim, again
429
- pending the sequence genre, `FLOWCHART-GENRE-DESIGN`).
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 `skill`. Omit it
122
- for the index. `experimental: true` adds the genre's EXPERIMENTAL files, which sit
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 published artifact read back through
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, and this server must contain no engine copy.
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
- + ' [' + ['nodes', 'edges', 'groups', 'classes', 'blocks', 'boundaries']
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
- ].join('\n');
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.2",
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]));\"",
@@ -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. **One correction applies to it.** `reference/reading.md` is
123
- a byte-frozen copy of a released reading contract, and one sentence in it is
124
- wrong: where it says *"Ignore the layout zone. Everything from the `layout`
125
- keyword down is …"*, do not read it as a rule about **position**. Ignore the
126
- layout **namespace** — its one member is `pin` — **wherever a member
127
- appears**, because a `pin` may legally sit *before* the `layout` line, and
128
- about half of them do. Membership decides, not position. The frozen file is
129
- left as-is on purpose; apply this rule instead, the same one stated above
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(docs.map(d => engine.render(d, RENDER_OPTS)))
43
- : engine.render(docs[0], RENDER_OPTS).svg;
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