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
|
@@ -166,10 +166,12 @@ flowline check -> accept class=yes
|
|
|
166
166
|
flowline check -> reject class=no
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
A class a flowline joins MUST declare `
|
|
170
|
-
|
|
171
|
-
legal
|
|
172
|
-
|
|
169
|
+
A class a flowline joins MUST NOT declare `fill=` with no `stroke=`: that is
|
|
170
|
+
an error naming `stroke=`. A class that declares **no paint at all** is
|
|
171
|
+
legal — it claims a meaning, draws it in the derived legend with no swatch,
|
|
172
|
+
and leaves the flowline its default line. `fill=` and `stroke=` are legal on
|
|
173
|
+
any drawn element, but a flowline has no interior, so `fill=` on one is an
|
|
174
|
+
error.
|
|
173
175
|
|
|
174
176
|
The connector's operators are `->`, `<-`, `--` and `<->`. In a procedure you
|
|
175
177
|
almost always want `->`: a step follows another step in one direction, and an
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Genre `sequence` — EXPERIMENTAL
|
|
2
|
+
|
|
3
|
+
The genre is outside the conformance surface: it may change or be
|
|
4
|
+
**withdrawn** in a later `0.x` without a migration. There is no portable
|
|
5
|
+
rewrite of a sequence figure — a scene genre cannot carry time order — so a
|
|
6
|
+
figure written here is written here.
|
|
7
|
+
|
|
8
|
+
**It draws a ladder.** Participants are columns, left to right in `lifeline`
|
|
9
|
+
declaration order; time runs down the page, one row per `message` or `state`
|
|
10
|
+
in the order you wrote them. A `fragment` is a frame around the rows that
|
|
11
|
+
name it, with its operator in a tab at the top-left corner; an `operand` is a
|
|
12
|
+
compartment of that frame, as wide as the frame, divided from the one above
|
|
13
|
+
by a dashed rule, with its guard drawn `[in brackets]` at the left; a `state`
|
|
14
|
+
is a pill centred on its own column. A message to and from the same lifeline
|
|
15
|
+
draws a small rectangular loop; a message between non-adjacent columns is one
|
|
16
|
+
straight shaft over the lifelines it crosses, which are left intact; `<->`
|
|
17
|
+
draws ONE shaft with two heads, because it is one message and not two.
|
|
18
|
+
|
|
19
|
+
**You place nothing.** This genre has no key that moves a coordinate — not
|
|
20
|
+
`flow`, not `rank`, not a position. `layout` and `pin` still parse, because
|
|
21
|
+
they are the language's and no genre may redefine them, and a `pin` here
|
|
22
|
+
changes nothing at all: the drawing is identical with and without it.
|
|
23
|
+
Everything you can change about the figure, you change by changing what the
|
|
24
|
+
source SAYS.
|
|
25
|
+
|
|
26
|
+
**There are no activation bars.** UML draws a bar for the period a
|
|
27
|
+
participant is executing; that is a different thing from a message, this
|
|
28
|
+
genre has no word for it, and the renderer will not invent one out of which
|
|
29
|
+
messages happen to be adjacent.
|
|
30
|
+
|
|
31
|
+
**It needs `figdown 0.4` on line 1.** `figdown 0.3 sequence` is a line error,
|
|
32
|
+
and a deliberately named one: `genre "sequence" requires figdown 0.4`.
|
|
33
|
+
|
|
34
|
+
## The vocabulary, which is five words
|
|
35
|
+
|
|
36
|
+
**This file is `sequence`'s whole vocabulary.** Load it and you can author in
|
|
37
|
+
this genre; do not load another genre's file, because another genre's
|
|
38
|
+
declaration of a word is not authority for this one.
|
|
39
|
+
|
|
40
|
+
| Line | What it declares |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `lifeline <id> ["label"]` | a participant column — this genre's word for a node |
|
|
43
|
+
| `message <a> -> <b> ["label"]` | one message from `<a>` to `<b>` — this genre's word for a connector |
|
|
44
|
+
| `state <lifeline-id> "<state name>"` | the named state a lifeline is in from here on |
|
|
45
|
+
| `fragment <id> ["label"] type=<operator>` | a combined fragment — a framed region of the exchange |
|
|
46
|
+
| `operand <id> ["guard"] in=<fragment-id>` | one compartment of a fragment |
|
|
47
|
+
|
|
48
|
+
All five are taken from OMG UML 2.5.1 clause 17, which is this genre's source
|
|
49
|
+
standard, and every one of them is published by ISO as well, in ISO/IEC
|
|
50
|
+
19505-2 clause 14. Three more words are legal because they are not about
|
|
51
|
+
exchanges at all: `class` (what a colour means), `title`, and `layout`/`pin`
|
|
52
|
+
— though a `pin` has nothing to place here.
|
|
53
|
+
|
|
54
|
+
**`flow` and `rank` are NOT this genre's keywords**, and that is a
|
|
55
|
+
consequence rather than an omission: both axes are already ordered by the
|
|
56
|
+
source. Columns are in `lifeline` declaration order; time runs down the page
|
|
57
|
+
in the order the `message` and `state` lines are written. A key that
|
|
58
|
+
reordered either would make the drawing disagree with the text.
|
|
59
|
+
|
|
60
|
+
**A `bitfield`, `table` or `timing` region cannot be opened here either.** A
|
|
61
|
+
scene genre can host one because it has a scene to host it in; a ladder has
|
|
62
|
+
no scene, so those three openers are line errors under `sequence`. Put the
|
|
63
|
+
companion table in its own section with its own header line.
|
|
64
|
+
|
|
65
|
+
## Order is declaration order, and it is TOTAL
|
|
66
|
+
|
|
67
|
+
The figure's time axis is the `message` and `state` lines **in the order you
|
|
68
|
+
wrote them**. There is one order, it is total, and every reader gets the same
|
|
69
|
+
one. UML and ITU-T Z.120 both define a *partial* order — total along each
|
|
70
|
+
participant's own axis only, with cross-participant order coming from the
|
|
71
|
+
messages themselves. FigDown asserts more than either, deliberately, so that
|
|
72
|
+
one source never admits two equally legal drawings and a reader never has to
|
|
73
|
+
compute an ordering. **Write the lines in the order the events happen.**
|
|
74
|
+
|
|
75
|
+
The one way to say that two messages are NOT ordered is a `par` fragment
|
|
76
|
+
around them. That makes `type=par` part of the order model rather than a
|
|
77
|
+
convenience: without it, adjacent means earlier-than, always.
|
|
78
|
+
|
|
79
|
+
## `message`: three operators, and `--` is an error
|
|
80
|
+
|
|
81
|
+
```figdown
|
|
82
|
+
figdown 0.4 sequence
|
|
83
|
+
lifeline c "Client"
|
|
84
|
+
lifeline s "Server"
|
|
85
|
+
message c -> s "DHCPDISCOVER"
|
|
86
|
+
message c <- s "DHCPOFFER"
|
|
87
|
+
message c <-> s "keepalive"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`->` and `<-` give the direction; `<->` is a sustained two-way exchange whose
|
|
91
|
+
individual messages are not enumerated — one message, drawn with one shaft
|
|
92
|
+
and two heads. **`--` is a line error here** and legal in every other genre:
|
|
93
|
+
a message has a sending event and a receiving event, so one with no direction
|
|
94
|
+
is not a thing this domain has.
|
|
95
|
+
|
|
96
|
+
The trailing quoted string is the message text, and it is where to write it.
|
|
97
|
+
The shared connector grammar also accepts a label in the inline bracket
|
|
98
|
+
position — `message c -[DHCPDISCOVER]-> s` — and both spellings reach the
|
|
99
|
+
same one field, so **writing both on one line is a line error**. Prefer the
|
|
100
|
+
trailing form; the bracket position reads as an annotation on the line rather
|
|
101
|
+
than as the message itself. `[tail]` and `[head]` are a different thing and
|
|
102
|
+
are unaffected: they label the two ends, not the message.
|
|
103
|
+
|
|
104
|
+
Both endpoints must be declared `lifeline` ids.
|
|
105
|
+
|
|
106
|
+
## `state`: slot 1 REFERENCES a lifeline
|
|
107
|
+
|
|
108
|
+
```figdown
|
|
109
|
+
figdown 0.4 sequence
|
|
110
|
+
lifeline c "Client"
|
|
111
|
+
state c "BOUND"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This is the one place a word means something different from what it means in
|
|
115
|
+
another genre. Under `statechart`, `state locked "LOCKED"` **declares** the id
|
|
116
|
+
`locked`. Under `sequence`, `state c "BOUND"` **references** the lifeline `c`
|
|
117
|
+
and declares nothing: nothing here refers to a state occurrence, so it needs no
|
|
118
|
+
id of its own. The quoted name is **mandatory** — a state occurrence with no
|
|
119
|
+
name asserts nothing.
|
|
120
|
+
|
|
121
|
+
Two consecutive `state` lines naming the same lifeline and the same name are a
|
|
122
|
+
line error: a state that has not changed is never restated.
|
|
123
|
+
|
|
124
|
+
## `fragment` and `operand`: `type=` is mandatory
|
|
125
|
+
|
|
126
|
+
```figdown
|
|
127
|
+
figdown 0.4 sequence
|
|
128
|
+
lifeline c "Client"
|
|
129
|
+
lifeline s "Server"
|
|
130
|
+
fragment renew "renewal cycle" type=loop
|
|
131
|
+
operand t1 "while the lease is live" in=renew
|
|
132
|
+
message c -> s "DHCPREQUEST" in=t1
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`type=` takes one of **twelve** values, bare, taken whole from UML's
|
|
136
|
+
`InteractionOperatorKind`:
|
|
137
|
+
|
|
138
|
+
`alt` `opt` `loop` `par` `strict` `seq` `critical` `neg` `assert` `ignore`
|
|
139
|
+
`consider` `break`
|
|
140
|
+
|
|
141
|
+
There is **no default**. UML gives the attribute one (`seq`) and FigDown does
|
|
142
|
+
not, because a default draws a frame that looks like an assertion and is not
|
|
143
|
+
one. An `operand` is a compartment **of** a fragment, so its `in=` is
|
|
144
|
+
mandatory, and its quoted string is the guard. A fragment or an operand with
|
|
145
|
+
no members is a line error: a container's extent is the span of the lines
|
|
146
|
+
that name it, and a container with no extent asserts nothing.
|
|
147
|
+
|
|
148
|
+
## `in=`: five acceptors, one meaning, one level
|
|
149
|
+
|
|
150
|
+
`in=` means *the element this one lives inside*, and its value is always a
|
|
151
|
+
`fragment` or an `operand` id. It is accepted on **`message`, `operand`,
|
|
152
|
+
`lifeline`, `state` and `fragment`** — five acceptors, all the same meaning.
|
|
153
|
+
An `operand`'s `in=` is narrower still: a compartment belongs to a FRAGMENT,
|
|
154
|
+
never to another compartment.
|
|
155
|
+
|
|
156
|
+
Three rules come with it:
|
|
157
|
+
|
|
158
|
+
- **Members must be CONTIGUOUS** in declaration order. An operand denotes the
|
|
159
|
+
ordered run of the occurrences it contains, so an occurrence that is not in
|
|
160
|
+
it cannot happen between two that are. A line that splits a run is a line
|
|
161
|
+
error naming the run.
|
|
162
|
+
- **Fragment nesting is capped at ONE level.** A fragment may sit in an
|
|
163
|
+
operand of one enclosing fragment and no deeper. Write a deeper interaction
|
|
164
|
+
as a sibling fragment, or state it in `description=`.
|
|
165
|
+
- **Containment is a tree.** A container that ends up inside itself is a line
|
|
166
|
+
error, because a cycle denotes nothing.
|
|
167
|
+
|
|
168
|
+
## Three things this genre refuses, and what to write instead
|
|
169
|
+
|
|
170
|
+
Each is a line error with a named diagnostic — the engine tells you the ground
|
|
171
|
+
and the replacement. They are listed here so you do not reach for them.
|
|
172
|
+
|
|
173
|
+
- **`gap` — refused.** The vertical axis is not proportional: non-zero time has
|
|
174
|
+
already passed between every adjacent pair of events, so the line would say
|
|
175
|
+
nothing new, and *draw it further apart* is a rendering request. Put the
|
|
176
|
+
elapsed time into the following message's label or its `description=`.
|
|
177
|
+
- **`group` — refused.** UML clause 17 has no lifeline-grouping construct, and
|
|
178
|
+
a band across a column span would silently enclose non-members. Declare a
|
|
179
|
+
`class` naming what the participants have in common and put `class=` on each
|
|
180
|
+
`lifeline`: it asserts membership without asserting adjacency, and it earns a
|
|
181
|
+
legend entry.
|
|
182
|
+
- **`lost=` — refused, and no option key was added for it.** For a message that
|
|
183
|
+
was sent and not delivered, declare `class dropped "Sent, never delivered"`,
|
|
184
|
+
put `class=dropped` on the message, and put the per-message reason in
|
|
185
|
+
`description=`.
|
|
186
|
+
|
|
187
|
+
## A whole document
|
|
188
|
+
|
|
189
|
+
```figdown
|
|
190
|
+
figdown 0.4 sequence
|
|
191
|
+
title "Address lease and renewal"
|
|
192
|
+
class dropped "Sent, never delivered" stroke=#b91c1c style=dashed
|
|
193
|
+
lifeline c "DHCP client"
|
|
194
|
+
lifeline s "DHCP server"
|
|
195
|
+
message c -> s "DHCPDISCOVER"
|
|
196
|
+
message c <- s "DHCPOFFER"
|
|
197
|
+
state c "BOUND"
|
|
198
|
+
fragment renew "renewal cycle" type=loop
|
|
199
|
+
operand t1 "when the renewal timer fires" in=renew
|
|
200
|
+
message c -> s "DHCPREQUEST" in=t1 class=dropped description="the issuing server does not answer"
|
|
201
|
+
state c "RENEWING" in=t1
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Presentation is the same everywhere: `fill=` the interior, `stroke=` the
|
|
205
|
+
outline and the whole of a line, `class=` one or more declared class ids
|
|
206
|
+
comma-separated, and `style=solid|dashed|dotted` for the dash pattern.
|
|
207
|
+
`fill=` is an error on a `message`, on a `fragment` and on an `operand` — a
|
|
208
|
+
line has no interior, and a frame drawn over messages must not hide them.
|
|
209
|
+
`shape=` is accepted by nothing here: every shape in the ladder is the
|
|
210
|
+
engine's. `note=` is a drawn aside in prose and `description=` is authored
|
|
211
|
+
documentation that is never drawn; neither is where a structural fact lives.
|
|
212
|
+
A `fill=`-only class joined by a `message`, a `fragment` or an `operand` is a
|
|
213
|
+
**line error**: a line has no interior, so that class reaches nothing on it —
|
|
214
|
+
write `stroke=`. (This was accepted, when the check ran over
|
|
215
|
+
`edge`s only; it reaches every collection that takes `class=`.) A class that declares **no paint at all** is legal on any
|
|
216
|
+
member and always was here: it draws its meaning in the derived legend with
|
|
217
|
+
no swatch, which is what `class dropped "Sent, never delivered"` does in the
|
|
218
|
+
reference figure. Read a paint-less class as a claim about MEANING, never as
|
|
219
|
+
a missing colour.
|
|
220
|
+
|
|
221
|
+
## What you may conclude from a `sequence` figure
|
|
222
|
+
|
|
223
|
+
- **Every `message` is a message**, its direction is the direction it travels,
|
|
224
|
+
and its position in the source is its position in time.
|
|
225
|
+
- **The order is total.** Two messages written one after the other happened in
|
|
226
|
+
that order.
|
|
227
|
+
- **`<->` is ONE message**, an exchange in both directions whose individual
|
|
228
|
+
messages are not enumerated — never a count of two.
|
|
229
|
+
- **The set of participants is closed.** A participant not declared is not
|
|
230
|
+
asserted to exist. Column position is declaration order and means nothing
|
|
231
|
+
else: not seniority, not layering, not a path.
|
|
232
|
+
- **A `state` line is the condition its lifeline is in from that point on**,
|
|
233
|
+
as free text — quotable, attributable, never parsable.
|
|
234
|
+
- **A `fragment` says what KIND of run its members are**, and its `type=` is
|
|
235
|
+
that claim; an `operand`'s quoted string is the guard on that compartment.
|
|
236
|
+
|
|
237
|
+
**Still not conclusible:**
|
|
238
|
+
|
|
239
|
+
- **No elapsed time, ever.** Vertical distance means only that non-zero time
|
|
240
|
+
passed. There is no duration, no timestamp and no rate in this genre.
|
|
241
|
+
- **No concurrency unless a `par` fragment says so.** Two adjacent messages are
|
|
242
|
+
ordered, not simultaneous.
|
|
243
|
+
- **No failure, no retransmission and no loss** unless a `class` says so in
|
|
244
|
+
words. There is no keyword for any of them, and a `class` meaning is prose:
|
|
245
|
+
attribute it to the prose, not to the model.
|
|
246
|
+
- **No activation, no execution period, no "busy".** Nothing is drawn for it
|
|
247
|
+
and nothing may be inferred from message adjacency.
|
|
248
|
+
- **No identity** between two messages with the same label, and none between a
|
|
249
|
+
state name here and a state in a statechart document. A shared `class` is a
|
|
250
|
+
shared category, never a shared identity.
|
|
251
|
+
- **Nothing from the layout zone.** Here that rule is total rather than
|
|
252
|
+
conventional: a `pin` changes no coordinate, because there is none to change.
|
|
@@ -79,9 +79,11 @@ spaces. Values from a fixed list are bare (`shape=box`, `style=dashed`,
|
|
|
79
79
|
`shape=` is pure geometry and changes only the drawing — `shape=box` (the
|
|
80
80
|
default), `shape=rounded`, `shape=circle`, `shape=ellipse`, `shape=diamond`,
|
|
81
81
|
`shape=cylinder`. `style=` takes `style=solid`, `style=dashed` or
|
|
82
|
-
`style=dotted`. A class a transition joins MUST declare `
|
|
83
|
-
`
|
|
84
|
-
|
|
82
|
+
`style=dotted`. A class a transition joins MUST NOT declare `fill=` with no
|
|
83
|
+
`stroke=` — that is an error naming `stroke=`; a class that declares **no
|
|
84
|
+
paint at all** is legal and claims only a meaning, which the derived legend
|
|
85
|
+
draws with no swatch. `fill=` on a transition is an error, because a line has
|
|
86
|
+
no interior. Under `figdown 0.3` a line may carry `note=`, a drawn aside in
|
|
85
87
|
prose — never parsable, never where a structural fact lives.
|
|
86
88
|
|
|
87
89
|
## What the genre adds, which is a reading and not a syntax
|
|
@@ -73,10 +73,12 @@ with what it *is* — "the internet", "the transit provider" — written in the
|
|
|
73
73
|
label. `style=` takes `style=solid`, `style=dashed` or `style=dotted`.
|
|
74
74
|
|
|
75
75
|
`class` declares what a colour **means** and is the only way to make colour
|
|
76
|
-
carry meaning; a class an edge joins MUST declare `
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
carry meaning; a class an edge joins MUST NOT declare `fill=` with no
|
|
77
|
+
`stroke=` — that is an error naming `stroke=`. A class that declares **no
|
|
78
|
+
paint at all** is legal: it claims a meaning, draws it in the derived legend
|
|
79
|
+
with no swatch, and leaves the edge its default line. `fill=` and `stroke=`
|
|
80
|
+
are legal on any element, but an `edge` has no interior, so `fill=` on one is
|
|
81
|
+
an error.
|
|
80
82
|
|
|
81
83
|
Endpoint labels nest (`edge a [xe-0/0/0:1] -- b`); quote them when they hold
|
|
82
84
|
a line break or an unbalanced bracket (`edge a ["slot\n1/1"] -- b`). A `#`
|
|
@@ -94,14 +94,19 @@ language, so reading any of the three out of a figure is invention.
|
|
|
94
94
|
|
|
95
95
|
## What you MUST NOT infer
|
|
96
96
|
|
|
97
|
-
- **Ignore the layout
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
- **Ignore the layout namespace.** Its one member today is `pin`. Ignore every
|
|
98
|
+
member **wherever it appears** — membership decides, never position. A `pin`
|
|
99
|
+
written *before* the `layout` line is ignored on exactly the same terms as
|
|
100
|
+
one written after it, and about half of them are written before it. The
|
|
101
|
+
members are geometry, they exist only to stabilise the SVG, and no keyword
|
|
102
|
+
turns this on or off. *(One exception, and it is a workaround for a missing
|
|
103
|
+
construct: if the layout looks load-bearing — dense pins forming a stack, a
|
|
104
|
+
grid or a map, where stack order, cross-layer alignment or spans are the
|
|
105
|
+
point — read it, and say in your summary that you did. Such a document has a
|
|
106
|
+
semantics gap.)* The `layout` line, and an older `# --- layout` comment in a
|
|
107
|
+
document that has no `layout` line, are a **reader's signpost** and not the
|
|
108
|
+
test: they say where the author collected the geometry, which is useful and
|
|
109
|
+
is not what decides.
|
|
105
110
|
- **Arrangement is not precedence.** Declaration order records the author's
|
|
106
111
|
statement order — a focus and reading-order signal. It is not a ranking, a
|
|
107
112
|
priority or a sequence.
|
|
@@ -122,7 +127,7 @@ language, so reading any of the three out of a figure is invention.
|
|
|
122
127
|
one file may legitimately carry sections at different versions. The
|
|
123
128
|
permanent exceptions: `figdown`, `title`,
|
|
124
129
|
`layout` and `pin` mean the same thing everywhere — which is exactly what
|
|
125
|
-
lets you skip the layout
|
|
130
|
+
lets you skip the layout namespace without first knowing the genre.
|
|
126
131
|
- **A shared `class` is a shared category, not an identity.** The language
|
|
127
132
|
has no equivalence or alias relation.
|
|
128
133
|
- **Status is not visible in the syntax.** A line that parses tells you
|
|
@@ -137,9 +137,14 @@ edge check -> accept class=yes
|
|
|
137
137
|
edge check -> reject class=no
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
A class an edge joins MUST declare `
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
A class an edge joins MUST NOT declare `fill=` with no `stroke=`: an edge has
|
|
141
|
+
no interior, so that class reaches nothing on it — write `stroke=`, and a
|
|
142
|
+
`style=` beside the `fill=` does not answer for it. A class that declares
|
|
143
|
+
**no paint at all** is legal on every member: it draws its meaning in the
|
|
144
|
+
derived legend with no swatch, and the edge keeps its default line. Read a
|
|
145
|
+
paint-less class as a claim about MEANING, never as a missing colour. One
|
|
146
|
+
class may carry both keys and serve nodes and edges at once — do not split
|
|
147
|
+
it.
|
|
143
148
|
|
|
144
149
|
**Never invent a label.** If the shape genuinely carries no text — a
|
|
145
150
|
junction, a bare glyph — write an explicitly empty label: `node j ""`. It
|