# FigDown Core Syntax — v0.1

> Status: **v0.1 core framework** (`GENRE-DOCUMENT-CONTRACT`). Working text, 2026-08-05.
> Renamed from `spec/core.md` in the **spec split**: this file is the
> **cross-genre** contract (skeleton, core keywords, layout zone, composition
> rules, presentation, errors, registry, ABNF, semantic model, stability
> policy). Per-genre vocabulary lives only under
> [genres/](genres/README.md) — core + one genre doc suffice to author and
> read that genre.
>
> **This file is FROZEN material, and it defines nothing experimental
>.** The three EXPERIMENTAL constructs — `bundle`,
> `threshold`, `band` — are DECLARED by the genre that has each:
> `threshold` and `band` by [genres/block.md](genres/block.md), `bundle` by
> [genres/experimental/topology.md](genres/experimental/topology.md), with
> [experimental.md](experimental.md) holding their status, model and
> history; the three EXPERIMENTAL genres are in
> [genres/experimental/](genres/experimental/). This file still **names**
> them, in §10's registry and in marked cross-references, because a closed
> language has to say what exists; it does not depend on them. (There were
> six until 0.1, when `EDGE-GEOMETRY-CONSTRUCTS` **WITHDREW `path` and `routing`** — and
> their option keys `points=`, `tailport=`, `headport=`, `routing=` — from
> the language outright; and four until 0.3, when `PAINT-ORDER-CONSTRUCT` **WITHDREW
> `plane`** — and its option keys `plane=` and `z-index=`. All three
> withdrawals took EXPERIMENTAL constructs outside the freeze scope, so no
> compatibility promise is broken; the need `path`/`routing` served is on
> file as §9 **`EDGE-IDENTITY-AND-GEOMETRY`**, and `plane`'s reservation is `RESERVED-SPELLINGS`'s `network`,
> RFC 8345 §4.1/§6.1, held against a layering relation and not spent.)
> Delete the experimental file set and what remains here is complete — the
> criterion `tools/isolation-check.js --strict` tests on every run.
>
> **Adopting FigDown? Read [§13 Stability and versioning](#13-stability-and-versioning-normative)
> first.** It is the shortest section and the one an adopter most needs:
> what a document you write today is promised, what it is not, and what is
> committed to instead. In particular, **0.x is a preview and is NOT
> stable**, and **"frozen" does not mean "stable"** (§13.2).
>
> Derived from requirements-notes.md
> (`IMAGE-LOCKED-KNOWLEDGE`–`ELEMENT-GEOMETRY-DIRECTIVE`, `RENDERING-DETERMINISM`–`RELEASE-FREEZE-CRITERIA`; the maintainer-ruling item codes cited here — `EDGE-WRITTEN-FORM`,
> `MARKER-TARGET-KINDS`–`EMPTY-LABEL-STATE` — are defined in that file's
> A-code registry).
> Type priorities follow the figure-type census —
> census.md.
>
>
> **Section numbers §0–§15 are stable.** Genre-specific prose that used to
> live in §2 / §4.x is now a pointer (or a short composition rule). Do not
> renumber when moving content — update the pointer target instead. §14
> (legibility floor) and §15 (security considerations) were appended after
> §0–§13 froze; appending is how this file grows.

## 0. Design constraints this syntax must satisfy

From the requirements log, the syntax is boxed in by:

1. **Closed, line-oriented grammar** — every non-blank, non-comment
   line begins with a registered line-start token (a keyword, or `|`
   for table rows); unknown lines are errors carrying a 1-based line number
   (powers the AI write→validate→fix loop). (`CLOSED-GRAMMAR`)

   **Which message.** A conforming implementation MUST reject
   the line, and the diagnostic depends on what is known at that point:
   under a DECLARED, valid genre the message names the genre — `"zzz" is
   not allowed in genre block` — because the genre allowlist is the
   narrower closed set and naming it tells the author where to look; with
   no valid genre in force (a missing or unknown genre token) the message
   is `unrecognized line`. Earlier revisions of this section promised the
   second message unconditionally, which no implementation can deliver:
   the allowlist necessarily fires first.
2. **Mechanically renderable** — a plain program (no LLM) converts text
   to SVG, deterministically. (`RENDERING-DETERMINISM`)
3. **Rigid/flexible attribute model** — every attribute either carries an
   explicit value (rigid constraint, renderer must honor) or is absent
   (renderer adapts, spillover minimized). (`LAYOUT-STABILITY`, `UNDECLARED-ATTRIBUTE-BEHAVIOUR`)
4. **Defaults = the statistically common case** — most figures should
   need no supplementary declarations. (`DEFAULT-VALUE-SELECTION`)
5. **Token-lean teachability** — the core must fit in a ~100-line
   authoring prompt. Borrow syntax conventions AI already knows
   (Mermaid, D2, DOT, WaveDrom) wherever possible. (`AGENT-TEACHING-COST`, `DESIGN-DECISION-METHOD`)
6. **Static first** — dynamic (page/step sequences) reserves keywords but
   is out of scope for v0. (`STATIC-DYNAMIC-PRIORITY`, `DYNAMIC-FIGURE-PURPOSE`)

### 0.1 Requirement keywords — BCP 14 (normative, `NORMATIVE-KEYWORD-DECLARATION`)

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in BCP 14
[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119)
[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) when, and only when,
they appear in all capitals, as shown here.

That is RFC 8174 §2's replacement wording, taken whole rather than
reconstructed. RFC 2119's own 1997 phrase was NOT copied: it omits
"NOT RECOMMENDED", which §4 of that same RFC defines, and it carries no
capitalization caveat — which is the entire subject of RFC 8174.

**Placed here, above §13, and that is the load-bearing half.** The
declaration lived in §13's opening paragraph from the first draft until
`NORMATIVE-KEYWORD-DECLARATION`, where it named four of the eleven spellings and cited one of the two
RFCs. Measured at the time of that ruling, `spec/core.md` carried **219**
all-caps keyword occurrences outside code fences, **44** of them inside
§13 — so **175, four in five, sat outside the only paragraph that said
what the words meant**. A declaration whose scope is one section of a
document cannot bind the other fourteen. This subsection's scope is the
document, and §13 now points here rather than declaring for itself.

**What this does NOT do to the lowercase modals, stated because the
opposite reading is the expensive one.** RFC 8174 §2: *"These words can be
used as defined here, but using them is not required. Specifically,
normative text does not require the use of these key words,"* and *"When
these words are not capitalized, they have their normal English meanings
and are not affected by this document."* This tree holds **779** lowercase
modal occurrences in its English `spec/*.md` files. **None of them is
promoted, reviewed or rewritten by this subsection.** They remain normative
English prose, and adopting BCP 14 creates no obligation to convert one of
them. What the adoption does bind is the all-caps occurrences — **567** in
this document set on the day it landed.

**This document's convention, declared.** `core.md` uses the eleven
spellings above in ALL CAPITALS, in prose. A keyword inside a fenced code
block, inside an inline code span, or inside a quotation is not a
requirement of this document; and where this document *mentions* a keyword
rather than using it — *"turning that MAY into a MUST would be a stricter
rule"* — the sentence is about the word. The three profile documents follow
a different practice and each declares it; see `spec/host-profile.md`,
`spec/figdown-manifest.md` and `spec/figdown-a11y.md`.

**SCHEDULED — the audit this subsection creates (`NORMATIVE-KEYWORD-DECLARATION`).** Adopting BCP 14
turns every existing all-caps occurrence into a claim on the day the
sentence lands, so the audit is not optional and is named here rather than
discovered later. Denominator: **567** occurrences in this document set,
unit = one occurrence, priced at roughly **9 hours** at the rate a
63-occurrence sample was classified. A 71% correct rate (95% CI 60–83%) was
measured on that sample; the largest non-correct class, at 14%, is the
keyword being MENTIONED rather than used, which is **expected and is not a
defect**. `tools/bcp14-check.js` (`gate:bcp14`) holds the three parts of
this that a machine can check, and states in its own header the parts it
must not pretend to check. The audit itself is tracked in
`decisions/registry.md` `NORMATIVE-KEYWORD-DECLARATION` and in `decisions/registry.md` L1.
**It is now ONE pass, merged with §0.2's actor pass (`CONFORMANCE-CLASS-LIST`)** — the two ask
different questions of the same sentence, *"is this a genuine requirement or
a mention?"* and *"who is bound by it?"*, and a reader holding the sentence
for the first is already holding what the second needs. Merged price
**~12 hours** against ~18 run separately. Nobody schedules two.

### 0.2 Conformance classes — whom the keywords bind (normative, `CONFORMANCE-CLASS-LIST`)

§0.1 says what the keywords mean. **This subsection says whom they bind.**
RFC 2119 §6 confines the imperatives to *"what is actually required for
interoperation"*, and interoperation is between **parties**, so a keyword
discipline with no party list is incomplete on its own standard's terms.
The classes below are not invented here: they are the parties `spec/`
already names, counted and collapsed in
`decisions/registry.md`
and ruled as `CONFORMANCE-CLASS-LIST`.

**The requirements in this document, in the genre documents and in the three
profiles apply to the conformance classes in the table below, and to no
others. The list is CLOSED**: a party not named here is not a conformance
class of FigDown, for the same reason the language itself is closed (`CLOSED-GRAMMAR`) —
an open list is a promise whose cost is paid by whoever believes it.

| Class | Kind | What it does | Where its obligations are already written |
|---|---|---|---|
| **Parser** | implementation | source → model | §1, §8, §10, §11, §12.1–§12.6, §13.7; each genre's vocabulary and its refusals |
| **Renderer** | implementation | model → artifact | §7, §13.0.3 `RENDERING-DETERMINISM`, §14, §15; each genre's drawing rules |
| **Reading agent** | implementation | model → conclusions | §12.7, plus each genre's own *MAY conclude* / *MUST NOT conclude* pair (`GENRE-DOCUMENT-CONTRACT`) |
| **Host** | implementation | Markdown / pipeline integration | [`host-profile.md`](host-profile.md) §§1–5, whole |
| **Publisher** | implementation | what leaves a repository | [`figdown-manifest.md`](figdown-manifest.md), [`figdown-a11y.md`](figdown-a11y.md) |
| **Document** | **artifact**, not an implementation | a `.fd` source that conforms | §1's header rule, §10's status table (the *portable v0.1* label), each genre's per-line `REQUIRED` / `OPTIONAL` rules |

**Every cell in the last column is a POINTER.** This subsection creates no
obligation of its own and moves no keyword: each class's minimum set is
sentences that were already normative before it landed, and the assembled
sets are listed, with a citation per entry, in the proposal `CONFORMANCE-CLASS-LIST` ruled on.

**Engine and Implementation are LABELS for a conjunction, not classes.**
*Engine* = **Parser ∧ Renderer**; *implementation*, unqualified, means the
same conjunction. The genre status blocks' *"a conforming implementation
MUST support it"* therefore binds the **Parser** (accept the vocabulary) and,
for a construct that draws, the **Renderer** — not a sixth party. Classes
OVERLAP by design: one program is routinely three of them at once.

**The nouns already in the tree, and what each one is.** `spec/` names
eighteen grammatical subjects for its obligations. Every one of them is on
this table, so that an implementer reading any normative sentence can tell
which class — if any — is being addressed:

| Noun as written in `spec/` | Is |
|---|---|
| *parser*, *producer*, *model producer* | the **Parser** |
| *renderer* (and §14's *conformant renderer*) | the **Renderer** |
| *reading agent*, *agent* (reading sense), *reader* (of a model), *consumer* (model sense, `migrations.md`) | the **Reading agent** |
| *host*, *caller*, *viewer*, *agent tool* (display sense) | the **Host** |
| *publisher* | the **Publisher** |
| *document*, *source*, *figure* (as a document) | the **Document** |
| *engine*, *implementation* | a **LABEL** for Parser ∧ Renderer |
| *verifier*, *consumer* (manifest sense) | a **ROLE** inside a profile |
| *author* | not a class — the requirement is the Document's |
| *editor*, *authoring agent*, *authoring tool* | **not a class today**; an authoring surface's own duties are unwritten, and one `SHOULD` repeated in three documents is not a class (`CONFORMANCE-CLASS-LIST`) |
| *tool*, *migration tool*, *the project* | **out of scope** — the maintainers, below |

**A profile MAY name a ROLE local to itself, and MUST NOT create a
conformance class.** Two exist: the manifest profile's **verifier**
([`figdown-manifest.md`](figdown-manifest.md) §4) and its **consumer**
(§2.1(b)). Both are scoped by that document alone. **The manifest's
*consumer* is not §12.7's Reading agent** — it is a party that reads a
*manifest*, where the Reading agent reads the *model* — and the two must not
be read as one class because they share a spelling.

**Author is not a conformance class, and a requirement phrased on an author
is a requirement on the DOCUMENT.** That is the HTML Standard's own rule for
the same situation, adopted here whole rather than reinvented (W3C;
`standards-claims.tsv` `S259`), and it is what gives every
author-phrased sentence a party that can actually comply. Naming the author
in prose stays free; an Author cannot be tested and has nothing to claim.

**The specification's maintainers are named here and declared OUT OF
SCOPE.** **126 of the 590** English all-caps occurrences — 21%, over 113
sentences in [`migrations.md`](migrations.md),
[`syntax-style.md`](syntax-style.md), [`.github/CONTRIBUTING.md`](../.github/CONTRIBUTING.md),
[`ERRATA.md`](ERRATA.md) and [`migrate.md`](migrate.md) — bind **this
project**: what a frozen construct may still change, what a revived spelling
must return as, when a mechanical rewrite is owed. **Nobody implements
FigDown by obeying them**, no conformance claim is made or denied by them,
and a list that appeared to include them would invite an implementer to
think a fifth of the specification is theirs to satisfy.

**A class is ORTHOGONAL to SCOPE (§1.1) and only a READING AID over STAGE.**
A Parser implements core *and* genres; a Renderer likewise: SCOPE says whose
rule it is, a class says whose duty it is, and the cross-product is
populated except that **Host and Publisher have no core and no genre
obligations at all** — they are profile-only classes, which is §1.1's
profile sentence seen from the other axis. Against STAGE the correspondence
is *"Grammar + Context + Resolution → Parser, Semantic → Reading agent,
Rendering → Renderer"*, and it is a reading aid with **three exceptions**
that stop it being a correspondence: (a) Host and Publisher have no stage at
all; (b) the Renderer owes rules whose origin is Semantic — *"the group band
encloses exactly its members"* is a model fact enforced at draw time; (c) the
Document has no stage either, because a document-validity rule is *checked
at* a stage without being *of* one.

**Core-only is not a conformance point.** A Parser or a Renderer claiming
the name supports **core plus the three NORMATIVE genres** (`block`, `bitfield`,
`table`) — which every frozen genre's status block already says in its own
words — while EXPERIMENTAL material stays `MAY` (§10's status tables;
`conformance/STATUS.txt` records the experimental fixtures a second
implementation may skip entirely). There is no claim of the form *"implements
FigDown core and no genre"*, and there is not one because it would make
*"I implemented FigDown"* mean something that cannot read a single figure in
this repository.

**How a claim is checked, per class, stated honestly — including where it
cannot be.** A conformance class nobody can test is a label, so the state of
each instrument is printed here rather than implied:

| Class | What a claimant runs | State |
|---|---|---|
| **Parser** | `conformance/cases/` — one `.fd` per fixture, each paired with a `.model.json` or a `.errors.txt` golden, plus the suite's single `.geometry.txt` golden (`369-pin-complete-cover-refused`); **no `.svg` goldens at all**, so nothing in this class is judged on rendered bytes. `PROOF.md` reports the suite's size; no count of it is typed here | **These fixtures ARE this class's conformance test suite**, said out loud (`CONFORMANCE-CLASS-OBLIGATIONS`): `conformance/README.md`'s first line already calls it *"the parser-conformance suite"* and `INDEPENDENT-IMPLEMENTATION-CRITERION`'s exercise already asks for *"a parser and model producer"* that passes them |
| **Renderer** | §15.2's Safe SVG list and §14's F1–F6, both stated as decidable from the emitted SVG alone; `gate:safesvg`, `gate:legend`, `gate:layout` | **Partly checkable, and honestly so.** `RENDERING-DETERMINISM` determinism is checkable only against the *same* renderer, and this repository's `gate:artifact` re-renders with the reference engine, which is self-reference |
| **Reading agent** | nothing | **NOT CHECKABLE TODAY.** No instrument exists. `tools/comprehension-check.js` scores a *model reading a picture*, not an implementation reading a model, and it is not re-pointed here to make this table look complete |
| **Host** | [`host-profile.md`](host-profile.md) §6's resolution table | Checkable, with the two holes that profile prints itself |
| **Publisher** | `gate:manifest`, `gate:a11y`, `gate:alt`, `figdown-manifest.schema.json` | Checkable |
| **Document** | a Parser | Checkable by construction — which is the argument for the class |

**One spelling, and the second is a filed defect.** The adjective is
**conforming**. §14's *"conformant renderer"* names the Renderer class under
a second spelling; it is not a second class, and it is on the audit's defect
list below rather than fixed silently in a normative sentence.

**SCHEDULED — the actor-tagging audit, MERGED with §0.1's (`CONFORMANCE-CLASS-LIST`).** ONE pass
over the same sentences, priced at **+3 hours** on §0.1's 9 rather than a
second 9. **Grain: per SECTION by default** — 187 `(file, §)` pairs of which
123 are on the live implementation surface, against 499 keyword-bearing
sentences; a document whose every section binds one class is tagged once
(`host-profile.md` already is), and a sentence is tagged on its own only
where its section genuinely mixes classes. The deliverable is **an actor per
section and a defect list**, not a rewrite: tagging a sentence with its party
is not an occasion to move its strength. Until that pass runs, this class
list has not been applied sentence by sentence, which is stated here rather
than implied — **adopt-then-audit, as §0.1 did**. The defects it starts from
are recorded in `decisions/registry.md` `NORMATIVE-SENTENCE-ACTOR`.

## 1. Document skeleton

A FigDown document is a UTF-8 text, one directive per line.

**What ends a line, and what may precede the first one.** A
line ends at **LF (`%x0A`) or CRLF (`%x0D %x0A`)**; both are accepted and a
document written either way yields the **same model**. A **bare CR** is
**not** a line terminator — a CR-only file is one line, and the reference
engine reports a line error on it rather than parsing it. The §11 ABNF
declared `CR` as a third form until 0.1; no implementation ever
provided it and no fixture pinned it, so the grammar was corrected to the
two forms that work. A **U+FEFF byte order mark at the very start of the
document is IGNORED** — it is stripped before the first-significant-line
rule below is applied, so a BOM'd file has the same header, the same line
numbers and the same model as the same file without one. This is stated
because a BOM is invisible: an editor can add one, and a second
implementation that did not strip it would reject documents this one
accepts. Nothing else about U+FEFF is normative; the reference engine is
wider here, and the lenience is recorded in
[conformance/DISCREPANCIES.md](../conformance/DISCREPANCIES.md).

<!-- fence-check: skip -->
```figdown
# comments and blank lines MAY precede the header
figdown 0.1 block               # version header + GENRE, REQUIRED first
                                # SIGNIFICANT line; the genre is REQUIRED
title "L3 Forwarding Datapath"  # optional
...directives...
```

**The header is the first significant line.** Comment lines
and blank lines MAY precede it; the `figdown` line MUST be the first line
that is neither. A provenance block above the header — source document,
image hash, reconstruction method — is common in production corpora and
is legal. Every line after the header is a directive, a table row, a
comment, or blank.

**The spec provenance line (`SPEC-PROVENANCE-LINE`) — a CONVENTION, not a
directive.** A `.fd` file travels away from the repository that produced
it: into a wiki, a ticket, a pasted chat message. It arrives beside a
reader who may never have heard of this format and has nothing telling
them what it is or where it is defined. Published FigDown documents
SHOULD therefore open with one comment line, above the header:

```figdown
# FigDown — figures as text. Spec: https://github.com/FigDown/figdown
figdown 0.1 block
```

It is a **pointer to the standard, which is provenance** — the same kind
of thing as the source document and image hash above, and legal for the
same reason. It is **not** language surface: the parser does not read it,
it carries no meaning to the renderer, and **its absence is not an
error**. Nothing enforces it but a repository's own tooling, which reads
files and can check a comment exactly as well as it could check a
keyword; this repository's `tools/comment-check.js` checks its presence,
wording and position over `examples/` and `figures/`. Test fixtures are
deliberately outside that scope: a fixture's job is to be a minimal
input, and inserting a line shifts the line numbers its expected
diagnostics name.

**The genre token is REQUIRED (`HEADER-GENRE-REQUIREMENT`).** The header names the
document's genre: `block` | `topology` | `flowchart` | `bitfield` |
`table` | `timing` — and, from `figdown 0.2`, `statechart` (`STATECHART-GENRE-SCOPE`), and from `figdown 0.4`, `sequence` (`SEQUENCE-GENRE-VOCABULARY`).
`figdown 0.3` adds **no genre**: it carries one option key
(`note=`, `DRAWN-ANNOTATION-FORM`) and one resolver widening (`MARKER-TARGET-KINDS`), so its genre set is
`0.2`'s exactly. **`figdown 0.5` adds no genre either** (`CONNECTOR-IDENTITY-KEY`): it carries one option key, `id=` on the four scene
connectors, so its genre set is `0.4`'s exactly. Two of the five accepted
versions therefore move the number for a KEY rather than for a dispatch
point, which is §13.0's `Y` doing what it says — added surface of any kind
moves `Y`, and only removal waits for `X`. A header with no genre is a line error. `bitfield`,
`table` and `timing` documents also declare their kind in their content
(they contain the typed block), but `block`, `topology` and `flowchart`
share the SAME vocabulary — `node`, `edge`, `group` — and differ only in
default flow, so the header is the only place such a document states
which kind of figure it is. Omitting it destroys that distinction with
no recoverable fallback.

**The genre set is indexed by the LANGUAGE VERSION (`STATECHART-GENRE-SCOPE`).** The
version token on the header line is not decoration: it selects the genre set
the second token is resolved against. `statechart` exists at `figdown 0.2`
and not at `figdown 0.1`, so `figdown 0.1 statechart` is a line error — and
a **named** one, `genre "statechart" requires figdown 0.2 (this document
declares 0.1) — write: figdown 0.2 statechart`, not `unknown genre`, because
the author's fix is to raise the header and not to hunt a typo, and the
message spells that fix out rather than leaving it to be derived. §13.7 is why an engine may not simply read the
document as `0.2` instead: guessing a version produces a figure that looks
right and means something else. `Y` never removes (§13.0), so the genre set
at a version is always a superset of the set below it, and a document
declaring `figdown 0.1` resolves against exactly the six it always did.
`sequence` repeats the pattern one version up (`SEQUENCE-GENRE-VOCABULARY`):
`figdown 0.3 sequence` answers `genre "sequence" requires figdown 0.4 (this
document declares 0.3) — write: figdown 0.4 sequence`, from the same search
that produced `statechart`'s message and with no code added for it.
**Sections carry the version independently.** A multi-section file (`MULTI-FIGURE-DOCUMENTS`) is a
sequence of documents, each with its own header, so one file may legitimately
hold a `figdown 0.2 statechart` section beside a `figdown 0.1 table` section.
Declare, per section, the **lowest** version that carries what that section
needs: a higher declaration narrows the set of engines that will read it and
buys nothing.

**The genre is a namespace (`GENRE-NAMESPACE`).** The genre names three things: the
document's **keyword namespace**, its **defaults**, and its **validation
profile**.

- **`GENRE-NAMESPACE`.** A header genre determines the keyword namespace of
  **that SECTION's** top-level lines, **except for the layout zone, which is
  a namespace of its own (`LAYOUT-ZONE-NAMESPACE`)**. The carve-out is stated here because it
  would otherwise be missed: layout-zone lines ARE top-level lines, so
  without it `GENRE-NAMESPACE` would hand the genre a zone that `LAYOUT-ZONE-NAMESPACE` says no genre owns.
  **Per SECTION, not per file.** A multi-section file (`MULTI-FIGURE-DOCUMENTS`, §1) is a
  sequence of documents, and each `figdown <version> <genre>` header opens a
  new one — with its own genre, its own version (see §1 above), its own id
  space and its own layout zone. **The genres may differ**, and a file
  holding a `figdown 0.1 block` section and a `figdown 0.2 flowchart` section
  is ordinary, not exotic: `edge` is the connector in the first and
  `flowline` in the second, in the same file, because each section resolves
  against its own header. What stays one-to-one is the ARTIFACT: one `.fd`
  renders to one `.svg` (§7), whatever it holds. Earlier wording said "the
  document's top-level lines", which read as a per-file namespace and
  prohibited something the language has always supported.
- **`GENRE-VOCABULARY-OBLIGATION`.** A genre MAY define keywords of its own,
  and MAY reuse a spelling with a different meaning and different defaults
  from another genre's. In exchange it MUST document its complete
  vocabulary — every keyword, option key, enum value and default — in its
  own document under [genres/](genres/README.md), which is normative for
  that genre.
  **A spelling accepted by more than one genre is MORE THAN ONE
  DECLARATION, never one declaration inherited** (`SUBJECT-VOCABULARY-SCOPE`). No
  genre's vocabulary is defined by reference to another's, and there is
  **no section of this specification that declares a keyword for several
  genres at once**. Two genres' declarations agreeing today creates no
  obligation that they agree tomorrow: either genre may withdraw, rename
  or constrain its own without touching the other's, and neither one's
  declaration is authority for the other's. This is the exchange above
  read strictly rather than a new rule — a genre that took its words from
  a neighbour's document would be documenting an INCOMPLETE vocabulary,
  which is the one thing the exchange forbids. It is also the clause that
  tells a future editor not to re-create a shared section for
  convenience; §1's category text below states the same rule from the
  other end, and `topology`'s own document said the quiet part out loud
  before the correction — *"Distinguished from `block` not by vocabulary
  — it shares all of it"* — which is a genre document declaring that it
  does not contain its own vocabulary.
  **What this bites on is SUBJECT vocabulary** — the words that say what
  a figure is *of*, and therefore the words a genre's readers arrive
  carrying a domain meaning for. Styling declaration (`class`) and layout
  intent (`flow`, `rank`) name no referent, so no genre's domain holds a
  competing meaning for one; this clause does not settle whether they are
  genre vocabulary at all (see the category text below).
- **`GENRE-COMPOSITION`.** A section may COMPOSE genres. A nested genre
  region (in v0.1: a `bitfield`, `table` or `timing` block) is governed by
  THAT genre's namespace; its child keywords belong to it and are NOT valid
  at the enclosing section's top level. (Composition is the NESTED case;
  a second `figdown` header is the SECTION case, `GENRE-NAMESPACE` — they are different
  mechanisms and a file may use both.) Composition is not inheritance: the header
  genre does not acquire the nested genre's vocabulary. See §4.
- **`PER-GENRE-DEFAULTS`.** A genre's defaults need no justification
  beyond that genre's own census statistics (`DEFAULT-VALUE-SELECTION` read per bucket).
- **`UNIVERSAL-CORE-KEYWORDS`.** A small core is fixed: **`figdown`,
  `title`, `layout`** — three keywords (`LAYOUT-ZONE-NAMESPACE`; it was five
  until then, and the two that left, `pin` and `size`, did not leave the
  language: `size` merged into `pin` under `ELEMENT-GEOMETRY-DIRECTIVE` and `pin` moved to the
  layout namespace of `LAYOUT-ZONE-NAMESPACE` below. `path` and `routing` were members until 0.1, were demoted to EXPERIMENTAL then, and were WITHDRAWN from
  the language, `EDGE-GEOMETRY-CONSTRUCTS` — §10).
  **What core means.** *Wherever a core keyword appears, its meaning is
  fixed, and no genre may redefine it.* Core is a **fixity** guarantee.
  It is **NOT** a ubiquity requirement: core does **not** mean "must
  appear in every genre", and a genre is complete without any of them
  beyond `figdown`. `bitfield` and `table` documents have no `pin` and
  no `layout` zone at all, and that is not a deficiency —
  reading the earlier wording as ubiquity made `UNIVERSAL-CORE-KEYWORDS` contradict the
  per-genre minimum sets in [genres/](genres/README.md).
  These three are not the figure's vocabulary; they are the document's
  **structure**, and they are what a reader can resolve before the genre
  is known. `figdown` must be readable before the genre is known, or
  nothing can be dispatched; `title` names the document; and `layout` is
  the zone **opener** — a structural marker, not a directive inside the
  zone, which is why it stays here rather than moving to `LAYOUT-ZONE-NAMESPACE` with the
  zone's own members. The two-zone reading contract (§3) only
  holds across genres if `layout` itself means the same thing everywhere.
- **`LAYOUT-ZONE-NAMESPACE` — the layout zone is a namespace of its own**.
  The zone opened by `layout` (§3) constitutes its own namespace, and
  **every member of it is genre-independent**: no genre may define,
  redefine or extend a keyword inside it. `GENRE-VOCABULARY-OBLIGATION` does not reach into the
  zone. **The zone is per SECTION** (`GENRE-NAMESPACE`): each section opens at most one
  `layout` zone, and that zone's `pin` lines address that section's ids and
  no other's — a `pin` naming an id declared in a different section is
  `pin of unknown id "<id>"`, verified, not assumed. Genre-independence is
  about which WORDS may appear in a zone, and is unaffected: `pin` means the
  same thing in every zone of every section under every genre. **(`EDGE-GEOMETRY-CONSTRUCTS`) its membership is `pin` (NORMATIVE) and
  nothing else**; `path` and `routing` were members until then and are
  WITHDRAWN from the language (§10). **The clause does not change — only
  its membership does**, and it still governs every keyword any future
  release puts inside the zone.
  **Status and belonging are orthogonal, and conflating them is what this
  clause exists to stop.** EXPERIMENTAL is a statement about
  **stability** — the construct has not converged and may change or be
  withdrawn before v1 (§10). Genre-independence is a statement about
  **belonging** — no genre owns the spelling or may give it a second
  meaning. A construct can be both, and `path` and `routing` were: while
  they lived they were EXPERIMENTAL *and* genre-independent. Until 0.1 §10 read their demotion as also releasing them to `GENRE-VOCABULARY-OBLIGATION`, which
  is the confusion this clause removed; the distinction stands on its own
  and applies to the next experimental member the zone acquires.
  **Why the zone needs this and not merely a fixed `layout`.** `GENRE-NAMESPACE`'s
  default is that a reading agent ignores EVERY MEMBER of this namespace,
  wherever in the document it appears (§3, as restated by `GENRE-NAMESPACE` — the
  default is stated over membership, never over the zone's textual
  extent). That default holds only if no genre semantics can ever appear
  in the namespace, and it is applied by RECOGNISING the members, so the
  member set has to be one list that is correct under every genre.
  While `path`/`routing` were genre-redefinable, the
  premise had a crack; `LAYOUT-ZONE-NAMESPACE` closes it by fixing the zone's **membership**,
  not just its opener.
  A genre that needs its own edge geometry does not get it by taking a
  layout-zone spelling for itself: it must clear the `NEW-CONSTRUCT-EVIDENCE-GATE` gate for a NEW
  keyword in this namespace, and that keyword is then genre-independent
  too.
  **The price, stated as a price.** Genre-independence means the zone's
  spellings are **reserved language-wide**: after `EDGE-GEOMETRY-CONSTRUCTS` exactly one
  keyword, `pin`, is spent, and no genre may ever define it as its own
  keyword, for any meaning, however natural that meaning is in its
  domain. That is what buys `GENRE-NAMESPACE`'s clean premise, and it is a real cost
  rather than a free tightening — it consumes future flexibility, and a
  later reader who finds the reservation surprising should find the
  reasoning here rather than have to reconstruct it. The standing
  principle it follows from is SYNTAX-STYLE RULE 4.7: *absent necessity,
  do not spend the same spelling in more than one namespace.* The bill
  this clause ran up was larger until 0.1: it also spent `path`
  and `routing` — two ordinary, general-purpose words that many future
  genres could want — and that exposure was filed as **`EDGE-GEOMETRY-CONSTRUCTS`** (§9).
  `EDGE-GEOMETRY-CONSTRUCTS` **CLOSES `EDGE-GEOMETRY-CONSTRUCTS` by releasing both spellings**: they are no longer
  members of any namespace, so a future genre may claim either under
  `GENRE-VOCABULARY-OBLIGATION`/`NEW-CONSTRUCT-EVIDENCE-GATE`. `pin` is the mild case that remains.
- **`DECLARATION-ORDER-SEMANTICS` — declaration order is drawing order along the genre's primary
  axis**. Where a genre has a primary axis, the order in which
  its elements are DECLARED is the order in which they are DRAWN along
  that axis. Its three v0.1 instances:

  | Genre | Primary axis | Declaration order draws… |
  |---|---|---|
  | `table` | columns, then rows | `width auto,70,…` and each `\|` row run left to right; rows run top to bottom |
  | `timing` | signal rows | each `signal` is the next lane down, in document order |
  | `bitfield` | the bits of a word | each `field` is the next cell to the RIGHT, wrapping to the next word |

  A genre MUST NOT reverse the drawing on the strength of an option.
  `bitfield`'s `numbering=` is the case that made the rule explicit
  (`DECLARATION-ORDER-SEMANTICS`): it relabels the ruler and moves nothing, so `numbering=lsb0`
  still draws the first-declared field leftmost — it is simply the field
  with the HIGHEST bit number there
  ([genres/bitfield.md](genres/bitfield.md) semantic model). `DECLARATION-ORDER-SEMANTICS` is a
  statement about DRAWING, and it is not a licence to read arrangement as
  precedence: §12.7 still forbids reading array order as ranking, priority
  or sequence. What `DECLARATION-ORDER-SEMANTICS` guarantees is only that the human's reading order
  and the agent's declaration order are the same order, which is what lets
  one document answer both readers identically.

What a genre still MUST NOT do: silently reinterpret ANOTHER genre's
document. The header names exactly one genre, and a document is read under
that genre's namespace and no other (the surviving half of `FIGURE-TYPE-MECHANISM`/`GENRE-NAMESPACE`). Nor
may a genre declare a keyword and then quietly withhold part of its
syntax: **where a genre declares a construct, it declares the whole of
it.** `topology` declares `edge`, so directed, coloured edges work under
`topology` exactly as under `block` — not because the two share a
namespace (they do not; each declares `edge` for itself, `GENRE-VOCABULARY-OBLIGATION`), but because
each genre's own declaration of `edge` is the whole of `edge`, arrowheads
and `stroke=` included. **Field feedback F1**: a downstream transcription
lost direction semantics because an earlier draft of this paragraph
implied otherwise. The lesson survives the dissolved shared namespace
unchanged, and 0.3 sharpens it — a genre that means to admit less
must **withdraw the word** (`SCENE-KEYWORD-MEMBERSHIP`), which is a visible act with a named
per-genre diagnostic, not a silent narrowing of a word it still accepts.
The earlier wording carried the lesson on the premise that "the three
scene genres share one namespace today", and that premise is gone. A new
genre requires corpus evidence AND semantic impossibility (`NEW-CONSTRUCT-EVIDENCE-GATE`).

**What v0.1 actually delivers (updated 0.1; 0.1).** `GENRE-NAMESPACE` is
**enforced**
at the top level (`GENRE-KEYWORD-ALLOWLIST`): each header genre has an allowlist of legal
top-level keywords. `UNIVERSAL-CORE-KEYWORDS` core (`figdown` `title` `layout`) and the layout
namespace's normative member `pin` (`LAYOUT-ZONE-NAMESPACE`) are in every allowlist. The pure
genres `bitfield` / `table` / `timing` declare no subject vocabulary at
all and reject every scene genre's: `node`, `edge`, `state` and
`flowline` are each a line error under `figdown 0.1 bitfield`.

The four scene genres — `block`, `topology`, `flowchart`, `statechart` —
**each declare their own subject vocabulary**, and each may open nested
`bitfield`/`table`/`timing` regions (`GENRE-COMPOSITION`).

**`sequence` (`SEQUENCE-GENRE-VOCABULARY`) is neither pure nor scene**, and it is
the first genre that is neither. It declares its own subject vocabulary —
`lifeline` `message` `state` `fragment` `operand` — like a scene genre,
and like a pure genre it has no scene: its two axes are ORDERED BY
DECLARATION, columns by `lifeline` order and time by the order the
occurrence lines are written, so `flow` and `rank` are not its keywords
and there is nothing for a layout intent to set. It also opens no nested
region, because there is no scene to stack one beside; that is an OPEN
question rather than a refusal. And it is the one genre with **no
renderer**: a `sequence` document parses to a complete model, reports no
error, and draws an empty canvas.

**Where several scene genres accept the same keyword, that is an
INTERSECTION and not a shared surface** (`SUBJECT-VOCABULARY-SCOPE`). Each genre
declared the word independently, in its own document under
[genres/](genres/README.md), and each declaration is independently earned
and independently revisable: a genre may withdraw, rename or constrain a
word it declared without touching any other genre's, and one genre's
declaration is never authority for another's. The word is spelled the
same because each genre's domain uses it, not because the language hands
it down — and a common word may already carry a precise and DIFFERENT
meaning inside a genre's domain, which is why the domain meaning wins:
readers arrive carrying domain knowledge, not this specification. Only
**core** (`UNIVERSAL-CORE-KEYWORDS`) is cross-genre by definition, and core is `figdown`
`title` `layout` — a **fixity** guarantee about what a word MEANS
wherever it appears, never a statement that a word is AVAILABLE
everywhere (the same distinction `UNIVERSAL-CORE-KEYWORDS` draws for itself).

The visible consequence is already in the language, at `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: `block` and `topology` spell the connector `edge`,
`flowchart` spells it `flowline`, `statechart` spells it `transition`,
and `sequence` spells it `message` —
**five independent declarations, four spellings**, stated in five genre
documents rather than in one shared one. The same release makes `state`
the first spelling whose two declarations differ in GRAMMAR and not only
in reading: under `statechart` its first slot DECLARES an id, under
`sequence` it REFERENCES a lifeline. Two genres agreeing on a spelling was
already two declarations; this is what it looks like when they do not
agree on the grammar either, and the language accommodates it without a
rename because `GENRE-VOCABULARY-OBLIGATION` was never a claim that the word means one thing. The experimental constructs are
declared per genre on the same terms, and none of them
is shared: `threshold` and `band` are `block`'s alone, `bundle` is
`topology`'s alone, `flowchart` and `statechart` declare none, and
`plane` is **WITHDRAWN from the language** (`PAINT-ORDER-CONSTRUCT`, §10). Being on a
genre's allowlist has never promoted a construct to the taught main
standard — that is [experimental.md](experimental.md)'s line, not the
allowlist's.

**The scope of all this is SUBJECT vocabulary**: the words that say what
the figure is *of*. `class` declares styling meaning, and `flow` and
`rank` declare layout intent; none of the three describes a referent, so
no genre's domain holds a competing meaning for one and no genre can
independently earn or lose one. They stay legal in all four scene genres,
and where they properly belong — nearer `LAYOUT-ZONE-NAMESPACE`'s genre-independent namespace
than `GENRE-VOCABULARY-OBLIGATION` — is a question this text does not open. The `GENRE-COMPOSITION` region openers
`bitfield`/`table`/`timing`/`chart` are composition, not subject
vocabulary, and are out of it for the same reason.

*Until 0.3 this paragraph read that the scene genres "share the
scene vocabulary", and it was wrong three times over: it named three
genres when `STATECHART-GENRE-SCOPE`/`GENRE-NODE-SPELLING` had made four; it recorded an INTERSECTION as if it
were a namespace, which is the defect `SUBJECT-VOCABULARY-SCOPE` exists to correct; and it had
been false in its own terms since `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`, because the scene genres do
not share `node`/`edge`.*

The layout namespace
has no experimental members (`EDGE-GEOMETRY-CONSTRUCTS` withdrew `path` and
`routing`); while it had, their presence on the scene allowlists only was a
**status** fact and not a namespace one, because `LAYOUT-ZONE-NAMESPACE` fixes what a
layout-zone keyword MEANS wherever it is legal and says nothing about which
genres admit it — the same fixity-not-ubiquity distinction `UNIVERSAL-CORE-KEYWORDS` draws. **
`flowchart` exercises `GENRE-VOCABULARY-OBLIGATION`** — it owns `process`, `decision` and
`terminator`, legal under that genre and no other (`FLOWCHART-ROLE-KEYWORDS`,
[genres/experimental/flowchart.md](genres/experimental/flowchart.md) §Roles). It is `GENRE-VOCABULARY-OBLIGATION`'s
"a genre MAY define keywords of its own" half; no v0.1 genre yet uses the
other half (the same spelling with a different meaning). Defaults still
differ only for `flowchart`→`flow down` and `bitfield`'s required
`numbering=`.

**Multi-section files (`MULTI-FIGURE-DOCUMENTS`).** A single `.fd` MAY contain more
than one `figdown 0.1 <genre>` line. Each starts a **section** with its
own genre allowlist and id space. Rendering still produces **one SVG per
file**: sections are drawn independently and stacked top-to-bottom. There
are no cross-section edges and no multi-SVG output. Hybrid “panels” in
one artifact use either (1) multiple sections, or (2) a `block` host with
nested typed regions. Putting `node` under `figdown 0.1 bitfield` is a
line error — not a hybrid pattern.

**Version compatibility.** The header carries the wire-grammar version
and the genre. An unknown major version MUST be rejected. **There is no
minor-version leniency left to scope, and this paragraph no longer
pretends otherwise (`NORMATIVE-SENTENCE-ACTOR`).** v0.1 has one conformance mode, **strict**
(§10); the *lenient* mode this sentence once scoped a `SHOULD` to was
closed permanently by `LANGUAGE-EXTENSION-POLICY`, and the *viewer-tier* / *strict
authoring-tier* pair it named was invented ad hoc to settle
`conformance/DISCREPANCIES.md` `UNKNOWN-MINOR-VERSION` and is not a conformance class —
§0.2's list is closed and neither tier is on it. What holds instead is
§13.0.1's compatibility rule, whole, and it is the Parser's: a declared
minor version at or below the one the Parser implements is accepted and
rendered under its own semantics, one above it is rejected with a named
diagnostic, and nothing is ever guessed (§13.7). The reference engine's
accepted set is stated in §13.7. An unknown genre MUST be rejected in
strict mode.

Lexical rules:

- Directive = `keyword positional-args… key=value-options…`
- **Quoting is decided by the POSITION's value type, and the four answers
  are exhaustive** (SYNTAX-STYLE §2; enforced in full, `RULE-POSITION-ENUMERATION`):
  - a **string** delimited by whitespace — every positional label, the
    `class` meaning, the classic `field` name, `description=`, `present=` —
    takes **MANDATORY** quotes. Whitespace is also the positional separator,
    so a bare token cannot express a phrase;
  - an **id** or a **reference** — every declaration id, every endpoint,
    `in=`, `class=` and `rank`/`bundle` members — is **BARE**. A
    quoted token there is a line error. (`plane=` was the fourth key on
    this list until 0.3, `PAINT-ORDER-CONSTRUCT`.);
  - an **enum value** or a **bare keyword flag** — `shape=` `style=`
    `numbering=` `extend=` `type=`, the `figdown` version and genre, the
    `flow` direction, `cell … highlight` — is **BARE**, for the same reason:
    the position accepts a closed set of spellings, and quoting one suggests
    it accepts arbitrary text;
  - a **number**, a **point**, a **percentage** or a **range** — `gap=`,
    `word=`, `at=`, `pin width=`/`height=`, a `cell` address, a
    `field` width, a table `width` element, `index=` — tolerates redundant
    quotes: they are **INERT** and change nothing in the model. `index=`'s
    inertness is load-bearing (§12.7). Two word-shaped spellings sit inside
    otherwise-numeric grammars and are inert with them: table `width auto`
    and `field … *` (SYNTAX-STYLE §8.6).
- IDs are `[A-Za-z_][A-Za-z0-9_-]*`, unique **per section** — which in a
  single-section file (the overwhelming majority) is the same thing as per
  document. **Resolved**: this clause read "unique per
  document" while the multi-section paragraph above said each section has
  "its own id space", and no fixture decided between them. The **section**
  reading wins, on three grounds: sections are drawn independently with no
  cross-section edges, so a shared namespace would constrain documents that
  can never refer to each other; the alternative makes a legal one-section
  figure illegal merely by being pasted below another; and it is what the
  reference engine already did. `node a` in section 1 and `node a` in
  section 2 are **two different elements**, and neither is a `duplicate id`.
  Pinned by `020-multi-section-id-reuse`. Read the
  pattern as a whole-token anchor: the **first** character is a letter or
  `_`; **every** later character is a letter, a digit, `_` or `-`, with no
  further restriction. In particular **a trailing `-` or `_` is legal** —
  `node mux_ "MUX"` parses. (Stated explicitly because a downstream
  author's comment records renaming `mux_` in the belief that it was not.)
  There is exactly one additional restriction: **`--` may
  not appear inside an id**, because `--` is the link operator (`edge a --
  b`, `bundle t1 a--b`) and one spelling cannot be both (`LINK-OPERATOR-IN-IDS`). Write a
  single `-` or a `_` instead.
- Colors are CSS hex (`#rgb` or `#rrggbb`, e.g. `#0d9488`) or CSS named
  colors (the 147 CSS/SVG color keywords, lowercase, plus
  `transparent`); any other value is a line error.
- `#` begins a comment only at the start of a line or after whitespace
  (so `fill=#0d9488` is never mistaken for a comment).
- Escapes inside quoted strings: `\n` line break, `\"` literal quote,
  `\\` literal backslash. Any other escape is a line error. Quotes also
  work inside option values: `description="on miss"`. (Pipe rows additionally
  use `\|` and `\^^`, §4.2.)
- `title` takes exactly **one quoted positional argument**:
  `title "TCP Header"`. It
  is an ordinary quoted token — the escapes above apply, a `#` inside it
  is literal, and a second positional argument is `unexpected argument`
  like everywhere else. It also takes **one option key,
  `note=`** (§2.9, §10):
  `title "TCP Header" note="RFC 9293 Figure 3"` is legal at `figdown 0.3`
  and a line error before it. That
  is `title`'s first option of any kind, and it is not a change to what
  `title` MEANS, which is what `UNIVERSAL-CORE-KEYWORDS` fixes. `title TCP Header` is a line error naming <!-- fence-check: skip -->
  migration 0.1. Until that entry `title` consumed the rest of
  its line, which made it the only directive accepting an unquoted string
  with spaces and made three things impossible or ambiguous: a title
  containing `#`, a title containing `"`, and one meaning for `\n`
  (escapes resolved in the quoted form only).
- Keywords, option keys, enum values, IDs and references are
  **case-sensitive**; all standard keywords and option keys are
  lowercase ASCII.
- A directive line containing positional arguments its grammar does not
  accept MUST be rejected (typos never pass silently).
- **`;` is RESERVED for a future statement separator (`SEMICOLON-STATUS`) and
  MUST NOT be given any other meaning.** A `;` anywhere a processor is
  reading grammar is a line error naming the reservation. It stays ordinary
  text inside the four VERBATIM REGIONS — a quoted string, an `[edge label]`,
  a comment, and a GFM pipe row — and nowhere else. *(This clause read
  "`;` has no directive-separating meaning" in an earlier release: a description of what the mark did NOT do, in the release that
  had already made doing it an error. It was the one assigned mark whose rule
  is "MUST NOT be given any other meaning" and which this document never
  stated.)*
- **`#` starts a comment**, at line start or after whitespace, and it is
  honoured in the same four verbatim regions as `;` — a `#` inside a quoted
  string, an `[edge label]`, a comment or a GFM pipe row is ordinary text.
  *(The `[edge label]` half landed, `VERBATIM-REGION-SCOPE`; before it,
  `edge a -[hop #1]-> b` was `unterminated [label]`.)* A `#` that is neither
  at line start nor after whitespace is ordinary text too, which is what
  keeps `fill=#0d9488` a colour.
- The SHA-256 embedded in artifacts is computed over the exact UTF-8
  byte sequence of the source; processors MUST NOT normalize before
  hashing.
- One directive per line. No line continuations. No expressions, loops,
  or macros — ever. (framework axiom)

### 1.1 The layering: two axes, SCOPE × STAGE (normative, `GRAMMAR-LAYERING-MODEL`)

Everything above this subsection describes the language's structure one
piece at a time. This subsection says what the pieces are pieces OF, so
that a second implementation reporting a divergence can name **where** it
diverged instead of reporting *"the fixtures fail"*. It states no new
restriction: every clause below is a restatement of `GENRE-NAMESPACE` (`GENRE-NAMESPACE`–`UNIVERSAL-CORE-KEYWORDS`), `LAYOUT-ZONE-NAMESPACE`/`EDGE-GEOMETRY-CONSTRUCTS`
(`LAYOUT-ZONE-NAMESPACE`), `GENRE-DOCUMENT-CONTRACT`, `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`, §8.3 or `EXTENSIBILITY-MECHANISMS`, and the four places the language crosses
its own layering are DECLARED at the end rather than repaired.

**A rule needs TWO coordinates, and one of them alone answers nothing.**
*"`type=` takes `alt` here and `bar3d` there"* is a **Context**-stage
divergence at **genre** scope — an implementation that gets it wrong
produces a wrong message, not a wrong picture. *"The compact `field` list
contributed an element after reporting an error"* is a **Grammar**-stage
divergence at **genre** scope. *"The renderer drew the group band around a
non-member"* is a **Rendering**-stage divergence at **core** scope. Naming
only the stage leaves an implementer asking which document to read; naming
only the scope says "genre layer" about a thing that could be a lexer bug
or a reading rule.

#### SCOPE — whose rule is it

| Layer | What is in it | Governed by |
|---|---|---|
| **core** | the line grammar (tokenization, `key=value`, quoting, comments, `eol`, UTF-8, BOM); the header; the error model (§8); the model and its canonical binding (§12); versioning (§13); the legibility floor (§14); Safe SVG (§15). Keywords `figdown` `title` `layout` (`UNIVERSAL-CORE-KEYWORDS`) | `UNIVERSAL-CORE-KEYWORDS`, §8, §12–§15 |
| **core, layout SUB-layer** | the namespace `LAYOUT-ZONE-NAMESPACE` closes: the keyword `pin` and the option keys `at` `width` `height` on it | `LAYOUT-ZONE-NAMESPACE` (`LAYOUT-ZONE-NAMESPACE`/`EDGE-GEOMETRY-CONSTRUCTS`) |
| **genre** | subject vocabulary, node and connector spellings, per-genre defaults, per-genre option keys, region children, refusals, and each genre's reading rules | `GENRE-NAMESPACE`, `GENRE-VOCABULARY-OBLIGATION`, `GENRE-COMPOSITION`, `PER-GENRE-DEFAULTS`, `GENRE-DOCUMENT-CONTRACT` |
| **profile** | non-core and **outside the language proper**: the host integration profile, the publication manifest, the accessibility profile | `EXTENSIBILITY-MECHANISMS`'s fourth mechanism; each profile's own status block |

**The layout namespace is a SUB-layer of core and not a fourth layer.**
`LAYOUT-ZONE-NAMESPACE` makes `pin` genre-independent and forbids any genre from defining,
redefining or extending it; §10's own words are *"owned by no genre"*,
which is the same relation to genres that `UNIVERSAL-CORE-KEYWORDS` has. What differs is
**fixity against ubiquity**, the distinction `UNIVERSAL-CORE-KEYWORDS` already draws for itself —
so the sub-layer records which of the two rules binds a construct without
buying a top-level row that would put two of the crossing rules below in
duplicate.

**Ubiquity is not membership, and `class` is the case that proves it.**
`class` is accepted at the top level of **all eight** genres and it is
**not core**, because a future genre MAY define its own meaning and
defaults for it under `GENRE-VOCABULARY-OBLIGATION` (§10). A construct present everywhere is not
thereby core.

**The twenty option keys with no live acceptor anywhere are a `status`,
not a layer.** Each is a registered spelling kept alive only so a retired
or withdrawn word earns a named diagnostic instead of a generic one (`SEMICOLON-STATUS`,
§10's last namespace row). They are core by fixity and belong to no genre;
what distinguishes them from a live core key is their STATUS, and the
carrier below already has a status column, so no fourth layer is created
for them.

#### STAGE — when does the rule apply

L1's five classes are named groupings of §8.3.2's eight stages. The eight
remain normatively underneath: where a rule needs the finer name, §8.3.2
is the authority and this table is the reading aid over it.

| Class | §8.3.2 stage(s) | What a divergence there looks like |
|---|---|---|
| **Grammar** | source scan · section split · pre-dispatch refusal | a different error on a lexically bad line; a section boundary in a different place |
| **Context** | dispatch · option stage · directive stage | the line is well formed and this genre or this directive does not take it |
| **Resolution** | resolution | a reference that names nothing; an incomplete region |
| **Semantic** | *(no error stage)* — §12, §12.3, §12.4, §12.7 and the genre reading rules | the model differs, or two readers draw different conclusions from the same model |
| **Rendering** | geometry, plus §14 and §15 | the bytes differ, or the picture contradicts the model |

**One honest wrinkle, recorded rather than smoothed.** Grouping *section
split* — which **cannot fail**, as §8.3.2 says in terms — with *source
scan* and *pre-dispatch refusal*, which abandon or join, makes "Grammar"
mean two things at once. The five names are used anyway because they are
what a second implementer arriving from an IETF background expects, and
because nothing is lost: the eight stages stay normative and a report may
always name one of them instead.

#### The carrier

The classification's unit is the **construct**, and its carrier is
[`vocabulary-sources.tsv`](vocabulary-sources.tsv), which holds one row per
registered option key, positional value shape, keyword spelling and mark —
one row each, however many that is today: `node tools/vocab-check.js`
reports the row count and `gate:vocab` runs it every time, so the number is
never typed here to go stale — and carries a `layer` column and a `stage` column
beside its existing `kind` and `status`. The **normative fixtures** are the
independent CROSS-CHECK on what those cells claim, never the classification
itself: a fixture pins a rule but is not one, several fixtures pin one
rule, and one fixture pins several. A classification is a claim about the
engine, so it must be checkable against the engine; that is why it lives in
a machine-readable file and not in prose.

The carrier's own limit is stated in its header and is repeated here so it
is not discovered as a surprise: **no row is `semantic` or `rendering`
today**, because §12's model rules and §14/§15's picture rules are not
constructs and have no vocabulary row. Extending the classification to
reach them would need a second carrier, and that is the condition on which
this choice reopens.

#### What a genre MAY

1. **Declare subject vocabulary of its own** (`GENRE-VOCABULARY-OBLIGATION`), documented completely
   in its own genre document (`GENRE-DOCUMENT-CONTRACT`).
2. **Rename the node and the connector** for its domain (`GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`), the
   renamed directive taking the original's grammar, option keys and model
   shape exactly.
3. **Reuse another genre's spelling with a different meaning** (`GENRE-VOCABULARY-OBLIGATION`, §1),
   the header being the disambiguator, subject to `UNSAFE-DEFAULT-ELIMINATION`'s *"must not cost
   the reader"*.
4. **Set its own defaults**, with no justification beyond its own census
   (`PER-GENRE-DEFAULTS`).
5. **REFUSE a word** — `sequence` refuses `group`, `gap` and `lost=` — the
   refusal being part of the registry, with a diagnostic that gives the
   ground.
6. **Declare option keys of its own**, and accept core-registered keys on
   its own directives — including, under §10's third arm, a second value
   grammar for a registered key on the four conditions stated there.

#### What a genre MUST NOT

1. **Redefine a core keyword's meaning** (`UNIVERSAL-CORE-KEYWORDS`, fixity).
2. **Define, redefine or extend a member of the layout namespace** (`LAYOUT-ZONE-NAMESPACE`).
3. **Change the error model** — the abandon-vs-keep rule, the stage order,
   the per-stage error counts, cascade behaviour (§8.3). *(One DECLARED
   EXCEPTION today: crossing 4 below.)*
4. **Add a line shape the core parser cannot tokenize.** *(One DECLARED
   EXCEPTION today: crossing 2 below.)*
5. **Re-lex another genre's tokens** — §10 states this one already, of
   `LANE-ALPHABET-KEY-RESERVATION`: *"a genre may redefine a spelling's MEANING, but it cannot re-lex
   another genre's lanes."* *(The converse crossing is real: crossing 3.)*
6. **Widen what a reading agent may conclude** beyond §12.7. A genre's
   reading rules are additive within its own document (`GENRE-DOCUMENT-CONTRACT`), never a
   licence over the core contract.
7. **Change the shape of the canonical model.** A genre adds element kinds
   and fields; it does not change the wrapper, the ordering rules or the
   normalization (§12.4, §12.5).

#### What a profile MAY and MUST NOT — one sentence

**A profile MAY add a sidecar format, a host or publisher obligation, an
artifact-level convention, and a render option whose effect is confined to
its own output surface (at the cost of a `FIGDOWN_VERSION` bump, `VERSION-EVENT-TRIGGER` and
`VERSION-EVENT-TRIGGER` clause 1); and a profile MUST NOT add, remove or change a construct, a
model field, a byte of an artifact rendered without its option, or what a
reading agent may conclude about the FIGURE (§12.7) — MUST NOT be required
for the language to be complete, a renderer implementing no profile being a
conforming renderer — and MUST NOT enter a conformance claim about the
LANGUAGE (`INDEPENDENT-IMPLEMENTATION-CRITERION`).** The three profiles each already say the prohibition in
their own words; this is the layer's sentence, hoisted so that the fourth
profile does not have to reinvent it.

#### What the CORE MUST NOT do — the direction nobody writes down

1. **Enumerate a genre's vocabulary in a production a genre cannot amend**
   without that being a language change. *(Two DECLARED EXCEPTIONS today:
   crossings 1 and 2.)*
2. **Take a permanent constraint from an EXPERIMENTAL construct** without
   saying so where the constraint lives. *(One DECLARED EXCEPTION today:
   crossing 3, and it is the sharpest one in the language.)*

#### The four crossings — DECLARED EXCEPTIONS, not defects

`syntax-style.md` §0's honesty obligation applied to the layers: **every
crossing is DECLARED or it is a defect**, and *"a rule with an undocumented
exception is worse than no rule."* These four are the census as of `LAYER-CROSSING-RULES`.
A fifth found later is a defect until it is written down.

**Crossing 1 — the core grammar enumerates the genres and the versions.**
§11's `version` and `genre` productions are literal lists, so adding a
genre edits the core grammar and releasing a language version edits the
core grammar. **DECLARED, and it is the right design**: the header must be
readable before the genre is known (`UNIVERSAL-CORE-KEYWORDS`'s own reason), so the core parser
has to know the genre token set in order to dispatch at all. Not repaired,
because both repairs cost more than the crossing: a
`genre = <keyword-shaped token>` production moves a Grammar-stage error to
the Context stage and therefore moves goldens, and a per-genre ABNF
fragment splits one grammar into nine while still needing `genre` to
dispatch. **Reopens** if the language ever gains a genre-registration
mechanism that is not a literal list.

**Crossing 2 — one of the three alternatives of `line` belongs to a
genre.** `table-row` and `cell-content` are the `table` genre's and they
sit in the core's own top-level production; §8.3.2's pre-dispatch stage
knows about it too, listing *"a pipe row outside a `table` region"* among
its four members. Counted, **3 of §11's 29 productions are genre-owned
outright** and **2 more are widened by a genre** — `option-value` and
`bare-token` both permit `=` inside a token, for the `timing` genre's
lanes. **DECLARED**, with the honest note that a layering which called this
a violation would be a layering this language cannot adopt: the alternative
is a per-genre lexer, which breaks the *readable before the genre is known*
property `UNIVERSAL-CORE-KEYWORDS` exists to protect. **Reopens** if a second genre ever earns its
own line shape, at which point one exception is a pattern and a mechanism
is worth having.

**Crossing 3 — an EXPERIMENTAL genre permanently constrains the CORE
namespace.** The `timing` lane alphabet's reservation in §10 — that a
single-letter option key spelled `p`, `n` or `x` is never registered, in
any genre including a future one — runs UPWARD: an EXPERIMENTAL genre
(`CONSTRUCT-STATUS-TIERS`), outside the conformance surface and withdrawable in a later `0.x`,
reserves three spellings in the core option-key namespace permanently and
language-wide, and because the language cannot express the constraint it is
enforced outside the engine by `conformance/run.js`, which refuses to run
while such a key is registered. **DECLARED.** Two invariants it makes
explicit: **a constraint that flows upward carries its own reason to where
it lands** (a reader of §10's namespace rule must be able to find out that
an experimental construct put it there), and **a layer's status does not
bound the reach of the constraints it imposes** — status governs the
compatibility promise about a construct, not what the construct's existence
has already made impossible. Not repaired: `p`, `n` and `x` are the source
standard's own lane characters (`UNSAFE-DEFAULT-ELIMINATION`), and renaming them would spend a
borrowed spelling to buy back three single letters nobody has asked for.
**Reopens** the day a key spelled `p=`, `n=` or `x=` is actually wanted.

> **OPEN SUB-QUESTION, deliberately unruled (`LAYER-CROSSING-RULES`).** If `timing` were
> withdrawn — which its EXPERIMENTAL status expressly permits — does the
> reservation survive it? **The reservation's own text says yes** (*"in any
> genre, including a future one"*). **The `plane=` / `z-index=` precedent
> says no**: both were withdrawn with their keyword under the coupling
> §10(b′) states, *"neither half can outlive the other."* `LAYER-CROSSING-RULES` declines to
> decide between them, and says so rather than leaving the silence to be
> read as an answer: deciding today would be deciding for a future with no
> evidence in it, and the question is answerable the moment there is a
> withdrawal to answer it about. Until then, the reservation stands as
> written.

**Crossing 4 — the one exception to the core error model is a genre's.**
§8.3.1's rule is *"a line that reports an error performs none of its state
effects"*, and §8.3.3 names the single exception: the compact `field` list
keeps the items parsed before the bad one. `field` is a `bitfield` child
keyword, so the one exception to the CORE error model belongs to one GENRE.
**DECLARED**; §8.3.3 already carries the whole text, its reason (without it
a bitfield with one bad field raises a second, useless *bitfield has no
fields*) and its fixture, and this is the cross-reference the layering owed
it.

## 2. Core scene model (covers block-architecture, topology, flowchart)

> **Spec split (`GENRE-DOCUMENT-CONTRACT`).** The scene model — boxes, containment, and
> connections — is **normative in the genre documents**, not restated at
> length here. `block`, `topology` and `flowchart` share one scene
> namespace and differ only in defaults and status (`CONSTRUCT-STATUS-TIERS`): `block` is
> NORMATIVE; `topology` and `flowchart` are EXPERIMENTAL.
>
> **Authoring contract:** [core](core.md) (this document) **+** one genre
> doc suffice. For scene figures start with
> [genres/block.md](genres/block.md).

| Topic | Normative home |
|---|---|
| Complete scene vocabulary (`node` `group` `edge` `class` `external` `flow` `rank` …) | [genres/block.md](genres/block.md) |
| Topology-only / experimental `bundle` | [genres/experimental/topology.md](genres/experimental/topology.md) |
| Flowchart defaults (`flow down`) | [genres/experimental/flowchart.md](genres/experimental/flowchart.md) |
| Composition with typed regions (`GENRE-COMPOSITION`) | §4 below |
| Layout zone (`layout` opener; `pin` — its whole membership since 0.1) | §3 below |
| Drawn annotations (`note=`) and their placement contract | §2.9 below |

Subsection numbers **§2.1–§2.8 are retained** so existing citations keep
working; each is a pointer only. **§2.9 is the exception**: `note=` is
accepted across three namespaces and by the `UNIVERSAL-CORE-KEYWORDS` core `title`, so no genre
document can own it and its contract is stated here (`DRAWN-ANNOTATION-FORM`).

### 2.1 Nodes

Normative: [genres/block.md](genres/block.md) (vocabulary + semantic model).
Shapes are purely geometric (`SHAPE-ENUM-VOCABULARY`); the enum is closed and has **six**
values — `box` `rounded` `circle` `ellipse` `diamond` `cylinder` —
listed with its default in each scene genre's option table. `cloud` was
removed (`SHAPE-ENUM-VOCABULARY`): it named a domain, not a geometry, and a
retired value produces a named diagnostic (§10). Labels optional /
absent / empty `""` are three distinct states (`OMITTED-LABEL-RECORDING`, `EMPTY-LABEL-STATE`) — see also §12.

Under genre `flowchart` a node MAY also be declared with a **role
keyword** — `process`, `decision`, `terminator` — which records a `role`
in the model and DERIVES the shape from it (`FLOWCHART-ROLE-KEYWORDS`). The role is
the meaning; the shape remains geometry, and a `shape=` written on a role
line overrides the drawing and never the role (§12.7). A bare `node` under
`flowchart` states no role, and that absence is meaning. Normative:
[genres/experimental/flowchart.md](genres/experimental/flowchart.md) §Roles.

### 2.2 Containment (groups / nesting)

Normative: [genres/block.md](genres/block.md). A `group` line declares the
container; membership is written on the MEMBER — `node <id> … in=<group>`.
`group … in=` is a **line error** (`group does not take in= — nesting is
one level (node in=group) in v0.1`): one level is the whole of v0.1's
containment, so there is nothing for a group to be `in=`. `gap=` packs
spacing (`gap=0` flush).

**The band means membership, in both directions.** A `group` draws as a
band around its members, and the reader's rule — *inside the box is in the
group* — MUST hold: the band contains **every member and nothing else**.
Membership is declared and never inferred from geometry (above), and this
is the same statement read from the drawing: a picture that puts a
non-member inside the band states a membership the source does not, and a
reader who has to check the source to know what the box means is a reader
the figure has failed. Responsibility follows **whoever chose the
position**:

- **The engine chose it** — auto-layout had freedom. The engine MUST place
  the members contiguously so the band encloses exactly them. This is
  silent: no author action, no diagnostic, no syntax.
- **The author chose it** — a `pin` fixed the intruder, or fixed the
  members whose extent *is* the band, and no freedom is left. The engine
  then reports a geometry-time error naming the pin line and the enclosed
  node (§8), and renders nothing. It never overrides the author's
  coordinate, and never draws the false statement instead.

An implementation that draws the band without this guarantee is **not
conforming**, however many fixtures it passes: the defect is invisible in
the source and visible only in the output.

**The band is CROSSED, not terminated on** (`GROUP-BOUNDARY-OBSTACLE`). An edge
with one endpoint inside a group and one endpoint outside it connects the
two ELEMENTS it names; the band — the rectangle AND the label strip that
names it — is something that run passes **through**, never something it
arrives at. A renderer MUST NOT re-anchor such an edge to the band, and
MUST NOT treat the band or its name as an obstacle to it: the source says
the line reaches the member, so the drawing has to say the same, and a
line stopped at the container states a connection to the container. The
band and its name stay obstacles to an edge that **neither starts nor
ends inside** the group — there, going round them asserts nothing the
source denies.

### 2.3 Edges

Normative: [genres/block.md](genres/block.md). Operators `->` `<-` `--`
`<->` — **four**, and `--` is the most used of them; edge labels and
options. (Earlier revisions of this line listed three and omitted `--`.)

### 2.4 Paint order — the EXPERIMENTAL `plane` keyword is WITHDRAWN (`PAINT-ORDER-CONSTRUCT`)

**There is no `plane` directive, no `plane=` and no `z-index=`.** All
three were EXPERIMENTAL and all three left the language; the
engine rejects each with a diagnostic that names **no replacement**,
because there is none, and `plane` is swept ahead of the genre allowlist
so it fires under every genre. Registry rows, kept as withdrawn
diagnostics: §10 (b), (b′). The spellings were `layer`/`layer=` until 0.1 (`PLANE-KEYWORD-SPELLING`), so the retirement chain a reader may arrive on is
`layer` → `plane` → withdrawn, and the diagnostics state it whole rather
than sending an author to a word that is itself a line error.

**What stays, and it is now the whole of the construct: paint order is
DOCUMENT ORDER.** A later line paints on top (closer to the viewer). The
implicit `base` plane and the `planes` array are NORMATIVE and unchanged
(§12.2, §12.4) — every document has `planes[0] = {id:"base", z:0}` and
every `node` and `edge` reports `plane: "base"` — so a reader loses no
case and gains none.

**Why the withdrawal was free, measured.** Stripping the declared plane
and its `plane=` from `examples/evpn-fabric.fd`, the most realistic
figure in the tree that used one, and rebuilding gives an SVG of the same
**12449 bytes**: of 198 markup tokens exactly one differs, and it is an
edge index. The overlay's whole visual identity came from `class=overlay`,
which the figure already declared — a `class` states what the elements
MEAN, and a plane stated only what order to paint them in.
[experimental.md](experimental.md) §E0 carries the full measurement, and
§9 records the reserved alternative (`RESERVED-SPELLINGS`) with what would reopen it.

### 2.5 Semantic annotations: `bundle` (topology vocabulary) — EXPERIMENTAL

**EXPERIMENTAL (`CONSTRUCT-STATUS-TIERS`), with the `topology` genre.** Definition:
[experimental.md](experimental.md) §E2. Genre document:
[genres/experimental/topology.md](genres/experimental/topology.md), itself
EXPERIMENTAL. Registry row: §10 (b), (b′).

### 2.6 Thresholds and zone bands: `threshold`, `band` (generic markers) — EXPERIMENTAL

**EXPERIMENTAL scene markers (`CONSTRUCT-STATUS-TIERS`).** Definition:
[experimental.md](experimental.md) §E3, which also records why they stay
experimental (they are the `GENRE-EARNING-THRESHOLD` *interim general constructs* for the
candidate genre in §9). Listed on scene-genre allowlists; registry row:
§10 (b). Not part of the taught main-standard path.

`threshold` was spelled `guide` in an earlier release (`THRESHOLD-KEYWORD-SPELLING`, §10).
`band` gained a MANDATORY quoted label (`BAND-LABEL-STATUS`).

### 2.7 Semantic classes: `class` (+ derived legend)

Normative: [genres/block.md](genres/block.md) and §5 / §12. Meaning rides
on the class label (`MEANING-RECOVERY-SOURCE`), not on colour alone.

**A class must not declare paint that cannot reach the member it joins
(`INTERIOR-LESS-ELEMENT-PAINT`; reaching every collection, `CLASS-CHANNEL-REACH`).** A
class is a bundle of channel defaults for HETEROGENEOUS members, so the
rule is per CHANNEL, not per class: one class carries one meaning for a
node and an edge alike (`class hot "…" fill=#fee2e2 stroke=#dc2626`
paints the box of one and the line of the other), and no class has to be
split. The channels a member HAS are the ones its drawing reads:

| member | channels |
|---|---|
| `node`, `group`, `lifeline`, `state` | `fill` `stroke` `style` |
| `edge`, `message`, `fragment`, `operand` | `stroke` `style` — no interior |
| `field`, `cell` | `fill` `stroke` — `style=` left both at 0.1 (`STYLE-KEY-SCOPE`) |

Two forms are line errors, and both are declared paint that cannot
arrive. **`fill=` with no `stroke=` on a class an interior-less member
joins** (`INTERIOR-LESS-ELEMENT-PAINT`): on a line `fill=` and `stroke=` name the SAME
channel, so the author who wrote `fill=` meant the line's colour, and a
`style=` beside it does not answer that. **A class whose channels are
ALL channels the member lacks** (`CLASS-CHANNEL-REACH`) — `style=`-only on a
`field` or a `cell` — for the same reason one key over. `style=`-only on
an `edge` or a `message` is fine: the dash reaches the line.

> **RETIRED (`CLASS-CHANNEL-REACH`): a class that declares NO paint was a
> line error when an `edge` joined it (`CLASS-PAINT-REQUIREMENT`'s second half).**
> It is now legal on every member, and the retirement is recorded rather
> than performed silently. `CLASS-PAINT-REQUIREMENT`'s stated harm was that such a class
> "shows nothing in the legend" — and the same release made the derived
> legend draw the meaning with no swatch (below), so the meaning does
> reach the reader. What survived was only "the member takes its default
> paint", which is exactly what a meaning-only class on a `field` has
> always given, legally, in four shipped example figures. A rule that
> could not generalise past one collection was not a rule about channels.
> Reopens on a measured case of an author writing a paint-less class
> EXPECTING paint. [MIGRATIONS](migrations.md) 0.4;
> requirements-notes `CLASS-CHANNEL-REACH`.

**The meaning field is mandatory; its VALUE may be empty (
`CLASS-EMPTY-MEANING`).** This is the language's own absent/`""`/`"text"` tri-state (`EMPTY-LABEL-STATE`,
§12.3), the rule labels already follow, applied to the one field that had
been collapsing the first two:

| form | status | legend |
|---|---|---|
| `class x` | **line error** — the field is missing | — |
| `class x ""` | **legal** — the author explicitly claims NO meaning | **no entry** |
| `class x "Hard ceiling"` | legal | entry drawn |

`class x ""` is **pure attribute grouping**: one name applied to many
members so a shared look is not repeated, asserting nothing about what the
look means. It is the honest spelling for what authors otherwise pay the
required field with — a restatement of the id. It is NOT the way to spell
a role: a `flowchart` role belongs in that genre's vocabulary, not in a
meaning-less class (§9, `GENRE-EARNING-THRESHOLD`/`LOGIC-FLOWCHART-GENRE-SCOPE`).

The **derived legend** shows a class's DECLARED paint and nothing else,
and only for a class that claims a meaning. A class that declares no paint
draws its meaning with no swatch, rather than a neutral box
indistinguishable from `fill=white stroke=#555`; a class whose meaning is
`""` draws no entry at all, and consumes no vertical space. The two rules
are the same rule read on two axes — **the legend draws what the author
declared, and nothing the author did not.** Every channel a class can
still declare (`fill`, `stroke`, `style`) is drawn there, so "declared but
not shown" is unreachable — which it was not before 0.1, when a
class whose only channel was the label colour rendered an empty swatch in
the legend the language derives FOR its meaning. That rule settles
WHETHER a swatch is drawn and WHAT PAINT it carries, and it still holds
whole. It never settled what SHAPE the swatch takes, and the shape is not
the author's to declare.

**The swatch depicts the channel the class actually paints** (
`LEGEND-SWATCH-SHAPE`). Whether an entry appears is a fact about the DECLARATION; what the
swatch is shaped like is a fact about the DOCUMENT — what the class is
carried BY — and a renderer MUST read that off the model instead of
always drawing a box:

1. A class carried **only by connector-kind elements** — the four
   connector spellings `edge` `flowline` `transition` `message` (§10),
   which are exactly `doc.edges` and `doc.messages` — draws a LINE
   sample: the class's `stroke` (defaulting to `#555`), the stroke width
   this engine gives a drawn connector, and the class's `style=` as the
   dash pattern, so a dashed class dashes ALONG a line rather than around
   a rectangle's perimeter.
2. A class carried **only by box-kind elements** — every other collection
   that carries `class=`: `node` `group` `lifeline` `state` `fragment`
   `operand` `field` `cell`, and a typed block itself — keeps the rect.
3. A class carried **by both** draws BOTH samples, side by side, LINE then
   BOX, then the meaning text. The legend exists to explain, and the extra
   sample is cheaper than making a reader guess which half of the figure
   the entry is about.
4. A class that declares `fill=` always gets a box sample, because a fill
   needs an area to be visible.
5. A class that declares no paint still draws no swatch (`CLASS-PAINT-REQUIREMENT`, unchanged).

Which kinds are connectors is READ OFF THE MODEL, not stored: no model
field, no option key and no spelling was added for this. A renderer that
draws a rect for a connector-only class is **not conforming**, however
many model goldens it passes — the entry would show a filled area where
the figure shows a line, and `DECLARATION-ORDER-SEMANTICS` forbids the drawing disagreeing with the
source, of which the derived legend is a part.

> **Considered and rejected: a per-class `legend=show|hide` option (`CLASS-EMPTY-MEANING`).**
> Prior art is exact — Highcharts' `series.showInLegend` and ECharts'
> equivalent are per-item legend opt-outs — and the need behind it is real:
> ten classes begin to dominate a figure. It is refused because
> `legend=hide` lets an author **claim a meaning and then hide it**: the
> reading agent gets the category from the model while the human sees an
> unexplained colour, which is the one-source-two-readers inversion this
> spec exists to prevent. `""` claims no meaning at all, so nothing is
> hidden and the inversion cannot arise. It also adds no key and no value
> shape. The full note, including what would reopen it and the separate
> `legend=auto` observation it raised, is in
> requirements-notes `CLASS-EMPTY-MEANING`.

### 2.8 External endpoints: `external`

Normative: [genres/block.md](genres/block.md). Open-ended edges; never a
drawn shape. The keyword was spelled `boundary` until 0.1 (`EXTERNAL-ENDPOINT-NAMING`,
§10).

### 2.9 Drawn annotations: `note=` (`DRAWN-ANNOTATION-FORM`)

**`note=` is the DRAWN annotation channel, and it requires `figdown 0.3`.**
It carries authored prose the human reader must SEE — the aside a figure
needs and its labels have nowhere to put — and it is written as an option
on the annotated element's **own line**, with no id, no target key and no
locator. Attachment is by **syntactic position**, which is what makes it
unambiguous when three elements carry the same label.

**The ten acceptors** are `node` and its `flowchart` role siblings
`process` / `decision` / `terminator`, the `statechart` `state`, `group`,
and the three connector spellings `edge` / `flowline` / `transition`, plus
`title`. Everything else refuses it, and each refusal is a named line
error rather than `unknown option`: `field` refuses at every version and
its message states the AUDIENCE division rather than offering a
replacement (§10 — `description=` is not a replacement, it reaches a
different reader), and `cell`, `external`, `threshold`, `band`, `bundle`
and `class` get the generic *`<directive>` does not take `note=`*.
(`plane` was on this list until `PAINT-ORDER-CONSTRUCT` withdrew it; a withdrawn keyword's
line never reaches an option check, so the entry had nowhere to fire.)
The acceptor set is measured, not guessed: of 70 annotation instances
across 23 figures of a 643-file corpus, **75.7% are about exactly one
element** and **14.3% about the figure with no element** — which is what
`title` covers — while the connectors are in the set for a structural
reason rather than a numeric one, because **a connector has no id, so an
attribute is the only form that can reach it at all**. The residual is
§9 `ANNOTATION-LOCATOR-SPLIT`.

**The author does not place the box (`DOMAIN-CONVENTION-DIRECTIVES`).** `note=` accepts **no `at=`,
no `side=` and no `left of` / `right of`** — no positioning channel of any
kind, present or future, is part of this key. Placement is the renderer's,
exactly as rank assignment, obstacle detours and the derived legend are
(§12.6). What the author writes is *this element has this aside*; where
the aside sits is a drawing convention.

**Six placement rules are NORMATIVE, and they are here rather than in the
engine because two engines must agree on them.** A note whose box lands
somewhere else is not a cosmetic difference: it is a different figure, and
`RENDERING-DETERMINISM`'s byte-reproducibility promise (§7) reaches the SVG, not only the model.

1. **Notes register LAST**, after every node, group rect, edge label and
   arrowhead has taken its space. **A note yields, and nothing yields to a
   note.** The asymmetry has a reason: a label is *on* the thing it names
   and an arrowhead *is* part of the connector, whereas a note is *beside*
   what it is about, so it is the one element that can move without
   changing what the picture says.
2. **Candidates are generated around the carrier's FINAL geometry** — the
   node box, the group rect, the edge's segment list — and scored by the
   same overlap function every other deferred label is scored by. A note
   is never placed against a provisional position.
3. **A leader line is drawn only when adjacency fails**, and it is drawn
   after placement, so it is correct by construction rather than by
   correction. A note that could sit beside its carrier takes no leader.
4. **A `note=` written on `title` has no geometry to sit beside.** It is the
   figure-level note: it draws with the caption, at the foot of the
   canvas, and it **never takes a leader** — there is nothing for a leader
   to point at.
5. **A note placed by ADJACENCY names its target; a note with a LEADER does
   not**. Adjacency carries the whole association in a gap,
   and the reader has to work out which neighbour the box belongs to — so
   an adjacency-placed note is drawn with its target's **label in bold,
   then a colon, then the note's own text**: *"**MAC layer:** Runs on the
   recovered RX clock, not the core clock."* The prefix is DERIVED from
   the resolved target and is never authored — it is the same fact the
   drawing already publishes as `data-note-for`. A note with a drawn
   leader takes NO prefix: the leader *is* the association, and saying it
   twice is ink for a fact already stated. Only a labelled `node` or
   `group` is named this way; a connector has no name a reader can see,
   and a `title` note names the figure, of which there is one.
   The two decisions are circular — the prefix changes the box, the box
   changes whether adjacency succeeds — so the ORDER is normative: the
   note is placed **with** the prefix first, and if that placement takes a
   leader it is placed again **without** it and that second placement
   stands. There is no third pass.
6. **A note's text wraps to a fixed CORRIDOR, measured in the note's own
   type**. A line that fits the corridor stays on one line;
   author newlines are honoured and never merged; a single word wider than
   the corridor is not broken. The corridor is a width in the drawing, not
   a character count — a count is blind to what the glyphs are.

**Determinism**: one greedy pass, no iteration, registration order =
**document order**. Two engines given the same source therefore reach the
same placement in the same order, which is what makes rule 1 checkable
rather than aspirational.

**The drawn form is the UML note symbol** — a box with a folded corner —
under SYNTAX-STYLE RULE 4.1's borrow-the-standard's-own-form rule. The
author chooses no colour, no side and no offset, so `note=` adds no
presentation surface and `GUI-WRITEBACK-STRUCTURE`/`PRESENTATION-AS-MEANING-CARRIER` are untouched: deleting every
presentation key still leaves the note's TEXT in the document.

## 3. Layout control — the three tiers (`PRESENTATION-CONTROL-TIERS`, `LAYOUT-STABILITY`)

Everything in this section is **optional**; with none of it, the renderer
auto-lays-out deterministically.

**Spec-split note.** `flow` and `rank` are **content-zone** scene keywords
(reading axis / peer alignment). Their vocabulary rows and reading rules
are normative under [genres/block.md](genres/block.md) (and the scene
siblings). This section keeps the **layout-zone** contract — `layout` and
`pin`, which (`EDGE-GEOMETRY-CONSTRUCTS`) is the whole of it — plus the shared
`GUI-WRITEBACK-STRUCTURE` strip invariant. The zone is a namespace of its own and every member of it
is genre-independent (§1, `LAYOUT-ZONE-NAMESPACE`). The example below shows both zones for
teaching order; only the lines after `layout` are layout-zone syntax.

```figdown
flow right                      # content zone: overall direction (see block genre)
rank l2,l3                      # content zone: peers share a rank/row
layout                          # opens the layout zone
pin l3 at=(420,80) width=120 height=60
                                # tier 3: at= is the position in px, relative to
                                #         the element's positioning context;
                                #         width=/height= are an explicit extent
```

**What a bare `layout` line IS.** `layout` takes **no
arguments and no options** — the whole directive is the word. A
positional after it is `layout takes no arguments`; a `key=` after it is
`unknown option "<key>="`, because no option key is registered against
this directive. It is a
**zone opener**: it draws nothing, contributes nothing to the model
beyond its own presence, and its single effect is that **every line after
it belongs to the layout namespace** and every line before it belongs to
the content zone. It is single-valued per section (§8), so a second
`layout` line is `duplicate layout line`; and because the zone runs to the
end of the section, there is no closing keyword. Deleting the `layout`
line and everything after it is exactly the `GUI-WRITEBACK-STRUCTURE` strip test, which is why
the zone has an opener at all: one word makes the presentation tail
mechanically separable from the knowledge above it.

This is stated in prose because a blind-reading test found `layout` the
**weakest keyword in the language on self-evidence** — a reader who had
only the syntax reached the right meaning but at low confidence, and by
POSITION alone (it appears once, immediately before the only run of `pin`
lines), remarking that "a bare keyword is strange in a format this
attribute-happy". The keyword is bare deliberately: it names a boundary,
and a boundary has nothing to configure.

`pin` is the zone's only directive. It admitted the EXPERIMENTAL `routing` and
`path` directives until 0.1, when `EDGE-GEOMETRY-CONSTRUCTS` withdrew both from the
language; §9 **`EDGE-IDENTITY-AND-GEOMETRY`** carries the requirement they served, and §10 the
diagnostics their spellings still fire.

Normative rules:

- **Rigidity** (`LAYOUT-STABILITY`): a `pin`/explicit attribute is a hard
  constraint; auto-layout arranges *around* pinned elements and never
  overrides them.
  **`pin` carries an element's whole declared geometry, and its two halves
  have DIFFERENT domains** (`ELEMENT-GEOMETRY-DIRECTIVE`, 0.1 — the extent keys were a
  separate `size` directive until then):

      pin <id> [at=(<x>,<y>)] [width=<px>] [height=<px>]

  All three keys are **optional** and **at least one is required**: a
  `pin` line carrying none of them is a line error, because it declares
  nothing and is therefore a typo rather than a weaker constraint.
  `pin a width=100` with no `at=` is legal and means exactly what a
  `size`-only line used to mean.
  - **`at=` applies to nodes, groups and `external` endpoints.**
  - **`width=`/`height=` apply to NODES ONLY.** A group sizes to its
    members; an `external` endpoint and a typed block (`bitfield`,
    `table`, `timing`) derive their geometry from their content. Each of
    the three carrying `width=`/`height=` is a line error naming its own
    subject, because an extent there is a claim the renderer cannot
    honour and silently ignoring it was the defect 0.1 closed.
  The split is on the KEYS, not on the directive: `pin` itself still
  applies to groups and externals, and pinning a group is the one-line
  edit that `PIN-COORDINATE-SCOPE`'s two-level coordinates exist for.
- **Determinism & stability (`RENDERING-DETERMINISM`, tiered conformance)**: a conforming
  parser MUST produce the same semantic model for the same source; a
  conforming renderer MUST be deterministic (same source + same
  renderer version → byte-identical SVG); different renderers SHOULD be
  visually equivalent (byte-identical output across implementations is
  NOT required — a Canonical SVG Rendering Profile may make it opt-in
  later). A local edit must change only the corresponding local region.
  The parser tier is testable against the golden fixtures in
  [conformance/](../conformance/README.md); known engine-vs-spec
  deviations are recorded loudly in its DISCREPANCIES file, never
  frozen silently.
- **Size adaptation** (`UNDECLARED-ATTRIBUTE-BEHAVIOUR`): an explicit extent → content shrinks to fit
  (font may step down). No explicit extent → box grows minimally.
  **CORRECTED (`BOUNDED-GROWTH-ACCOMMODATION`, 2026-08-21): this said "without displacing the global
  layout" — the layout-locality instrument measured that promise was never
  delivered (6 of 12 corpus pairs, `decisions/registry.md`
  §5(a)). `BOUNDED-GROWTH-ACCOMMODATION`'s first amendment attempt, SAME-RANK LOCALITY, was itself
  measured undelivered the day it landed (a label grows a box's WIDTH,
  which is the main-axis/rank-progression extent whenever flow is
  horizontal, so growth there is structurally cross-rank) and is
  SUPERSEDED. The promise is now BOUNDED ACCOMMODATION: an unnamed node
  may move, but never farther than the named node's own box grew — a
  moved node displaced past the growth delta is a violation. Measured
  12 of 12 clean at ruling time. Instrument-guarded by
  `tools/layout-stability/run.js`'s `label-longer` class
  (`boundedGrowth`). Reopen condition: any measured displacement exceeding
  the growth delta — see `BOUNDED-GROWTH-ACCOMMODATION` for the full ruling, including the
  superseded same-rank attempt and why it failed.**
  Extents are **px only** in v0.1 —
  percentage sizes are reserved for a future version. The value grammar
  is exactly `\d+(\.\d+)?` and the number MUST be **positive** (no unit
  suffix, no scientific notation, no zero, no negative — `at=` keeps
  negatives; a width is a box extent). A second `pin <id>` is a line
  error (single-valued per id, `REPEATED-DIRECTIVE-HANDLING`), and that one
  rule covers the position and the extent together: last-wins on a merged
  line would silently delete whichever keys the second line omitted.
- **Two-level pins (`PIN-COORDINATE-SCOPE`)**: a pinned **group** anchors its local origin
  in canvas px; a pinned **member** is group-local (relative to that
  origin). Moving a group is therefore a one-line edit and edits inside
  one group can never disturb another. Ungrouped pins are canvas px.
- **Semantic-completeness invariant (`GUI-WRITEBACK-STRUCTURE`)**: stripping every `pin` line
  and the `layout` opener from a document MUST leave one
  that still parses, still renders under auto layout, and expresses
  the identical structure and relationships. Editors conventionally
  place layout after a `layout` line so the structure reads first.
  **The invariant is frozen and unchanged; its MEMBERSHIP shrank.** It named `pin`, `routing` and `path` until `EDGE-GEOMETRY-CONSTRUCTS` withdrew
  the latter two from the language, and it names `pin` alone now. A strip
  set is a list of the constructs the zone contains, so removing a
  construct removes it from the list without touching the rule.
- **`PIN-COORDINATE-UNITS` resolved: `at=` is px relative to the element's positioning
  context** — the canvas for ungrouped nodes and groups; the group's
  local coordinate system for group members (`PIN-COORDINATE-SCOPE`). Canvas-relative
  fractions were tried and rejected (canvas growth moved every
  fractional pin). Edges are always derived from node borders — they
  adapt, and can never be pinned.
- **The reference corner of `at=` is the TOP-LEFT (
  normative).** `pin <id> at=(x,y)` places the **top-left corner of the
  element's layout box** — the axis-aligned bounding box of the drawn
  shape — at `(x,y)`. The anchor **does not vary by shape kind**: a
  `circle`, an `ellipse` and a `diamond` are pinned by the top-left of
  their bounding box exactly like a `box`, and the renderer derives the
  `cx`/`cy` it needs from it. This holds for every pinnable element —
  nodes, groups and `external` endpoints. The axes run
  right and down (SVG's), so a larger `y` is lower on the canvas.
  *Stated because it had never been written down: §3 said only "px
  relative to the element's positioning context", which does not choose
  a corner.*
  **`pin` is NOT Visio's `PinX`/`PinY`.** Visio shares the word and means
  the opposite anchor: its pin is the shape's **centre of rotation**, so
  a Visio-literate author who assumes centre semantics lands every
  circle, ellipse and diamond off by (w/2, h/2) — a legal, silently wrong
  figure. FigDown borrows only the word.
- **The two-zone reading contract (`CONTENT-LAYOUT-ZONE-SPLIT`, normative)**: a document has a
  content zone (top) and a layout zone — everything from a `layout`
  line to the end. The `layout` line is a bare keyword (no arguments);
  after it, ONLY layout declarations (`pin`, which is
  the zone's whole membership)
  are legal — any semantic directive after `layout` is a line error.
  The layout zone MUST NOT carry semantics; the content zone SHOULD
  NOT hold information needed only for rendering.
  **The layout NAMESPACE is DEFAULT-IGNORED (`GENRE-NAMESPACE`, strengthening `CONTENT-LAYOUT-ZONE-SPLIT`;
  restated over the namespace by `GENRE-NAMESPACE`).** The zone exists
  ONLY to stabilise the rendered `.svg`. Any information that
  is content, logic or concept MUST be expressible in the content zone.
  A reading agent's DEFAULT behaviour is therefore to **ignore every
  member of the layout namespace, wherever in the document it appears**
  — not merely that it may. **MEMBERSHIP decides, never position.** The
  namespace is ENUMERATED in §10 (a′) and has exactly
  one member, `pin`; a `pin` written before the `layout` opener is
  ignored on precisely the same terms as one written after it. No opt-in
  or opt-out keyword is introduced; the default is the contract.
  **Why the promise is stated over the namespace and not over the zone's
  textual extent (`GENRE-NAMESPACE`).** `pin` MAY legally appear before `layout`
  (below), and in practice about half of it does — so a promise phrased
  as "ignore everything from the `layout` line down" is literally true
  and practically empty: an agent that kept it would still meet half the
  document's layout information, because the zone was meant to be the
  container for presentation and only holds half of it. Two alternatives
  were rejected. **Requiring `pin` inside the zone** is a tightening that
  invalidates previously legal documents, and `pin` is NORMATIVE rather
  than EXPERIMENTAL, so under §13's X/Y/Z policy only a major version may
  do it — a migration plus a version gate, buying nothing but a sentence
  being literally true, since the position constraint carries no meaning.
  **Naming `pin` in the promise** instead of the namespace abandons the
  container model and creates recurring debt: every future member would
  owe an amendment to this sentence, and a missed amendment is another
  promise that does not do what it says. The namespace form costs
  nothing, because §1 `LAYOUT-ZONE-NAMESPACE` already makes the layout zone a namespace of its
  own — `GENRE-NAMESPACE` aligns two statements the spec already makes rather than
  adding a rule.
  **The enumeration is therefore NORMATIVE, and it is checked.** A
  namespace-based promise is only actionable if a reader can ENUMERATE
  the namespace, so §10 (a′) is the enumeration a reading agent applies,
  not a summary of one; `tools/namespace-check.js` asserts that it agrees
  with the reference engine's accepted set and fails
  when either moves.
  This also puts an obligation on genre design: if a genre needs
  "arrangement carries meaning", it owes a content-zone construct
  (`MEANINGFUL-ARRANGEMENT`) — `pin` is not a substitute, and neither is anything else in
  the layout namespace. `UNIVERSAL-CORE-KEYWORDS` and `LAYOUT-ZONE-NAMESPACE` (§1) are together what makes the
  default hold across genres. `LAYOUT-ZONE-NAMESPACE` fixes the MEMBERSHIP, and that is what
  the default now rests on: every member of the namespace is
  genre-independent, so ONE enumeration is correct under every genre and
  an agent can apply it without first knowing which genre it is reading,
  and there is nothing a genre could have put there for the ignoring
  agent to miss. `UNIVERSAL-CORE-KEYWORDS` fixes the OPENER: `layout` is core and means the same
  thing under every genre, which is what gives authors a conventional
  place to collect the members and gives the `GUI-WRITEBACK-STRUCTURE` strip set a boundary —
  it is no longer what carries the skip. `LAYOUT-ZONE-NAMESPACE` was added
  (`LAYOUT-ZONE-NAMESPACE`) because `UNIVERSAL-CORE-KEYWORDS` is not sufficient on its own —
  while `path`/`routing` were genre-redefinable, a future genre could have
  given one of them a meaning of its own inside the zone, and an agent
  that never looks at the contents has no way to notice. Both were
  withdrawn from the language (`EDGE-GEOMETRY-CONSTRUCTS`), so the namespace now
  holds `pin` alone; `LAYOUT-ZONE-NAMESPACE` is what keeps the premise true for whatever it
  holds next.
  The one narrow exception is the gap the language has not yet closed:
  until a declared-arrangement construct exists (`MEANINGFUL-ARRANGEMENT`), a reading agent
  SHOULD read the namespace's lines, and say that it did, when a
  document's layout looks load-bearing — dense pins arranged as a stack, a
  grid, or a map (`PRESENTATION-AS-MEANING-CARRIER`). That is a workaround for a missing construct, not a second
  contract; a document that needs it has a semantics gap. Meaningful
  colour and shape are never in this position: they are declared as a
  `class` (§2.7) and already live in the content zone.
  Layout directives (`pin`) MAY appear
  before `layout` for documents that do not use the zone separator; a
  document with no `layout` line is valid. The zone opener was spelled
  `render` until 0.1; it was renamed because the zone admits only
  geometry, not presentation — and
  because `render` collided with the renderer and the render options of
  §7. `layout` is also the cross-tool word for this half of a diagram
  language, and this zone carried the `# --- layout` comment convention
  before it became a keyword.
- *Informative (editor policy, not wire format)*: editors MAY
  materialize computed positions into `pin` lines (the reference editor
  does so on the user's first drag — "pin-on-first-touch") and SHOULD
  place generated layout after a `layout` line. The old `# --- layout`
  comment convention is now just a comment with no special parser role;
  `layout` is the machine-enforced zone opener.

## 4. Typed blocks (census-dominant types)

Three figure families are *not* box-and-wire graphs and get dedicated,
closed sub-grammars. Each is introduced by its keyword and terminated by
the next top-level directive (sticky scope — no `end` keyword). Priority order below is final, per the completed census.

**Per-genre documents are normative and self-contained (`GENRE-DOCUMENT-CONTRACT`).** Each genre
has a dedicated document in `genres/` covering defaults, a **complete
vocabulary table**, the normative semantic model, error cases, and an
example. Core doc + genre doc alone suffice to author and read that genre.
The summaries below are informative; the genre docs are normative.

**A typed block is a nested genre region (`GENRE-COMPOSITION`, §1; `LOGIC-FLOWCHART-GENRE-SCOPE` §4).** Composing
genres is how FigDown expresses a hybrid figure — a block architecture
above a timing figure, a topology beside its VLAN table — and it is never a
reason to found a new genre (`LOGIC-FLOWCHART-GENRE-SCOPE` §4: hybrid figures are composition, not a
new genre). Stated at the vocabulary level:

- A `bitfield`, `table` or `timing` line opens a region governed by THAT
  genre's namespace. Its child keywords (`field`/`break`, the `|` row token
  with `cell`/`width`, `signal`/`gap`) are valid **only inside the region**;
  at the document's top level each is the line error
  `"<kw>" is a typed-block child — it needs a bitfield/table/timing block above it`.
- The reverse also holds: the host document's own keywords are not valid
  inside the region. A keyword from another genre's child set inside a
  region is `"<kw>" not valid inside <genre>`.
- Composition is **not inheritance**. A `topology` document containing a
  `table` block does not acquire the `table` genre's vocabulary; it reads
  each region under the genre that governs it.
- **In v0.1 the regions a document composes stand in document order**, each
  complete in itself, with no declared relation between them. Whether a
  region can be declared SUBORDINATE to an element of the host document —
  "this table is about node `X`", "this bitfield details the packet carried
  on edge `E`" — is `CROSS-BLOCK-REFERENCES` (cross-block semantic references), v0.2. Nothing
  here asserts that a composed region must be a peer; document order is
  what v0.1 provides, not a definition of what composition is. One
  constraint is already fixed for whoever designs `CROSS-BLOCK-REFERENCES`: a subordination
  relationship is SEMANTIC, so it MUST live in the content zone — it cannot
  be carried by `pin` or by anything else in the layout zone, which reading
  agents ignore by default (§3, `GENRE-NAMESPACE`).
- Only `bitfield`, `table` and `timing` have a region form in v0.1, so only
  they can be composed. `block`, `topology` and `flowchart` have no region
  syntax; giving them one is `GROUP-LEVEL-FLOW`/`CROSS-BLOCK-REFERENCES` territory, v0.2.

### 4.1–4.3 Genre documents are normative

Per `GENRE-DOCUMENT-CONTRACT`, each typed genre owns its **complete** vocabulary, defaults,
semantic model, errors, and examples in `genres/`. The core document
states only composition (`GENRE-COMPOSITION`, above) and the pointers below. Do not
treat the one-line summaries as a second source of truth.

### 4.1 `bitfield` — packet headers / register layouts

**NORMATIVE genre.** Normative document: [genres/bitfield.md](genres/bitfield.md).

### 4.2 `table` — config/state tables, memory maps

**NORMATIVE genre.** Normative document: [genres/table.md](genres/table.md).

### 4.3 `timing` — timing / waveform — EXPERIMENTAL GENRE

**EXPERIMENTAL (`CONSTRUCT-STATUS-TIERS`).** Spelled `wave` until 0.1 (`TIMING-GENRE-NAMING`). Normative-for-the-genre document: [genres/experimental/timing.md](genres/experimental/timing.md). Outside the v0.1 conformance
surface; see §10.

### 4.4 `chart` — charts from table data (EXPERIMENTAL)

`chart <table-id> …` maps an existing `table` to a chart. Not a
genre; experimental, outside the v0.1 conformance surface (§10).
Principle: chart features SHOULD reuse table blocks as data (`NEW-CONSTRUCT-EVIDENCE-GATE`).
The opener was spelled `plot` and its type option `kind=` until 0.1 (`CHART-BLOCK-NAMING`, §10). Its second option, `level=` (a reference plane
drawn through the bars), was **deleted** (`CHART-LEVEL-KEY`): zero uses
corpus-wide, zero 3-D bar charts, zero requests, and it was the only
construct in the language whose caption the engine wrote rather than the
author. `type=` is the one option key left.

### 4.5 Priority note

`block` (census #1) needs **no** typed-block opener — it is the
scene model ([genres/block.md](genres/block.md); historical §2).
Flowchart and topology share that model with different defaults/
status. Census: scene family + bitfield + table + timing cover most
classifiable diagrams (census.md).

## 5. Presentation attributes (`PRESENTATION-CONTROL-TIERS`)

Optional on any element: `fill=` (the interior), `stroke=` (the outline of
a shape and the whole of a line), `style=solid|dashed|dotted`;
`gap=` on groups. **`plane=` was a fourth until 0.3**, when `PAINT-ORDER-CONSTRUCT`
withdrew it with the `plane` keyword that declared its only legal values;
this section's tables lost every row that named it. **There are exactly
two paint channels, and they are
SVG's own two.** A label's colour is not a third one: v0.1 has no
label-colour key, and the default is derived (below).

**`stroke=` reads differently on a shape and on a line, and that is the
rule, not an exception.** On anything with an interior — a `node`, a
`group`, a typed block or one of its items — `stroke=` is the OUTLINE and
`fill=` is what it encloses. On an `edge`, a `bundle` ring or a
`threshold` there is no interior: the construct IS a line, so `stroke=`
is the whole of it and `fill=` is a line error naming `stroke=` (the
carve-out table below and the §10 registry row; `INTERIOR-LESS-ELEMENT-PAINT` — the citation read
"§8.4" until 0.1 and §8 has no subsections). This is SVG's asymmetry borrowed unaltered — `<rect stroke>` is a
border, `<line stroke>` is the line — and every surveyed system reads it
the same way: mxGraph's `strokeColor` applies to vertices and edges
alike, D2's `style.stroke` is documented as applying "to shapes and
connections", and Mermaid colours a link with `linkStyle … stroke:`. A
reader needs one sentence for both cases: *`stroke=` is the ink the
element's outline is drawn with, and a line is all outline.*

> **`stroke=` is NORMATIVE (`STROKE-KEY-STATUS`).** It was demoted to
> EXPERIMENTAL (`CONSTRUCT-STATUS-TIERS`) on a count of **5** uses across a
> 50-document corpus, which is no longer the fact: `examples/` and
> `figures/` alone write it **56+** times, and the downstream production
> corpus has **567 edge-colouring sites** — 549 `edge … class=` lines
> whose class carries a colour, plus 18 direct `edge … color=` — every
> one of which is a `stroke=` site. Nothing needed rewriting for the
> promotion. Documents that were not portable v0.1 documents while it was
> demoted — including this repository's own `examples/reference/block.fd`,
> `README.md`, `guide/showcase.md` and `guide/layout.md` — became portable with
> no edit. `plane=` stayed EXPERIMENTAL at that release, because the
> `plane` keyword that declared its only legal values was demoted
> (§10 (b′)); the same coupling took it out of the language
> altogether with that keyword (`PAINT-ORDER-CONSTRUCT`, §2.4). The sentence is kept because
> it is the record of the status rule working, and the rule is unchanged:
> a key and its only declaration point move together, whichever way they
> move. (`plane=` was spelled `layer=` until 0.1, `PLANE-KEYWORD-SPELLING`.)

> **`color=` is RETIRED language-wide (`COLOUR-KEY-STATUS`), and nothing
> replaces it.** The same six characters meant two opposite things in two
> eras: in one era `color=` set the **interior**, and
> in another it set the **label**. No engine can tell the
> two source files apart, so while the key stayed live a pre-0.1
> document parsed and drew a legal, WRONG figure in silence — `TEXT-COLOUR-KEY-NAMING`
> conceded in writing that the required diagnostic was "not
> implementable". Retiring the key is the only mechanism that makes the
> difference DIAGNOSABLE: every one of those lines is now a line error
> whose message names both eras and hands the choice to a human.
> No surveyed system has ever re-pointed a live colour key — DOT grew
> `fillcolor`/`pencolor` around an unchanged `color`, and PlantUML renamed
> and kept the old name working — and this reverses a third re-pointing.
> `text=` (an earlier release's spelling for the same channel) stays
> retired too. **Do not expect a replacement in v0.1**: the colour would
> have to attach to a LABEL, and an edge carries three of them
> (`[tail]`/`[mid]`/`[head]`), so any key on the *edge* colours all three
> identically. That makes every owner-level key that could be added today
> the wrong shape; the question is filed as a LOCATOR problem under §9
> `ANNOTATION-LOCATOR-SPLIT`, with Graphviz's `labelfontcolor` as the worked precedent for a
> system that hit the same wall.

**The default label colour is DERIVED, and it is not an option (`LABEL-COLOUR-SOURCE`).**
A label takes its colour from the background it is drawn on, so it cannot
assert a falsehood (`UNSAFE-DEFAULT-ELIMINATION`: a default that can be wrong is not allowed).
The arithmetic is normative and is WCAG 2.1's, unaltered:

1. **Relative luminance** of the background colour, per WCAG 2.1's
   *relative luminance* definition. For each 8-bit sRGB channel `C`, with
   `c = C / 255`:

   ```
   clin = c / 12.92                      when c <= 0.03928
   clin = ((c + 0.055) / 1.055) ** 2.4   otherwise
   L    = 0.2126*Rlin + 0.7152*Glin + 0.0722*Blin
   ```

2. **The threshold is not a taste value.** WCAG 2.1's *contrast ratio* is
   `(L1 + 0.05) / (L2 + 0.05)`. White (`L = 1`) and black (`L = 0`)
   contrast EQUALLY against a background of luminance `L` when
   `1.05 / (L + 0.05) = (L + 0.05) / 0.05`, i.e. at

   ```
   L0 = sqrt(0.0525) - 0.05 = 0.179128784747792…
   ```

   **The closed form is what is normative**; the decimal is an aid, and a
   renderer MUST NOT round it before the comparison. A renderer MUST use
   the **dark** ink when `L > L0` and the **light** ink otherwise. Any other threshold knowingly picks the less readable of
   the two.

3. **Which background.** A label drawn inside a filled shape takes that
   shape's RESOLVED fill — the element's own `fill=`, else its class's,
   else the block's per-item default, else the renderer's default fill.
   A label owned by a line-only construct (an `edge`'s three labels, a
   `threshold`'s label, a `bundle` ring's label) has no fill to sit on and
   takes **its owner's line colour**: labels follow their owner. A label
   drawn on the canvas — a typed block's title, an `external`'s label —
   takes the canvas ink. `transparent` is not a fill: it is the canvas.

The two ink values themselves are the renderer's, exactly as fonts and
spacing are; what this section fixes is the *choice between them*. The
reference engine uses `#ffffff` as the light ink and its per-context
default text colour as the dark one.

> **The edge-label split this closed.** Until 0.1 an edge's `[mid]`
> label followed the line colour while its `[tail]`/`[head]` labels did
> not — one construct, two undocumented defaults, so a `stroke=#0f766e`
> edge drew a teal mid label and two grey endpoint labels. All three now
> follow the line.

Dimensions belong exclusively to the `pin` directive's `width=`/`height=`
keys (a separate `size` directive until 0.1, `ELEMENT-GEOMETRY-DIRECTIVE`) — `width=`/`height=`
on a node line is an error (one mechanism, not two). The abbreviations
`w=`/`h=` were retired and are a line error wherever they
appear (`UNSAFE-DEFAULT-ELIMINATION`: SVG, CSS, DOT, mxGraph and D2 all spell these in full, and
`fill=`/`stroke=` were borrowed whole from SVG). Edge routing is not in
the language at all (`EDGE-GEOMETRY-CONSTRUCTS` withdrew `routing` and `path`;
§9 `EDGE-IDENTITY-AND-GEOMETRY`) — and it was never expressible on an `edge` line even when it
was. Everything else (fonts, spacing,
arrowheads) belongs to the renderer/theme, not the language.

**Where they apply, and the deliberate carve-outs.** "Any element" means
every construct that can actually *draw* the thing the attribute names.
The four attributes are accepted on `node`, `group`, `edge`, `class`,
`bundle`, `threshold`, `band`, `external`, the typed blocks
(`bitfield`/`table`/`timing`) and their items (`field`, `cell`, `signal`)
— with exactly the exceptions below. Each exception is a line error
(`<directive> does not take <key>=`), never a silent no-op, because the
grammar is closed (§10):

| element | NOT supported | why |
|---|---|---|
| `edge`, `bundle`, `threshold` | `fill=` | none of the three has an **interior**: the construct IS a line, so `fill=` and `stroke=` would name one channel and `fill=` was the one that lost silently. Retired at 0.1 (`INTERIOR-LESS-ELEMENT-PAINT`, and the paragraph above); the message names `stroke=`. `stroke=` and `style=` stay live on all three — a line has ink and a dash. |
| `external` | `fill=`, `stroke=`, `style=` | an external endpoint is **never drawn** (`EXTERNAL-EDGE-ENDPOINTS`, §2.8): there is no fill, no border and no dash for the attribute to act on. Since 0.1 (`COLOUR-KEY-STATUS`) it takes `plane=` and nothing else: its label was the one thing it drew and `color=` was the one key it kept, so retiring that key leaves it with no paint at all. The label is still drawn, in the canvas ink (`LABEL-COLOUR-SOURCE`). |
| `bitfield`, `table`, `timing` | `style=`, `plane=` | typed blocks stack in document order **outside** the scene, so there is nothing to layer them against; and a block-wide `style=` would collide with the per-item dash convention (a field carrying `present=` is drawn dashed). |
| `field`, `cell`, `signal` | `plane=`, **`style=` (since 0.1, `STYLE-KEY-SCOPE`)** | a block item lives inside its block, which is not part of the plane stack. `style=` was accepted per item until 0.1 and was removed for a reason stronger than tidiness: on a `field` the dash is conditional presence's ONLY visual carrier, so `field "B" 8 present="" style=solid` erased it while the model still recorded the field as conditionally present — a reading agent and a human got different figures from one line, which is exactly what `MEANINGFUL-ARRANGEMENT` forbids. (The construct was the bare flag `optional` when `STYLE-KEY-SCOPE` was written; `PRESENCE-CONDITION-EXPRESSION` replaced it with `present=` at 0.1.) The three were removed together as one minimum set (11 in-repo uses, **0 downstream**); a per-item dash now comes only from the item's own semantics (`present=`) or from a `class=` it joins. | <!-- fence-check: skip -->

> **`band` left this table (`BAND-LABEL-STATUS`).** It was listed here as
> refusing `color=`, with the reason *"a zone band carries no label"* — a
> DEFECT stated as a design feature. `band` had no label slot at all, so
> its complete model was `{target, from, to, extend, fill, line}`; strip
> `fill=`, which this section and `PRESENTATION-AS-MEANING-CARRIER` entitle a reader to discard, and a
> band asserted **nothing whatsoever** — its meaning rode on colour alone,
> which the paragraphs below declare must never happen. The label is now
> MANDATORY and quoted, written first (`band "Headroom" 15..35% in=pool`),
> and the text channel came with it — a channel 0.1 then removed
> from the whole language (`COLOUR-KEY-STATUS`), so a `band`'s label now takes the derived
> colour like every other label. Every interval region in the measured corpus is a
> *named* one; the buffer-region figure that motivated this was forced to
> carry its names in three `class` declarations all spelled "region".

**`plane=`'s honesty note is discharged by `PAINT-ORDER-CONSTRUCT`, and the finding that
forced it is why the withdrawal was easy to argue.** Until 0.3
this section had to record that the key was accepted on `node`, `group`
and `external` and put in the model, while the only pass that acted on a
plane's `z` as paint order was the annotation pass — edges, bundle rings,
threshold lines and zone bands; nodes and groups were painted by the
scene pass in document order whatever their plane said. A key that three
of its acceptors recorded and did not obey is a key that was already
mostly organizational, and `class` is the construct for saying what
elements have in common. The key is gone (§2.4) and the note goes with
it; paint order is document order everywhere, with no acceptor exempt.

Value rules are uniform wherever an attribute is accepted: colors are
`#rgb`/`#rrggbb` or a CSS named color (plus `transparent`), and `style`
is one of `solid|dashed|dotted`. The third clause here required
`plane=` to name a declared plane, and went with the key at `PAINT-ORDER-CONSTRUCT`.

Normative boundary (the presentation-ignorable invariant, extending
`GUI-WRITEBACK-STRUCTURE`): removing all presentation-only attributes (`fill`, `stroke`,
`style`, `gap`) and layout directives (`pin`)
and the `layout` zone-opener line MUST NOT change the document's semantic structure;
semantic consumers MAY ignore them. Consequently **color and style MUST NOT be the sole
carrier of meaning** — if color/dash denotes state, role, plane or
classification, that meaning SHOULD also appear in text or a semantic
annotation — the `class` mechanism (§2.7) is that carrier: when
color/dash classifies, declare a `class` and join elements to it;
bare `fill=` remains for decoration. Semantic-color profiles can be layered on later; the
document scenario keeps colors free. (resolves the `PRESENTATION-CONTROL-TIERS` tension)

General principle (`PRESENTATION-AS-MEANING-CARRIER`): **any attribute this spec calls presentation MAY
render meaning but MUST NOT be its only carrier** — if something is
knowledge, some text in the content zone must say it. Two consequences.
When the colours are the figure's SUBJECT MATTER (a spectrum, colour bands,
a wire colour code), the colour NAME MUST appear as text (a cell/node label
or a `class` label) and the SEQUENCE MUST live in an ordered construct
(table cell order, bitfield field order), so that a consumer that cannot
render can still answer which band is third and what that colour means; when
the exact colour VALUE is itself the datum, write the value in the text too
— the duplication with `fill=` is deliberate, the text being normative.
The same law applied to position is what `MEANINGFUL-ARRANGEMENT` (§9) has to close.

## 6. Dynamic — reserved, not specified (`STATIC-DYNAMIC-PRIORITY`, `DYNAMIC-FIGURE-PURPOSE`)

`page`, `set` and `pulse` are reserved keywords — the three the sketch
below actually uses, and the whole reserved set (§10). `step` was reserved
until 0.1 and has been RELEASED: it appeared in no sketch, no genre
claimed it, and a word reserved against nothing costs authors a name for
nothing. Sketch (non-normative):

<!-- fence-check: skip -->
```figdown
page "After ARP resolution"
set r1.fib row="10.1.0.0/16 R4 p2"    # sticky delta on the static scene
pulse r1                                # transient highlight
```

Dynamic = the static scene + an ordered list of page deltas (sticky vs.
transient). Deferred until the static core ships.

## 7. Embedding & artifacts (`MARKDOWN-EMBEDDING-CONVENTION`, `RENDERING-DETERMINISM`)

- Fenced block in Markdown: ` ```figdown … ``` `; sidecar file: `X.fd`.
- Generated artifact: `X.svg`, embedded in .md by plain image reference.
- The renderer MUST embed in the SVG: the full source text
  (`<metadata id="figdown-source">`), a SHA-256 of the source
  (`data-sha256="…"` — the hash is of the SOURCE, never of the artifact),
  and **the full version string of the engine that rendered it**
  (`data-engine-version="…"`) — making the artifact self-carrying and
  staleness detectable.
- **Why the engine version is required.** `RENDERING-DETERMINISM` promises byte-identical
  output for *the same source and the same renderer version*, so the
  renderer version is half of the input to that promise: without it an
  artifact cannot be reproduced or verified, only re-rendered and hoped
  about. Under §13 a 0.x renderer MAY differ from the next, which makes
  the recorded version the only thing that can explain a diff between
  two renderings of one source — the alternative is to suspect the
  source, which the SHA-256 has already ruled out.
- **What the `data-*` provenance channel carries on a connector
  (`CONNECTOR-IDENTITY-KEY`).** The renderer marks each drawn connector with
  `data-edge`, and its value is **the authored `id=` where the connector has
  one, and the connector's 1-based source line where it does not**. The two
  key spaces cannot collide: an id begins with a letter or `_` (§1), so no id
  is a decimal number and no line number is an id, and a reader can tell which
  it holds by looking at it. The attribute NAME is unchanged, so the Safe SVG
  enumeration (§15.2) is unchanged with it. **The fallback is stated rather
  than implied, because the two halves make different promises**: an authored
  id is stable under editing, while a source line changes when any line above
  it is inserted or deleted — which is why a channel that carried only the
  line could not honestly claim to name an element across revisions.
- Same-basename pairing (`X.fd` ⇔ `X.svg`) is normative.
- **Render options (renderer tier, not language).** A renderer MAY
  accept presentation options — v0.1 defined one and there are now
  **two**: `with-title` (draw the title inside the SVG) and
  `with-a11y` (emit the accessibility profile's role, non-visual
  `<title>` and state-flagged `<desc>` — see
  [`figdown-a11y.md`](figdown-a11y.md), adopted `ACCESSIBLE-TEXT-EMISSION` as the emission
  route for `ACCESSIBILITY-PROFILE`/`ACCESSIBLE-DESCRIPTION-SOURCES`). For `with-title` the DEFAULT is not drawn:
  embedded figures almost always sit under the host document's
  caption (`DEFAULT-VALUE-SELECTION` — defaults follow the majority; mainstream tools also
  do not draw titles by default). The title TEXT stays semantic in
  the source either way. **The two options are ORTHOGONAL**: one
  decides ink, the other decides the accessible name, and a figure
  rendered with both does not get two names. `with-a11y` also
  defaults to off, which is what keeps every artifact rendered before
  it byte-identical. Any non-default option MUST be recorded in the
  artifact metadata (`data-render-options="…"`), so an artifact
  remains a pure function of (source, recorded options) and
  third-party rebuilds stay bit-identical; when more than one option
  is in force the value is the option names **in the order listed
  here, separated by single spaces** (`with-title with-a11y`), so
  that one render has exactly one spelling. Options never appear in
  the source grammar — the registry is untouched, and option
  vocabulary is kept minimal (a knob must justify its existence like
  syntax must).

## 8. Error model

- Unknown keyword / malformed line → `Line N: <message>`, parse continues
  (error-recovery mode) so all errors report in one pass.
- Unknown `shape`, duplicate ID (node/group/`external` share one
  namespace), dangling edge endpoint (an id that names no node and no
  `external`), `in=` cycle (**a RESERVED category, not a v0.1 error** —
  `in=` is node-only and `in=` on a `group` is itself a line error, so
  containment is one level and no cycle is constructible; the entry holds
  the category for a future nesting syntax, and 0.1 removed it from
  `genres/block.md`, which stated it as a live error), a **compact**
  bitfield item wider than the row
  (`field A:16,B:64` under `word=32`), table row/col
  mismatch → all line errors. The **classic** form does NOT overflow:
  `field "B" 64` under `word=32` is the documented spanning case (§4.1),
  parses clean and stays ONE field of width 64 across two rows. The
  asymmetry is the compact form's own convention — it is C's bit-field
  spelling, where an item must fit its storage unit, and its options are
  LINE-wide, so no single item in a compact list can say "this one spans".
- **Repeating a single-valued directive is a line error on the second
  occurrence** (`REPEATED-DIRECTIVE-HANDLING`). `title`, `flow` and `layout` are single-valued per
  **section**; `pin` is single-valued **per node id within its section**
  (`pin a` + `pin b` is fine, `pin a` twice is not). "Per section" and
  "per document" coincide in a single-section file; the distinction was
  made explicit with the id-scope resolution in §1, and it
  is what the reference engine already did — two sections may each carry
  their own `title`, their own `flow`, their own `layout` zone, and a
  `pin` for the same id. Last-one-wins would be a silent failure:
  the document says two things and the reader is never told which one
  was dropped. Messages: `duplicate title line`, `duplicate flow directive`,
  `duplicate layout line`, `duplicate pin for "<id>"`.
- A document with errors renders nothing (no partial/best-effort output —
  determinism over convenience).
- **Geometry-time errors.** Not every error is visible in the source. A
  parser cannot see a coordinate, so a document that is impeccable line by
  line can still describe a drawing that states something it does not — the
  `group` band that encloses a non-member (§2.2) is the recorded case. Such
  an error is raised by the RENDERER, carries the same `Line N: <message>`
  form as every other, and names the line the author can act on (the `pin`
  that fixed the position; failing that, the `group` line). It costs
  exactly what a parse error costs: **nothing is rendered and no artifact
  is written** — a caller that writes `.svg` files MUST treat a non-empty
  render diagnostic list exactly as it treats a non-empty parse error list,
  because the alternative is publishing the picture the engine has just
  said is wrong. The class is deliberately narrow: a geometry-time error
  reports a FALSE STATEMENT in the drawing, never a matter of taste. Ugly
  is not an error; wrong is.
- **A repeated option key on ONE line is a line error**, never last-wins:
  `node a fill=red fill=blue` → `duplicate option "fill=" on one line`.
  It is a property of the **option lexer**, not of any keyword, so it
  holds uniformly for **every directive that takes an option**, frozen or
  experimental, without being restated on each — verified on `node`,
  `edge`, `class`, `pin`, `bitfield` and `table`. It was **stated only in
  [syntax-style.md](syntax-style.md) §7 (I5) until 0.1**, a
  document whose own opening says it is normative for the DESIGN of the
  language and is not an authoring guide, and whose evidence cites engine
  line numbers an implementer is not supposed to read. Same key on
  DIFFERENT lines is a different question and is answered per construct
  (§8.1).
- **Forward references are legal.** Reference resolution is a **post-pass
  over the whole section**, not a left-to-right walk: a name may be used
  before the line that declares it. `edge a -> b` before `node a`,
  `in=g` before `group g`, and `class=c` before `class c "…"` all parse
  clean and produce the same model as the same document written in
  declaration-first order. A single-pass resolver that rejects a forward
  reference is **not conforming**, however many fixtures it passes. Stated
  and pinned by `021-forward-references`; before that no
  normative fixture exercised one, so a single-pass implementation could
  pass the whole suite while rejecting legal documents.

### 8.1 Duplication: what repeats, and what that costs

"Is writing this twice an error?" is asked of every construct, and the
answer is not uniform. Three behaviours exist and each construct has
exactly one:

| behaviour | meaning | constructs |
|---|---|---|
| **line error on the second** | single-valued (`REPEATED-DIRECTIVE-HANDLING`) | `title`, `flow`, `layout` (per section), `pin` (per id per section), `width` (per table), the `\|---\|` delimiter row (per table), a duplicate id in the node/group/`external`/typed-block namespace (per section) |
| **accepted, both kept** | the construct is a set member, and two identical members are two members | `edge`, `rank` |
| **accepted, merged** | two writings would combine into one object | *(none — no v0.1 construct merges)* |

The two accepted cases are the ones an implementer is most likely to guess
wrong, so they are stated rather than left to inference:

- **`edge` repeated identically.** `edge a -> b` written twice yields
  **two** *Edge* objects in `edges`, in document order, each with its own
  `line`. There is **no de-duplication**: the model is a multiset of
  authored statements, not a graph the engine simplifies. An implementation
  that collapses them emits a shorter `edges` array and fails the byte
  comparison.
- **`rank` lines with overlapping ids.** `rank a,b` followed by `rank b,c`
  is accepted; so is the same line written twice. Each `rank` line is one
  *Rank* object in `ranks`, and the arrays are **not** merged into
  connected components, not de-duplicated, and not checked for overlap. A
  shared id in two ranks is an ordinary authored statement.

The two per-table errors above are listed in the [table genre's Errors
table](genres/table.md#errors); they were golden-pinned but absent from it
until 0.1.

### 8.2 The error-message catalogue: where it is, and what is missing

**The `.errors.txt` goldens in `conformance/cases/` are the normative
catalogue of error messages, and this section is NOT exhaustive.** That is
the honest statement of the position, made; before it, the
project implied a catalogue existed somewhere in `spec/`, and none did.

- **What is specified.** Every message a normative fixture produces. **How
  many that is, is not written down here.** A number that nothing
  recomputes drifts every time a fixture lands, so this section states the
  command that derives it instead — the rule
  [conformance/ERROR-COVERAGE.md](../conformance/ERROR-COVERAGE.md) and
  `conformance/STATUS.txt` already apply to their own tallies. Run from the
  repository root:

  ```sh
  node -e '
  const fs=require("fs"), d="conformance/cases";
  let files=0, lines=0; const msgs=new Set();
  for (const f of fs.readdirSync(d).filter(x=>x.endsWith(".errors.txt"))) {
    files++;
    for (const l of fs.readFileSync(d+"/"+f,"utf8").split("\n")) {
      if (!l.trim()) continue;
      lines++; msgs.add(l.trim().replace(/^Line \d+:\s*/,""));
    }
  }
  console.log(lines+" error lines across "+files+" fixtures, "+msgs.size+
              " distinct message texts");'
  ```

  A second implementation reads the messages out of the goldens, which are
  ordinary UTF-8 text files in this repository and are **not** the reference
  engine. Reading a golden is exactly what the conformance recipe asks
  for; it is not a back door into the PoC.
- **The format is fixed even where the text is not**: one error per line,
  `Line N: <message>`, `N` 1-based and file-wide, all errors from one
  pass, output sorted for comparison (`conformance/run.js` sorts before
  it compares, so an implementation's own emission order is free).
- **What is NOT specified, stated plainly.** The reference engine has
  ~195 `err()` call sites; the suite pins the messages of a subset. **For
  an error case that no normative fixture covers, a second implementation
  has no way to learn the required message text.** It can know the input
  must be rejected, and it can match the `Line N:` frame, but the words
  are unavailable to it. This is a real gap, it is not closed by this
  section, and it is the reason
  [conformance/README.md](../conformance/README.md) no longer claims the
  whole suite is reachable from the normative documents.
- **What would close it.** Either an exhaustive catalogue in this
  document, versioned and gated so it cannot drift from the engine, or a
  fixture per `err()` site so the goldens become complete by construction.
  The second is the cheaper of the two and is the direction
  [conformance/ERROR-COVERAGE.md](../conformance/ERROR-COVERAGE.md)
  already measures; neither is a v0.1 deliverable.

### 8.3 Error recovery and precedence (normative)

~~**8.3 Error recovery and precedence: UNSPECIFIED in v0.1**~~ — the heading
this section carried until **`ERROR-RECOVERY-MODEL` (2026-08-22)**, struck
through rather than deleted because every citation of it, in this repository
and outside it, was written against a section that said *v0.1 does not specify
recovery*, and a reader arriving from one of those citations has to be able to
see what replaced it.

**Nothing in the reference engine changed, and no golden moved.** What changed
is the direction the knowledge flows. The recovery behaviour was already
FROZEN — the `.errors.txt` goldens compare **sorted, complete error lists**,
so every multi-error fixture in `conformance/` has been pinning it since the
day it landed, and there are more of those than there are single-error ones
(§8.2 gives the command that counts them, and states why no count is written
here) — but the RULE that predicts those lists was not written down,
so a second implementation could only match them by matching the PoC. The
rule below is that missing half. It is derived from the engine and the
goldens, it is stated as it IS rather than as it ought to be (the three
asymmetries §8.3.6 names are specified, not repaired), and what it still does
not cover is named in §8.3.8 rather than left to be discovered.

#### 8.3.1 The rule

**A line that reports an error performs none of its state effects. No other
line's parse changes because of it.**

That is the whole model. Everything below is the enumeration a second
implementation needs to apply it: which effects are *state* effects (§8.3.2),
the one place a line contributes anything at all while failing (§8.3.3), what
follows for later lines (§8.3.4, §8.3.5), which single error a multi-fault
line reports (§8.3.6), and how conformance compares the result (§8.3.7).

#### 8.3.2 The stages, and how many errors each may report

A document is processed in stages. A fault found in one stage **abandons the
line** and the later stages never see it, so an earlier stage's message
SUPPRESSES every message a later stage would have produced for that line.

| stage | scope | on a fault |
|---|---|---|
| **source scan** | the whole document, before it is split | reports **every** illegal character (§15.3, `XML-CHARACTER-LEGALITY`); suppresses nothing — its findings JOIN the pass |
| **section split** | the whole document | purely lexical — a section starts at any line whose first token is `figdown`, valid or not. **It cannot fail, and no error can move a boundary** |
| **pre-dispatch refusal** | one line | **one** error, line abandoned, **an open region stays open** (§8.3.5). Four members: a lexical fault (an unterminated string, a bad escape), the reserved `;` (`SEMICOLON-STATUS`), a pipe row outside a `table` region, and a RETIRED or WITHDRAWN top-level spelling |
| **dispatch** | one line | **one** error, line abandoned. Closes any open region *first* (§8.3.5). The reserved dynamic words (§6), the genre allowlist (`GENRE-KEYWORD-ALLOWLIST`), the connector-word rules (`GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`) and typed-block-child placement are decided here |
| **option stage** | one line | reports **every** offending key rather than stopping at the first, then abandons the line. Repeated key on one line, unknown key, a key this directive or this genre does not take, a retired or withdrawn key, and the value grammar of the language-wide keys (`fill=`/`stroke=`, `style=`, `class=`, the id-valued keys such as `in=`, and the quoted-string keys `description=`/`present=`/`note=`) |
| **directive stage** | one line | reports the **FIRST** fault and nothing else, then abandons the line. The keyword's own checks: the layout-zone gate (`CONTENT-LAYOUT-ZONE-SPLIT`), id spelling, uniqueness, arity, positional form and the directive's own option values (`shape=`, `at=`, `word=`, `numbering=`, `gap=`) |
| **resolution** | one section, after every line has been read | reports **every** unresolved reference and **every** incomplete region. Independent per element: one failure never stops a sibling |
| **geometry** | the render channel | §8 above. Reached only by a document whose parse reported nothing, so a geometry diagnostic and a parse error never appear in one list |

Two consequences an implementer should read off the table directly. **A
directive whose keyword is refused never runs its option pass** — `zork
fill=bad` reports the keyword and not the colour. And **the header line is the
one line that reports several of its own faults**: a bad version, a bad genre
and a surplus positional on one `figdown` line each get their own message,
because the header has no single fault to abandon on.

#### 8.3.3 State effects, and the one exception

The state effects a line can perform, all of which a failing line skips:

1. **declaring an id** into the node/group/`external`/typed-block namespace
   (or into the `class`, `plane` or `bundle` namespace);
2. **contributing an element** to the model — a node, an edge, a field, a row,
   a `pin`, a `rank`;
3. **opening a region** — `bitfield`, `table`, `timing` (§8.3.5);
4. **consuming a single-valued slot** — `title`, `flow`, `layout`, a per-id
   `pin`, a per-table `width` and delimiter row (§8.1). A failed `title` line
   does not spend the slot, so the next `title` is the first one, not a
   duplicate;
5. **establishing the header** — the declared version and genre. A header line
   that fails leaves both UNESTABLISHED, which is visible in later
   diagnostics: after `figdown 0.1 blok`, an unknown keyword answers  <!-- fence-check: skip -->
   `unrecognized line` and not `"…" is not allowed in genre block`, because
   there is no genre to be outside of.

**The one exception, and it is the only one: the compact `field` list keeps
the items parsed before the bad one.** `field a:8,b:99,c:8` under `word=32`
reports the overflow on `b:99` and still contributes `a`. This is deliberate —
without it a bitfield whose only `field` line has a fault in the middle would
raise a second, useless *bitfield has no fields* — and it is the sole place in
the language where a line reports an error and writes to the model anyway.

#### 8.3.4 Cascades are the specified behaviour, not a defect

Because a failed line declares nothing, later lines that name it fail too, and
a second implementation that "recovers" by keeping the failed declaration
reports FEWER errors and fails fixtures whose subject is not recovery at all.
The four cascade shapes, each with the fixture that pins it:

- **Reference cascade.** `node a "A" shape=hexagon` then `edge a -> b`  <!-- fence-check: skip -->
  yields **two** errors: the shape, then `unknown endpoint "a"`. The same
  shape holds for `in=` over a failed `group`, and for `class=` over a failed
  `class` (`920-errors-recovery-model` §1).
- **Region-completeness cascade.** A failed child line leaves its region
  short, and the region's own completeness check — *bitfield has no fields*,
  *timing has no signals*, *table has no header row* — reports on the OPENER's
  line, not the failing one (`112-lex-extra-args-every-directive`, where a
  surplus positional on line 13 produces *timing has no signals* on line 12).
- **Orphan-child cascade.** A failed region opener opens nothing, so every
  child line below it reports `"<kw>" is a typed-block child …`
  (`407`, `410`, `414`).
- **Diagnostic-shape cascade.** A failed header changes the WORDS of later
  messages, per §8.3.2's fifth state effect. No extra error, a different one.

**Cascaded errors are never suppressed** — there is no de-duplication pass and
no "errors downstream of a failure are hidden" rule. The engine's one
suppression of this kind is the compact-field exception in §8.3.3, and it
works by contributing to the model, not by hiding a message.

#### 8.3.5 Regions open and close by rule

A `bitfield`, `table` or `timing` line opens a REGION that subsequent child
lines join. Both halves are normative:

- **A region opens only if its opener line completes.** An opener that fails
  at any stage opens nothing — `bitfield x "X" trailing` (arity, directive
  stage) and `bitfield x "X" at=50%` (option stage) both leave the following  <!-- fence-check: skip -->
  `field` orphaned. The **exception is the tolerated fault**: the two checks
  the engine performs AFTER building the region — a missing or invalid
  `numbering=`, and a non-integer `word=` — report and let the region open, so
  the child lines still get their own diagnostics in the same pass. These two
  are the complete list.
- **A region closes when a line reaches DISPATCH**, whether or not that line
  then succeeds. A failed opener therefore closes the region above it while
  opening none of its own (`414-block-id-cross-kind-collision`: the failing
  `timing t` closes the `table` before it, which is why the `signal` under it
  is an orphan and not a table child). The four PRE-DISPATCH refusals of
  §8.3.2 are the ones that do **not** close it: a `;` line, a lexical fault, a
  stray pipe row and a withdrawn spelling all leave the region open, and the
  next child line still lands in it.

`layout` is the same rule in a different shape: a failed `layout` line does
not open the zone, so the semantic directives after it are ACCEPTED rather
than refused by `CONTENT-LAYOUT-ZONE-SPLIT`'s gate (`920-errors-recovery-model` §2).

**A section is an island.** No id, header, single-valued slot, open region or
layout zone crosses a `figdown` line. Line numbers are re-based to the whole
file (§8.2) and nothing else about a section is shared.

#### 8.3.6 Precedence: which single error a multi-fault line reports

Between stages, the earlier stage wins (§8.3.2). Within one line carrying
several faults, three rules decide which single message it gets:

- **ONE TOKEN, ONE ERROR.** A quoted token in a BARE position (an id, an enum
  value, the header's version and genre) reports the quoting rule and
  suppresses the value check on that same token: `shape="bkx"` says the
  spelling is wrong, once, and never adds `unknown shape` (§2.1, `RULE-POSITION-ENUMERATION`).
- **The uniqueness check runs first — with one construct's exception.**
  `duplicate id`, `duplicate title line`, `duplicate flow directive`,
  `duplicate layout line`, `duplicate width` and `duplicate delimiter row` are
  reported in preference to any other directive-stage fault on the same line;
  under a two-column table, `width auto,auto` followed by `width auto,12em`
  reports `duplicate width` on the second line and swallows the bad value.
  **`pin` is the exception**: its `at=`, `width=` and `height=`
  value checks run BEFORE its uniqueness check, so a second `pin a at=zz`
  reports the point and never `duplicate pin for "a"`.
- **A `width` that fails its ARITY spends the slot; one that fails its VALUE
  does not** — so the bullet above only bites when the FIRST `width` got far
  enough to spend it. A value-failing `width` follows §8.3.3 state effect 4
  exactly: `width auto,12em` written twice under a two-column table reports
  **two** `bad width` and never `duplicate width`, because the first line
  never spent the slot and the second is therefore the first `width`. An
  arity-failing one does the opposite: `width auto` then `width auto,auto`
  reports `width has 1 values, expected 2` on the first line **and**
  `duplicate width` on the second (`510-table-width-errors`), so that failure
  DID spend the slot. Same directive, opposite answer, and unlike the other
  two asymmetries this one moves the document's error COUNT — one error or
  two from the same pair of lines, according to which check the first one
  failed.
- **The layout-zone gate is applied to a connector line ahead of its option
  pass and to every other directive after it.** One error either way, but not
  the same one: inside the zone, `edge a -> b stroke=notacolor` reports the  <!-- fence-check: skip -->
  zone and `node e "E" fill=notacolor` reports the colour.  <!-- fence-check: skip -->

The three asymmetries — `pin` against every other single-valued construct, the
connector against every other directive, and an arity-failing `width` against
a value-failing one — are **specified as they are and not repaired**.
Repairing any of them moves existing goldens, which §13 makes a
MIGRATIONS-entry change rather than a bug fix; they are filed as
**engine-backlog item 79** so
the cost is on a list rather than in this paragraph. What this section
guarantees is that both are now PREDICTABLE, which is what a second
implementation actually needed.

#### 8.3.7 Conformance compares SORTED LISTS, and order is not promised

`conformance/run.js` sorts both sides before comparing, so **an
implementation's own emission order is free** and only the multiset of
messages has to match. The sort is a plain lexicographic sort of the whole
`Line N: <message>` string, which is why `Line 18` precedes `Line 3` in a
golden — the goldens are ordered for stable diffing, not for reading. Nothing
in this specification states an emission order, within a pass or between
stages, and an implementation MUST NOT be judged on one.

#### 8.3.8 What is still UNSPECIFIED, named

- **The order of a directive's own checks beyond §8.3.6's rules.** When two
  directive-stage faults compete and neither is a uniqueness check —
  `node a B C`, which is both an unquoted label and a surplus positional —  <!-- fence-check: skip -->
  which one is reported is not stated here. The engine is deterministic and
  the goldens pin the cases the suite covers; the general rule is not written
  down, and an implementation may differ on an input no fixture reaches.
- **The message TEXT for any error no fixture covers** — unchanged by this
  section, and still §8.2's gap.

Both are narrower than what this section closed, and neither can change an
error COUNT: a line with several directive-stage faults reports exactly one
error under any reading of the rule above. **That is a guarantee about ONE
LINE, and it must not be read as one about a document.** A document's total
is not invariant, and §8.3.6's third asymmetry is exactly where it moves:
whether a failed `width` spent the single-valued slot decides whether the
next `width` is a duplicate, so the same two lines yield two errors or one
according to which check the first of them failed. That behaviour is
SPECIFIED in §8.3.6, not residue; it is named here only so this paragraph's
per-line guarantee is not mistaken for a per-document one.

## 9. Open syntax questions

- ~~`INDENTED-BLOCK-SUGAR`: indented block sugar~~ — **rejected** (second containment
  syntax; `NEW-CONSTRUCT-EVIDENCE-GATE`).
- ~~`PIN-COORDINATE-UNITS`: `pin at=` units~~ — resolved: px relative to the positioning
  context (§3).
- ~~`TABLE-COLUMN-WIDTHS`: column widths~~ — resolved: `width` is in v0.1 (§4.2); mixed
  per-column alignment stays out (census 1.4%).
- ~~`EDGE-LABEL-OFFSET-CONTROL`: edge label position hints~~ — **rejected for v0.1** (pure
  presentation; would invite pixel-level hand-tuning). User ruling
  2026-07-10: stay conservative now; if real figures ever demand it,
  it can be added later — additions are cheap under `VERSION-MIGRATION-MODEL`, and the
  three-position model (§2.3) already carries *which position* as
  semantics.
- ~~`MULTI-FIGURE-DOCUMENTS`: multi-figure documents~~ — resolved: one `.fd` produces one
  `.svg` artifact; a document MAY contain multiple top-level blocks,
  composed in document order.
- ~~`D2-RELATIONSHIP`: relationship to D2~~ — informative appendix:
  prior-art.md §3.
- ~~`EDGE-LABEL-PLACEMENT`: edge labels~~ — **resolved (2026-07-10)**: inline labels at
  the three meaningful positions, `edge A [tail] <-[mid]-> [head] B`
  (§2.3); `<-` joined the operator set;
  `label=`/`taillabel=`/`headlabel=` retired (migration 0.1).
  Survey: prior-art.md §1.
- ~~`LEGEND-MECHANISM`: legend/class mechanism~~ — **resolved (2026-07-10, `CATEGORICAL-MEANING-MAPPING`)**:
  `class` adopted (§2.7); legend strip derived. Evidence:
  prior-art.md §4 (56% of corpus figures
  carried unmapped color semantics; explicit legends only ≈3%).
- `BITFIELD-DISCRIMINATED-VARIANTS`: **discriminated variants** in bitfields — the same bits
  reinterpreted by an external mode (field feedback; register maps
  do this constantly). Current standard practice: carry the condition
  in `description="valid when …"` and flag for human review. A row-level
  `when=` or a first-class variant concept needs prior art
  (SystemRDL/IP-XACT) + corpus evidence.
- `GROUP-LEVEL-FLOW`: group-level `flow` (side-by-side sub-areas inside one
  figure; field feedback). Current standard answer: split composite
  originals into one `.fd` per concept and let the Markdown compose
  them.
- `NAMED-LANES`: **named lanes / semantic layering** —
  the unifying umbrella for the layout-carries-meaning family
  (direction · layering · side-by-side, subsuming `GROUP-LEVEL-FLOW`). `rank`
  already declares same-layer membership; the open question is a
  named, rendered lane construct. Needs a prior-art pass (PlantUML
  swimlanes, BPMN lanes, Mermaid subgraphs) + corpus frequency before
  the `NEW-CONSTRUCT-EVIDENCE-GATE` gate.
- `TABLE-SPARKLINE`: in-table **trend/sparkline** primitive.
  Current sanctioned answer: Unicode block characters (▁▃▅▇) as
  authored cell text. A first-class construct waits on corpus
  evidence (chart family ≈1%).
- `LAYOUT-ZONE-INLINE-ATTRIBUTES`: **inline presentation attributes in the rendering zone** —
  presentation attributes on semantic lines (e.g. decorative `fill=`
  on a `node` line) are currently legal in both zones. A dedicated
  spelling that moves them into the rendering zone would let a reading
  agent skip them without parsing each semantic line. Tracked as
  `LAYOUT-ZONE-INLINE-ATTRIBUTES` should field demand appear (`CONTENT-LAYOUT-ZONE-SPLIT`).
- `ROW-INDEX-GUTTER`: external per-row **index/address gutter** (a semantic per-row
  property + `gutter` placement; field feedback `ANNOTATION-FAMILY-SEQUENCING`). Sanctioned
  workaround: inline the index in the label. **Trigger fired
  (2026-07-26)**: downstream pipeline delivered a concrete `annotate`
  callout proposal (53 corpus figures) and a gutter spelling proposal
  (19 corpus figures); evidence accepted. Maintainer disposition: the
  entire annotation family (gutter, callouts, edge taps `TAP-VERSUS-JUNCTION-SPELLING`, cell
  anchors `CELL-EDGE-ANCHORS`, brace grouping `CONTIGUOUS-RANGE-GROUPING`) shares one design axis and
  will be designed holistically in one v0.2 session — no piecemeal
  ruling. All v0.2 (see `ANNOTATION-FAMILY-SEQUENCING` evidence update 2026-07-26).
  **Corroboration (2026-07-27)**: downstream first authoring pass hit
  attached-callout needs in 4 more downstream evidence figures; interim
  workaround (adjacent-floating-node) sanctioned (`ANNOTATION-FAMILY-SEQUENCING` 2026-07-27).
  **Severity note (2026-07-29 needs audit)**: the sanctioned interim
  workaround — a detached dashed node used as a callout — is
  structurally a `node`, so a reading agent counts it as a participant
  in the figure's subject matter. This is not a cosmetic defect: it
  corrupts topology counts and mixes annotation text into architecture
  descriptions. This raises `ROW-INDEX-GUTTER`'s priority within the v0.2 holistic
  annotation design session: the solution must give annotations a
  first-class identity that is syntactically distinct from participants,
  not merely a styling convention that a reader must learn to ignore.
  **That severity note is DISCHARGED (`DRAWN-ANNOTATION-FORM`), and the interim
  it describes is WITHDRAWN.** `note=` gives the aside exactly the
  first-class identity the note demanded: it is an option on the annotated
  element rather than an element of its own, so there is no second `node`
  to miscount, `nodes` still holds participants and only participants
  (§12.7), and `tools/strip-check.js` no longer sees an orphan to warn
  about. An author writing a callout today writes
  `node cache "L2 cache" note="write-back, 64 B lines"` — not a detached
  dashed node — and the topology-count corruption this entry recorded has
  no way to arise. **The CALLOUT half of `ROW-INDEX-GUTTER` is therefore SOLVED**; the
  entry stays open for its **gutter** half, which is a locator problem
  (§9 `ANNOTATION-LOCATOR-SPLIT`) and is untouched by this release.
  **The spelling half was PRE-EMPTED (`BITFIELD-REPETITION-CONSTRUCT`), and this entry no
  longer names `index=` as its candidate.** `index=` is now spent in
  `bitfield`'s namespace as the repetition range on `field`, so the gutter
  may not reuse it; the filing against that choice is **`INDEX-KEY-NAMESPACE-CONTENTION`** below.
  **`ANNOTATION-FAMILY-SEQUENCING`'s own evidence points at a better spelling anyway, and the record
  should say so rather than let a name be chosen twice.** `ANNOTATION-FAMILY-SEQUENCING`'s gutter
  evidence is **100% addresses** — *"hex addresses, one per row"*, *"n+0x0,
  n+0x4"* — and **`address` is registered nowhere**: zero rows in
  `vocabulary-sources.tsv` as a spelling (the one row whose id contains the
  word, `cell.address`, is a positional SLOT id, not a key). So on its own
  evidence the gutter's better spelling is `address=`. That is **recorded, not ruled**: the v0.2 holistic
  session decides it with the rest of the family, and this note exists so
  the session starts from the evidence rather than from a spelling that was
  never measured against it.
- `LOGIC-GATE-PRIMITIVES`: digital **logic-gate primitives** (gate shapes
  and/or/xor/xnor/nand/nor/not/buf + mux/demux; edge options
  inv=/role=select/width=). **Genre ruled IN under `GENRE-EARNING-THRESHOLD`/`LOGIC-FLOWCHART-GENRE-SCOPE`
  (2026-07-25); all work is v0.2.** Evidence (`CHART-SCOPE-BOUNDARY`): vision census +
  prior art (ANSI/IEEE 91-1984 / IEC 60617-12; no text-tool ships
  them). The full-corpus measurement now scopes and prioritizes
  delivery — it no longer gates existence. **Design direction (`LOGIC-FLOWCHART-GENRE-SCOPE`)**:
  first-class gate construct family — scene participants like
  `external`, with edge signal attributes (inversion, bus width,
  select role) — NOT a shape-enum extension; `shape=` stays purely
  geometric (`SHAPE-ENUM-VOCABULARY`/`EXTERNAL-EDGE-ENDPOINTS`); the author names the meaning and the engine owns
  the standard symbol drawing (`DOMAIN-CONVENTION-DIRECTIVES`). **Staged scope (`ELECTRICAL-SCHEMATIC-SCOPE`/`LOGIC-FLOWCHART-GENRE-SCOPE`)**:
  combinational gates + inversion + mux first; sequential elements
  (D flip-flops, latches, clock-edge symbols, reset logic) ordered by
  the frequency result. Electrical schematic symbols (capacitor,
  transistor, diode, pad, etc.) remain OUT-OF-SCOPE for FigDown
  entirely (distinct domain; `ELECTRICAL-SCHEMATIC-SCOPE`).
- `CONTIGUOUS-RANGE-GROUPING`: **curly-brace grouping** — a named set over a contiguous
  range of fields/rows/nodes (`brace <fields|rows|nodes> <range>
  "label"`), distinct from `group` (box) and `class` (legend).
  Semantic gap plausible; updated frequency ~48 downstream corpus
  figures (re-scored baseline 2026-07-26, supersedes the prior `CHART-SCOPE-BOUNDARY`
  count of ~20 from one PDF). Scope note: edge-set grouping ("ellipse
  around a set of edges") is already expressible as `bundle` (`ELECTRICAL-SCHEMATIC-SCOPE`);
  `CONTIGUOUS-RANGE-GROUPING` covers contiguous field/row/node ranges only and receives no
  expansion. All v0.2; part of the holistic annotation/targeting design
  session.
  **STILL OPEN.** The annotation half of that session
  landed (`note=`, `DRAWN-ANNOTATION-FORM`) and the locator half did **not**: the coordinate
  grammar was designed and deliberately not built (`ANNOTATION-LOCATOR-SPLIT` disposition), so
  a range over fields, rows or nodes still has nothing to name its ends
  with. `note=` does not reach this — it attaches to one whole element by
  position, which is the opposite of naming a range.
- `CELL-EDGE-ANCHORS`: **table/grid cell anchors for edges** — edges can attach to
  nodes and external endpoints, but not to a named cell within a `table`
  block. Pointer/linked-list figures (descriptor rings, free-list
  chains, name-to-slot maps — a real hardware-doc genre) therefore
  degrade to whole-table relations, losing per-cell targeting.
  Semantic argument strong; updated frequency ~40 downstream corpus
  figures (re-scored baseline 2026-07-26, supersedes the "pending"
  status from `ELECTRICAL-SCHEMATIC-SCOPE`). Sanctioned workaround: use a whole-table relation
  and note the targeted cell in an edge label or `description=`. All v0.2.
  **STILL OPEN**, for the same reason as `CONTIGUOUS-RANGE-GROUPING`: the
  locator coordinate grammar was deliberately not built (`ANNOTATION-LOCATOR-SPLIT`
  disposition). The workaround gains one option — the aside may now be a
  drawn `note=` on the edge rather than prose only a machine reads — and
  that is an ergonomic improvement, not the anchor this entry asks for.
- `FLOWCHART-GENRE-DESIGN`: **flowchart genre design** — flowchart ruled a genre under
  `LOGIC-FLOWCHART-GENRE-SCOPE` (2026-07-25). Components (decision, process, terminal, …) have
  definite meanings that a `shape=diamond` spelling forced readers to
  infer — a genre-interpretation loss under `GENRE-EARNING-THRESHOLD`/`MEANING-RECOVERY-SOURCE`.
  **(a) and (b) are CLOSED by `FLOWCHART-ROLE-KEYWORDS`.** (a) the closed role
  set is `process` / `decision` / `terminator`; (b) the spelling shape is
  *scene role constructs* — three top-level keywords following `external`,
  not a typed block following `bitfield`/`table`. What stays open:
  **(c) the migration story for existing `shape=diamond` approximations**,
  which is deliberately NON-mechanical — the corpus measurement that
  justified the vocabulary (of 216 question-labelled nodes, 78% diamond,
  14% ellipse, 8% no shape) is itself the proof that no inverse mapping
  from geometry to role exists. General node + `shape=` remains the
  baseline fallback (`GENRE-EARNING-THRESHOLD` §4): a pre-0.1 flowchart stays valid and
  readable, it simply says less. The excluded role candidates — `fork` /
  `join` / `merge`, a loop construct, a default branch, edge roles,
  swimlanes — are recorded with their measured evidence in
  [genres/experimental/flowchart.md](genres/experimental/flowchart.md) §Roles, *What is excluded*.
  **Sub-question recorded (2026-07-29, NOT a new OQ)**: AND-split/
  AND-join versus XOR-split/XOR-join distinction — whether a fan-out
  means "all branches fire" or "exactly one fires"; whether a join
  waits for all or races. Today only a free-text `class` label carries
  this distinction, so no closed vocabulary can be queried. Kept inside
  `FLOWCHART-GENRE-DESIGN` rather than filed separately: `LOGIC-FLOWCHART-GENRE-SCOPE` already ruled a gate
  construct family for the `logic` genre, and the split/join vocabulary
  for flowcharts and the logic-gate join/fork vocabulary must be
  designed together. Also record explicitly that this need is NOT
  flowchart-only — it appears in pipelines (fan-out to parallel workers
  vs alternative routes) and hardware datapaths (replicate vs demux);
  the vocabulary must generalize across genres.
- `TAP-VERSUS-JUNCTION-SPELLING`: **edge taps and junctions** (`EDGE-ENDPOINT-ELIGIBILITY`, 2026-07-26) — mid-edge
  attach points for two semantically distinct cases: branch/tap
  (observation only, no flow change) and junction (true connectivity
  split/join). Also covers annotation leaders targeting an edge region
  and packet-view extraction from a datapath edge. Downstream candidate:
  named edges (stable edge identity, a prerequisite) + a `tap` entity
  with `on=<edge>` and a role vocabulary. Related in spirit to `CELL-EDGE-ANCHORS`
  (sub-element targeting) but distinct in scope (scene-graph edges, not
  typed-block cells). Frequency: 22 downstream corpus figures. All v0.2;
  design deferred to the holistic targeting/annotation session with
  `ROW-INDEX-GUTTER`/`CONTIGUOUS-RANGE-GROUPING`/`CELL-EDGE-ANCHORS` (see `ANNOTATION-FAMILY-SEQUENCING` evidence update 2026-07-26).
  **Corroboration (2026-07-27)**: downstream first authoring pass
  independently hit the mid-edge gap in 4 downstream evidence figures
  (bus topologies flattened to star = semantic error; mid-wire taps
  modelled as duplicate edges = semantic error); intermediate-bus-node
  workaround sanctioned (`EDGE-ENDPOINT-ELIGIBILITY` 2026-07-27).
  **STILL OPEN, and the release cuts exactly one of its
  four cases.** *Annotation leaders targeting an edge region* no longer
  needs an edge locator to be readable: `note=` is accepted on `edge`,
  `flowline` and `transition`, and it is accepted there for this entry's
  own structural reason — **a connector has no id**, so an attribute is
  the only form that can reach one at all. That is a whole-edge aside, not
  a mid-edge point: the tap, the junction and packet-view extraction all
  still require stable edge identity plus the locator grammar, which was
  deliberately not built (`ANNOTATION-LOCATOR-SPLIT` disposition). Frequency and priority are
  unchanged.
- `CROSS-BLOCK-REFERENCES`: **cross-block semantic references** (`CROSS-BLOCK-REFERENCES`, 2026-07-26) —
  formalizes the gap acknowledged in `LOGIC-FLOWCHART-GENRE-SCOPE` point 4: no first-class
  mechanism for one typed block to reference an element in another typed
  block within the same `.fd`. Downstream candidate: a `rel` construct
  with typed block-qualified locators and a closed role vocabulary;
  unresolved locator = line error. 5 corpus figures are explicit
  mixed-layout integrations requiring this. Explicitly NOT a new genre
  (`LOGIC-FLOWCHART-GENRE-SCOPE` point 4 stands: hybrid figures are composition, not a new genre).
  **Also the home of SUBORDINATE COMPOSITION (`GENRE-NAMESPACE` `GENRE-COMPOSITION`)**: v0.1 composes
  genre regions in document order with no declared relation between them;
  declaring that a region is *about* an element of the host document
  ("this table is about node `X`", "this bitfield details the packet
  carried on edge `E`") belongs here. Two constraints are already fixed
  for that design: the never-implemented `table … attach=` spelling is
  not a starting point (it was removed, and the engine rejects it as an
  unknown option), and a subordination relationship is SEMANTIC, so it
  MUST live in the content zone — `pin` and the rest of the layout
  namespace cannot carry it, because reading agents ignore every member
  of that namespace by default, wherever it is written
  (§3, `GENRE-NAMESPACE`). All v0.2.
  **STILL OPEN.** `MARKER-TARGET-KINDS` widened `in=` on
  `threshold`/`band` to resolve a REGION id, which is the first time any
  key crosses from the scene into a typed block — but it is a widening of
  one key's value domain, not the block-qualified locator this entry
  needs, and it declares no relation between regions. `note=` does not
  reach this either: an aside is not a subordination claim, and §12.7
  forbids reading one out of it.
- ~~`PUBLICATION-MANIFEST-PROFILE`: **publication manifest and provenance profile**~~ —
  **CLOSED at `PUBLICATION-MANIFEST-PROFILE` (2026-08-22)**, filed 2026-07-26 (`PUBLICATION-MANIFEST-PROFILE`). **Answered
  by the ruling, tracked to a shipped document by the exit criteria.** The
  profile RULED into existence is exactly the non-core JSON sidecar this
  entry filed: `X.manifest.json`, per file, closed and versioned (`PUBLICATION-MANIFEST-PROFILE`);
  the core `document`/`language`/`renderer`/`source` blocks with the
  restatement rule; the `provenance` array with its closed `relation`
  vocabulary; the `accessibility` field as a pointer into `ACCESSIBILITY-PROFILE`'s states;
  a five-assertion verifier (`PUBLICATION-MANIFEST-PROFILE`); the four-state `review` enum and its
  hash binding (`MANIFEST-REVIEW-STATE`); `dependencies`' cache-signal rule and
  `semantic_sha256` as a designed, RESERVED slot still blocked on the semantic-projection
  work, which is registered and not yet ruled (`MANIFEST-DEPENDENCY-AND-SEMANTIC-SLOTS`); and per-element provenance, this entry's own
  "panel logical id"-adjacent want, REFUSED against a measured
  reading-contract bar (`PER-ELEMENT-PROVENANCE`). **What closing does not do**: it does not
  ship `spec/figdown-manifest.schema.json`, and it does not build the
  verifier — `decisions/registry.md` P1 carries that remaining work
  as a still-open row, narrowed but not satisfied by a ruling. Original
  filing kept below for the record.
  *As filed (`PUBLICATION-MANIFEST-PROFILE`, 2026-07-26):* a standardized non-core JSON manifest
  profile per published figure: source page/bbox provenance,
  original-image hash, panel logical id, review/render status,
  accessibility description review status, hash-based dependency
  invalidation. Non-core: no grammar impact; currently served by a
  private downstream sidecar. Post-freeze profile work, v0.2+.
- ~~`ACCESSIBILITY-PROFILE`: **accessibility contract profile**~~ — **CLOSED at
  `ACCESSIBILITY-PROFILE` (2026-08-22)**, filed 2026-07-26 (`ACCESSIBILITY-PROFILE`). **The role changed
  from this entry's own filed `role="img"` to `role="graphics-document"`,
  on the first-hand W3C reading `spec/standards-claims.tsv` registers as
  S223–S238 (`ACCESSIBILITY-PROFILE`): `role="img"` carries ARIA's `img`
  *Children Presentational: True*, which would flatten away the
  per-element `<title>` elements this project's own artifacts already
  carry, against the WAI-ARIA Graphics Module's own SHOULD for exactly
  this class of structured graphic. `role="img"` survives only as a
  publisher's DECLARED downgrade, recorded in the manifest.** The
  non-visual `<title>` and the decoration rule land the same way, also
  `ACCESSIBILITY-PROFILE`. The `<desc>` semantic summary lands as a five-state vocabulary
  (`absent`/`derived`/`generated`/`authored`/`reviewed`) rather than the
  filed *"reviewer-approved text (not free LLM generation)"* read as a
  blanket ban: `ACCESSIBLE-DESCRIPTION-SOURCES` keeps this entry's ban on SILENT machine text
  permanently, and separately DECLINES a blanket ban on `derived` text,
  which is a function of the model rather than free generation. The
  Markdown alt-text convention lands in `spec/host-profile.md` §1.2, not
  in this profile (`ACCESSIBLE-TEXT-EMISSION`); `aria-hidden` for decorative output lands in
  its weak, MAY form (`ACCESSIBILITY-PROFILE`); and validation severity per profile lands as
  a six-assertion A–F set, with assertion E (non-empty `alt` on every
  embed) shipped immediately as `gate:alt` (`ACCESSIBLE-TEXT-EMISSION`). **What closing did
  not do**: it did not ship the `with-a11y` render option that makes a
  renderer emit any of this, and it did not build assertions A–D/F of the
  verifier. **Both shipped 2026-08-22 under `decisions/registry.md`
  P2, now SATISFIED**: the profile is normative at
  [`figdown-a11y.md`](figdown-a11y.md), the render option is §7's second,
  and the verifier is `tools/a11y-check.js` (`gate:a11y`). Original filing
  kept below for the record.
  *As filed (`ACCESSIBILITY-PROFILE`, 2026-07-26):* a normative profile covering SVG
  `role="img"`, non-visual `<title>` (distinct from the `TITLE-RENDER-DEFAULT` visual-title
  render option), `<desc>` semantic summary from reviewer-approved text
  (not free LLM generation), Markdown alt-text conventions, `aria-hidden`
  for decorative output, and validation severity per profile. New
  territory; profile-level, v0.2+.
- `TIMING-MEASUREMENT-ANNOTATIONS`: **timing measurement annotations** (`TIMING-MEASUREMENT-ANNOTATIONS`, 2026-07-27) —
  named timing parameters (setup, hold, cycle) spanning two signal events,
  with dimension arrows, edge-alignment guides, and cycle numbering. The
  timing genre has no construct that names a span between two specific
  signal events; floating text fails `MEANING-RECOVERY-SOURCE`. Direction: model a timing
  parameter as a named semantic relation between two signal-event locators
  (nth rising/falling edge of a named signal); renderer derives dimension
  arrows and guides automatically (prior art: WaveDrom anchor-and-relation
  model). Signal-event locators are part of the secondary-target family;
  design deferred to the v0.2 holistic targeting/annotation session.
  Downstream evidence: 4 downstream evidence figures affected, 2 outright
  unrepresentable (`TIMING-MEASUREMENT-ANNOTATIONS`). Interim: keep original raster + prose. All v0.2.
  **STILL OPEN.** The signal-event locator was designed
  and deliberately not built (`ANNOTATION-LOCATOR-SPLIT` disposition), and `note=` is not
  accepted on `signal` at all — an aside beside a lane would be floating
  text against two events it cannot name, which is the `MEANING-RECOVERY-SOURCE` failure this
  entry already rules out. Nothing here is unblocked.
  **Production evidence, 2026-08-20:** a downstream figure adds the
  real-duration case — equal-width cycles cannot carry the ratio between a
  20-second cell and a sub-second one, and the sanctioned workaround is a
  not-to-scale statement in the `title`, which makes the figure truthful by
  subtracting the fact.
- `BYTE-UNIT-PACKET-BLOCKS`: **byte-unit packet-block figures** (2026-07-27) — downstream
  first authoring pass found one figure where `bitfield` was misused for
  a byte-sized header block diagram, producing a misleading bit ruler.
  First-party corroboration now exists: the project's own
  packet-encapsulation flagship example could not express byte-sequence
  order without geometry, and the accepted workaround is a single-row
  `table`, which works but reads as tabular data rather than as a packet
  on the wire. Open question: whether a byte-oriented sequence construct
  (or a scale-less bitfield mode using `word=8` with suppressed ruler) is
  warranted, or whether `table`/`node` remains the answer. Frequency
  evidence still pending from corpus measurement; the `table` workaround
  is sanctioned meanwhile. Cross-reference `CELL-EDGE-ANCHORS` (cell anchors) since
  the table workaround loses the ability to draw a relation to one field.
- `MEANINGFUL-ARRANGEMENT`: **declared meaningful arrangement** (`MEANINGFUL-ARRANGEMENT`, 2026-07-29) — there is
  no positional equivalent of `class`: no way for an author to assert "this
  arrangement carries knowledge, and here is what it says". Where the
  language has no construct for a spatial meaning — layer stacks (stack
  order, cross-layer alignment, via spans), memory maps, floorplans — the
  meaning can only be encoded in `pin`, and the two-zone contract
  (§3) then tells readers to discard exactly those lines. Direction ruled
  (spelling deferred to the v0.2 design session): the author declares in the
  content zone that the figure's arrangement is load-bearing and states in
  prose what it expresses; such a declaration SUSPENDS the default-ignore
  rule for that document (a reading agent MUST read the layout zone),
  and the prose statement is MANDATORY so the knowledge survives for a reader
  that cannot interpret coordinates — which is what keeps the standard above
  ASCII art. Genre constructs (`GENRE-EARNING-THRESHOLD`/`LOGIC-FLOWCHART-GENRE-SCOPE`) remain the destination for recurring
  spatial domains; layer/stack, memory map and floorplan are candidate
  genres, and the escape hatch is the bridge until they land. Diagnostic
  already shipped: `tools/strip-check.js` flags elements whose only relation to
  the figure is geometric — a document that trips it is either a semantics
  gap or an arrangement that needs declaring. v0.2.
  **Evidence note (2026-07-29 needs audit):** the proportional memory map is
  a distinct sub-case of `MEANINGFUL-ARRANGEMENT` — a figure whose primary knowledge is that a
  region occupies a given FRACTION of an address space, where area is the
  encoding. This is distinct from the lookup-table memory map (address values
  in cells), which is fully covered today. The audit's corpus arithmetic is
  approximate: memory-map figures were about 14% of the table-census slice
  (`TABLE-MERGE-SUPPORT`), and the table slice about 10% of the corpus, so on the order of 1%
  overall — small but a standard convention in hardware documentation.
  Confirmation of the exact count is needed before this sub-case triggers any
  new construct under `NEW-CONSTRUCT-EVIDENCE-GATE`.
- `QUANTITY-EXTENT-GENRE`: **a quantity-extent genre — candidate under `GENRE-EARNING-THRESHOLD` (2026-08-06,
  `QUANTITY-EXTENT-GENRE`)** — "a quantity extent carrying named reference values and named
  regions". The scalar-marker need is real, but it is **genre-shaped, not
  keyword-shaped**, and `GENRE-EARNING-THRESHOLD` governs: *"General constructs like `block`
  must not be over-stretched to satisfy it — doing so loses much of the
  original intent"*, and *"approximating a genre with general
  block-and-edge draws a similar picture while discarding the meaning:
  'looks right' is not 'expressed'."* The buffer-region figure that
  motivates this draws 11 sized nodes and three classes all named
  "region"; it looks right, and "these are the buffer's regions and their
  bounds" is not expressed anywhere a reader can find it.
  **Evidence (corpus measurement over two downstream trees, 643 `.fd`
  files, 163 module pages, two chip families):** ~22 figure-identities
  need it, concentrated in ~10 of 163 module pages and spread over ≥5;
  **12 of the 22 were never drawn at all** — the downstream
  "zero-Connect → GFM table" rule routed them away, because a
  threshold-scale figure **has no edges by nature** and so trips that
  rule every time. The need has its own vocabulary — thresholds, named
  regions, capacity bounds, guarantee floors, watermark quads — which is
  what `GENRE-EARNING-THRESHOLD` means by a genre earning its own words.
  **Prior art:** HTML's `<meter>`, defined as *"a scalar measurement
  within a known range"*, is exact.
  **`threshold` and `band` are the `GENRE-EARNING-THRESHOLD` INTERIM general constructs, and
  they are deliberately NOT frozen** (§2.6, §10). Freezing them would be
  the over-stretching `GENRE-EARNING-THRESHOLD` forbids and would foreclose this genre by
  binding two scene constructs to the compatibility promise. `GENRE-EARNING-THRESHOLD` supplies
  the interim in its own text: general constructs *"let the user at least
  draw the figure they want to express, so authors are never left without
  a way forward."* The 0.1 renames (`THRESHOLD-KEYWORD-SPELLING`/`BAND-LABEL-STATUS`) correct defects in
  them; they do not promote them. All v0.2+.
- `ANNOTATION-LOCATOR-SPLIT`: **the annotation family is TWO families — annotations and
  locators (2026-08-06, `ANNOTATION-LOCATOR-SPLIT`)**. Five open questions were filed against
  "the annotation family" and scheduled for one holistic v0.2 session
  (`ROW-INDEX-GUTTER`'s disposition). They do not share one design axis; they share
  two, and the split is what bounds that session:
  - **annotations** — *a body attached to a target*. W3C's Web Annotation
    Data Model frames it exactly: an annotation has "0 or more Bodies …
    1 or more Targets". This is `ROW-INDEX-GUTTER`'s **callout half** and `NON-GRAPH-ANNOTATION-NODE`.
  - **locators** — *the grammar for naming a sub-element as a target*.
    This is `CELL-EDGE-ANCHORS` (a table cell), `TAP-VERSUS-JUNCTION-SPELLING` (a point on an edge), `CONTIGUOUS-RANGE-GROUPING`
    (a contiguous element range), `TIMING-MEASUREMENT-ANNOTATIONS` (a signal event), and `ROW-INDEX-GUTTER`'s
    **gutter half**.
  Locators are **not annotation vocabulary**: edges need them too —
  `TAP-VERSUS-JUNCTION-SPELLING`'s junction case is real connectivity, not commentary — so the
  locator grammar must be designed ONCE and consumed by annotations,
  edges and whatever comes after. **Evidence that this is the live
  constraint, not a taxonomy:** the two WRED `table`-genre figures in the
  corpus need a threshold marker on a **table**, which has neither nodes
  nor groups; `in=` on `threshold`/`band` takes a node or a group id, so
  those figures cannot use the construct at all and became GFM tables.
  That is a **locator** problem, not an annotation one, and no amount of
  annotation vocabulary fixes it.
  **The v0.2 annotation target key must be `on=`**, not a third sense of
  `in=` — recorded in SYNTAX-STYLE §8.1, which holds the declared `in=`
  exception and the standing promise that a third sense is refused.
  Choosing `on=` **closes** that exception instead of deepening it.
  **Measured demand ranking (2026-08-06):** `note`/callout demand is
  roughly **3×** the scalar-marker demand — ~66 figure-identities (96
  instances across 32 figures expressed as workarounds, 48 across 34
  dropped entirely) and, decisively, **20 distinct annotation `class`
  declarations**: the same construct independently reinvented twenty
  times because the language has no `note`. If capacity exists for one
  v0.2 annotation construct, it is `note`. (Its spelling arrives as a
  borrow, not a coinage: UML/PlantUML `note`, Mermaid `note`, GFM
  `> [!NOTE]` — see vocabulary-sources.tsv.) All v0.2.

  **Source added 2026-08-13, and its limit stated with it.** ISO 5807:1985
  — `flowchart`'s own source standard — **§9.4.3 defines an annotation
  symbol**, reached from §9.3.2.3 (*Dashed line*), which cross-references
  it as the symbol surrounding *"an annotated area"*. Read first-hand from
  the published preview. That makes the drawn annotation a construct this
  domain's standard already has, not only a UML notation and a tool
  vernacular, and it strengthens RULE 4.1's case for building it. **It is
  not a source for the SPELLING**: the clause number is confirmed from the
  readable region, the symbol's NAME sits past page 8 and has not been
  read, so nothing here says ISO calls it `note`. The borrow stays
  **partial** on UML's notation plus the four-way tool attestation. Where a
  word is the domain's common term rather than the standard's orthography,
  this spec says so in those words.

  **DISPOSITION (`DRAWN-ANNOTATION-FORM`/`MARKER-TARGET-KINDS`). The ANNOTATION half is answered;
  the LOCATOR half is not, and was deliberately not built.** What landed,
  and what each part of this entry is owed:

  - **The annotation construct is an OPTION KEY, `note=`, and not a
    standalone `note` KEYWORD** (§2.9, §10). The keyword shape was
    measured against the same corpus that justified the construct, and it
    is **NOT EARNED**. Of 70 annotation instances across 23 figures of a
    643-file corpus, **75.7% are about exactly ONE element**, **14.3%
    about the figure with no element**, and **10.0% span two or more**; a
    second 252-file flowchart sub-corpus puts spanning at **6.7%**. A
    keyword buys exactly one thing an attribute cannot buy — a body with
    more than one target — and it buys it for the smallest of the three
    classes while costing a top-level word, a target key and a locator
    grammar. Attachment by syntactic position covers 75.7% with no
    spelling at all, and a `note=` on `title` covers the 14.3% figure-level
    class, which is what removes the last argument for a keyword: the
    figure has a declaration line, so position reaches it too.
  - **What would REOPEN the keyword**, stated so the question is decidable
    rather than re-litigated: (a) a corpus in which spanning demand
    **exceeds** single-element demand — the ratio, not the absolute count,
    because the attribute already serves the majority; or (b) a measured
    need to make many-to-one attribution **machine-readable** rather than
    a footnote marker, i.e. a reading agent that must answer *which
    elements is this aside about?* from the model. Either would be a
    filing under `NEW-CONSTRUCT-EVIDENCE-GATE` with its own count.
  - **The interim for the spanning residual is the corpus's OWN
    workaround**, adopted rather than invented: a **footnote marker in the
    label text** — `*`, `**`, `#`, `##` — on each element the aside covers,
    with the aside itself carried in one `note=`. It is legal today, it
    draws, and a human reads it the way they read a footnote anywhere
    else, which is why the corpus reached for it unprompted. **Its one
    loss is stated rather than glossed:** the correspondence between the
    marker and the note is **not machine-readable** — the model records
    two strings and no relation between them, and §12.7 forbids a reader
    inferring one from the text. That loss is exactly what (b) above
    measures, and it is the honest price of not spending a keyword on
    10.0%.
  - **`NON-GRAPH-ANNOTATION-NODE` is SOLVED** by the same release — see its entry below.
  - **The LOCATOR half is untouched, and `CONTIGUOUS-RANGE-GROUPING`, `CELL-EDGE-ANCHORS`, `TAP-VERSUS-JUNCTION-SPELLING`, `CROSS-BLOCK-REFERENCES`
    and `TIMING-MEASUREMENT-ANNOTATIONS` all stay OPEN.** The coordinate grammar this entry calls
    for (`in=q(3)`, addressing a row inside a region) was designed and
    **deliberately NOT built**: it has no shipping consumer,
    and SYNTAX-STYLE RULE 4.7 argues against spending a grammar before one
    exists. It stays on the record, unbuilt. What DID land is narrower and
    must not be mistaken for it: **`in=` on `threshold`/`band` now
    resolves a REGION id** (a `bitfield`, `table` or `timing` block) in
    addition to a node or a group — `MARKER-TARGET-KINDS`, a widening of the value domain,
    **not a third sense of `in=`** and not a new spelling. That unblocks
    the two WRED `table`-genre figures named above, which is the concrete
    evidence this entry rests on; it addresses nothing about naming a row,
    a cell, a point on an edge or a signal event. The `on=` reservation
    for a future annotation TARGET key stands unspent (SYNTAX-STYLE §8.1),
    because `note=` needs no target key at all.
  **A LABEL is a sub-element, so LABEL COLOUR is filed here (
  `COLOUR-KEY-STATUS`/`LABEL-COLOUR-SOURCE`).** v0.1 has no label-colour key and will not gain one: the
  reason is structural, not demand. Measured on the reference engine, a
  `color=` on an `edge` coloured **all three** of `[tail]`, `[mid]` and
  `[head]` identically, and no syntax could tell them apart —
  `tailcolor=`, `midcolor=` and an in-operator form `-["M" color=…]->`
  are all line errors. So **every owner-level key that could be added
  today is the wrong shape**: it would foreclose the right design rather
  than approximate it. Graphviz is the worked precedent — it needed a
  SECOND key (`labelfontcolor`) the moment an edge carried more than one
  label. The colour has to attach to the LABEL, which is exactly the
  locator problem above: naming a sub-element as a target, the same
  problem as a table cell (`CELL-EDGE-ANCHORS`), a point on an edge (`TAP-VERSUS-JUNCTION-SPELLING`) and a
  contiguous range (`CONTIGUOUS-RANGE-GROUPING`). What makes the absence tolerable in the
  meantime is `LABEL-COLOUR-SOURCE`'s derived default (§5): the label colour is computed
  from the background it sits on, so it is right by construction on every
  fill an author can write, and the case that motivated an explicit key —
  light text on a dark box — needs no key at all.
- `POSITIONAL-VS-NAMED-ARGUMENTS`: **MAY a positional argument also be written as a named
  argument?** (2026-08-07, maintainer proposal — **v0.2, explicitly NOT
  for the v0.1 freeze**.) The proposal borrows Python's
  positional-or-keyword parameter model: an argument written without a key
  is read by standard position, and the same argument written WITH a key
  may appear in any order. So `field "Checksum" 16 present="C = 1"` would
  also be writable as
  `field name="Checksum" bitlength=16 present="C = 1"`. (The example <!-- fence-check: skip -->
  originally used the bare flag `optional`, spelled `conditional` when the
  question was written; `PRESENCE-FLAG-SPELLING` reverted that and `PRESENCE-CONDITION-EXPRESSION` replaced
  the flag with `present=`, which removes one of this
  question's two motivating cases — see `POSITIONAL-FLAG-SPELLING`.)
  **What it buys:** `bitlength=16` is self-documenting where a bare `16`
  is not — a real readability gain for machine-generated content and for a
  reader who does not know the grammar — and order-independence removes a
  failure mode from the generating side.
  **The tension that must be resolved before it can be adopted**, stated
  rather than softened: it collides head-on with SYNTAX-STYLE RULE 5 —
  *two forms of one construct are justified ONLY when each accepts input
  the other cannot express; if both forms have the same accepted-input
  set, one is a spelling variant and MUST be retired.* `field "A" 8` and
  `field name="A" bitlength=8` produce an **identical model**, and <!-- fence-check: skip -->
  order-independence is not new input — it is the same input in a
  different order. **Under the project's own test this fails as a spelling
  variant.** Adopting it therefore requires either a stated exception with
  its cost written out, or a demonstration that the named form accepts
  something the positional form cannot.
  **Prior art, and one caution.** HTML/XML is **not** a precedent for the
  hybrid: it has no positional form at all, so it is a precedent for
  keyword-ONLY. Every diagram language surveyed uses the shape FigDown
  already has — a small number of positionals (the id, the label) plus
  keyword options: DOT (`node [label=…, shape=…]`), Mermaid, D2, TikZ.
  None makes its positionals nameable. Python's own experience is the
  caution: it later had to add `/` and `*` markers to force parameters
  positional-only or keyword-only, because **once a parameter name is
  public as a keyword, renaming it is a breaking change**. For FigDown
  that means `bitlength=` enters the compatibility promise the moment it
  ships — and the v0.1 freeze renamed a dozen keys precisely because their
  first names were wrong (`SIZE-AND-DIRECTION-KEY-NAMING`, `TIMING-LANE-ALPHABET`–`THRESHOLD-KEYWORD-SPELLING`).
  **What a v0.2 evaluation must answer.** (1) Does the named form accept
  any input the positional form cannot? (2) If not, what exception
  justifies two spellings, and what does that exception cost applied
  consistently to every directive? (3) Which names become frozen public
  API, and are they the right names — noting that `bitlength=` is itself a
  naming decision that has never been reviewed.
- `POSITIONAL-FLAG-SPELLING`: **SHOULD bare positional flags become option keys carrying a
  value list?** (2026-08-07, maintainer proposal — **v0.2, explicitly NOT
  for the v0.1 freeze**.) Separable from `POSITIONAL-VS-NAMED-ARGUMENTS` and, in the maintainer's
  judgement, **the stronger half**. **One of its two cases has since been
  settled by other means:** `optional` on a `bitfield` `field` became the
  option key `present=` (`PRESENCE-CONDITION-EXPRESSION`) — carrying a condition rather
  than a value list, so the flag family did not survive to need a list.
  What remains is `highlight` (on a `table` `cell`), and any future flag.
  The proposal spells a flag as an option key carrying a comma list:
  `cell 1 attributes=highlight`. <!-- fence-check: skip -->
  **Why this is the stronger half.** A bare flag cannot grow.
  `attributes=optional,deprecated` is expressible; two bare flags are
  not — bare words have no terminator, no order, and no way to say "no
  attributes". This is RULE 3.1 (`POSITIONAL-LIST-SPELLING` — *a variable-length list needs
  an explicit terminator so the line end stays free for future `key=`
  options*) applied to FLAGS rather than to lists, and it needs **no**
  two-form exception: it replaces one spelling with another rather than
  adding a second, so RULE 5 is satisfied outright.
  **What a v0.2 evaluation must answer.** (1) Is `attributes=` the right
  key name, or should each flag family have its own key? (2) Do the flags
  of different genres (today only `highlight` on `cell`, plus whatever a
  future genre adds) belong in one namespace or separate ones — `GENRE-NAMESPACE` `GENRE-VOCABULARY-OBLIGATION` makes option keys
  per-genre, so one spelling could carry different value sets per genre.
  (3) Migration is mechanical and small (in-repo only), but it is a
  frozen-surface change and therefore owes a MIGRATIONS entry and a named
  diagnostic.
- `IDENTITY-ASSERTION`: **identity assertion** ("these two elements are the same entity")
  (2026-07-29 needs audit) — there is no equivalence or alias relation in
  FigDown: `class` asserts shared CATEGORY, not identity. This gap was the
  strongest finding of the 2026-07-29 needs audit: two independent audit
  slices converged on the same missing concept. *Structure slice*: one real
  component appearing in two views (e.g. a table participating in both a
  control-plane and a forwarding-plane view within the same document) must
  today be duplicated as two nodes joined by a prose-labelled edge — a
  reading agent counts two entities. *Data slice*: the same register field
  shown under two bit-numbering conventions, or a register field reused
  across two layered views, can only share a `class`, which asserts shared
  CATEGORY, not shared IDENTITY. Analysis: `class` is category membership;
  there is no equivalence/alias relation. Under `MEANING-RECOVERY-SOURCE` a reader derives meaning
  from constructs, so "same thing shown twice" is currently unrecoverable.
  `NEW-CONSTRUCT-EVIDENCE-GATE` status: semantic impossibility is arguable and the convergence of two
  independent slices on the same gap is real evidence, but corpus FREQUENCY
  has not been measured — the ruling trigger is a targeted count of
  dual-view/dual-context figures. Prior art to survey: identity/alias
  mechanisms in modelling languages (UML's object identity across views,
  SysML parts), and how ERD tools handle the same entity appearing in
  multiple diagrams. All v0.2; pending frequency measurement.
  **Where else this one gap is filed (`OPEN-QUESTION-CITATION-STATUS`).** It is ONE primitive with
  three faces, and until 0.2 none of the three named the others:
  (1) this entry, the MODEL-side statement; (2)
  the project’s working record — ISO 5807's **Connector**, in the *"Not
  in this repository's record of the standard"* table, status **unknown**
  (§9.4.1 was never read). That is not a second requirement: the Connector
  is `flowchart`'s **drawing** of the model fact this entry describes — a
  marked continuation rather than a second symbol — so a decision here
  decides how the drawing is spelled, not whether it is needed; (3)
  `examples/showcase/tcp-state-machine.fd` — the live instance. It draws
  CLOSED twice because RFC 9293's Figure 5 does, so under `statechart`,
  whose premise is that one `state` is one state, a reading agent counts
  **12 states where TCP has 11**; only a `class` legend line says
  otherwise. Merging the two nodes was tried and REJECTED on readability
  evidence (the terminal edges struck the setup/abort labels around a
  single CLOSED), so that figure is evidence that the primitive is
  MISSING, not that a workaround exists.
  `spec/genres/experimental/statechart.md` states what a reader may and may
  not conclude from two identically-labelled states meanwhile. Note that
  `statechart` has **no UML clause** to borrow for this: UML 2.5.1 §14 has
  no "same state drawn twice", so this gap is NOT covered by any claim that
  the genre's unexpressible items are §14 surface deliberately not
  borrowed. **Trigger named (2026-08-17):** the dormant "targeted count of
  dual-view/dual-context figures" trigger above is now stated operationally,
  with an owner, in
  element-identity-proposal.md §9
  — measurement still owed, not yet taken.
- `BITFIELD-UNION-VARIANTS`: **union/case bitfields** (`PRODUCTION-CORPUS-MEASUREMENT`, 2026-07-30) — CORRECTNESS trap:
  `break` rows currently read as one contiguous bit sequence, so a
  register whose bits have mutually exclusive encodings (e.g. eight
  prefix-selected interpretations of an 18-bit field) computes the
  wrong cumulative width when each encoding is expressed as a separate
  `break` row. A conforming reader following §4.1 gets confidently wrong
  bit-width values with no warning. Candidates: (a) a union/case marker
  on the `bitfield` block so rows are offset-independent; (b) a declared
  total width so mismatched field sums are a line error. Both under
  evaluation; interim: express each encoding alternative as a separate
  `bitfield` block. Priority: high (CORRECTNESS; zero-warning parse
  failure). All v0.2.
- ~~`NON-GRAPH-ANNOTATION-NODE`: **annotation node that does not join the graph**~~ —
  **SOLVED (`DRAWN-ANNOTATION-FORM`)** by `note=` (§2.9) together with the
  figure-level form, a `note=` written on the `title` line. The entry is
  kept in full below because two of its paragraphs are the evidence the
  solution was built from, and because the shape of the answer is the
  thing worth recording: the construct is **not** a participant that a
  convention asks readers to ignore — it is **not an element at all**. An
  aside about one element rides on that element's own line, so `nodes`
  keeps holding participants and only participants (§12.7); an aside about
  the whole figure rides on `title`, which is the one line every figure
  has and no figure's subject matter contains. Both of this entry's named
  defects therefore stop being reachable rather than being mitigated:
  `strip-check.js` has no orphan to warn about, and a reading agent has no
  extra `node` to miscount. The **capability loss** the `SHAPE-ENUM-VOCABULARY` corroboration
  below records is repaid in the same release — the author who borrowed
  `shape=cloud` to get "a shape that reads as a comment bubble" now has a
  shape that IS one, the UML folded-corner note box, and does not spend a
  geometric enum value to get it. *As filed (`PRODUCTION-CORPUS-MEASUREMENT`,
  2026-07-30):* a floating explanatory block that comments on the
  figure as a whole without being a participant. Authors today use an
  orphan dashed node, which `strip-check.js` flags as a warning and
  which a reading agent counts as a participant, corrupting topology
  descriptions. Distinct from `ROW-INDEX-GUTTER` (attached callouts with a target):
  the floating comment addresses the entire figure with no target.
  Candidate: a `note` or `comment` construct that is layout-visible but
  semantically outside the participant set. Priority: medium. All v0.2.
  **Measured corroboration (2026-08-06, `SHAPE-ENUM-VOCABULARY` — filed under `NON-GRAPH-ANNOTATION-NODE` rather
  than as a new OQ, because it is the same gap)**: while removing
  `shape=cloud` from the enum, every downstream use of that shape in a
  production corpus was inspected. All but one were domain icons
  (labels naming a network, a transport, a fabric). The exception was a
  node whose label is several lines of explanatory field values — a
  callout, not a participant — where the author had borrowed the cloud
  purely to get a shape that reads as a comment bubble. This is
  evidence of a kind the sanctioned-workaround note above does not
  have: the author did not reach for the dashed orphan node the interim
  guidance suggests, they reached for a DIFFERENT SHAPE, because no
  construct in the language says "this is an aside". The retirement
  leaves that use with no replacement at all, which raises this OQ's
  priority from medium: it is now a documented capability loss, not
  only an ergonomic gap. Design still deferred to the v0.2 holistic
  annotation session with `ROW-INDEX-GUTTER`/`CONTIGUOUS-RANGE-GROUPING`/`CELL-EDGE-ANCHORS`/`TAP-VERSUS-JUNCTION-SPELLING` — this note records the
  measurement, not a proposal.
  **The capability loss is CLOSED (`DRAWN-ANNOTATION-FORM`).** From
  `figdown 0.3` the aside that borrowed the cloud is written
  `note="…"` on the element it is about, or on `title` when it is about
  the figure, and it draws as the UML note symbol. `SHAPE-ENUM-VOCABULARY`'s retirement of
  `shape=cloud` no longer costs a capability: the one non-icon use it
  removed has a construct of its own, which is the outcome `SHAPE-ENUM-VOCABULARY` said it was
  owed and could not supply at the time. The `shape=` enum stays purely
  geometric (`SHAPE-ENUM-VOCABULARY`/`EXTERNAL-EDGE-ENDPOINTS`) and gains nothing back — the aside was never a
  geometry question, which is precisely why a shape was the wrong carrier
  for it.
- `REPEATED-SUBGRAPH`: **repeated subgraph / instantiation** (`PRODUCTION-CORPUS-MEASUREMENT`, 2026-07-30) —
  no construct exists to declare that a sub-structure of K nodes and M
  edges appears N times; authors must spell out all N copies in full.
  Structural sibling of the multiplicity candidate in `AUTHOR-INTENT-AUDIT` §7(a) (K=1,
  M=0); the two should be designed together. A reading agent can infer
  structural isomorphism but cannot recover the author's intent that
  these are instances of one pattern, not independently authored.
  Priority: medium (authoring burden; reading agents tolerate but do
  not identify repetition structure). All v0.2.
- `BITFIELD-REPETITION-CONSTRUCT`: **`bitfield` had no repetition construct, and still has no way to
  derive a count from another field** — a **DEFECT the project owes a fix
  for**, not a limit it chose (2026-08-07, `BITFIELD-REPETITION-CONSTRUCT`; the first half is fixed, see the end of this entry). *As filed:* a repeated element must be spelled out
  as N literal `field` lines, and nothing in the model says the list is a
  sample rather than the whole. Hits, measured in this repository's own
  corpus: `srh.fd` (RFC 8754's Segment List is `Last Entry + 1` addresses and
  the file writes two, `[0]` and `[n]`, with `Last Entry` declared as an
  ordinary 8-bit field that nothing links to the list); `mpls.fd` (RFC 3032's
  label stack is 1..n entries terminated by S=1; the file writes exactly two
  and the model asserts two); `dns.fd` (QDCOUNT/ANCOUNT/NSCOUNT/ARCOUNT are
  literally the repeat counts for four repeated sections, with nothing
  linking them). **This is the one gap that degrades to a *confidently
  wrong* number** rather than to "unknown", which is why §12.7 carries a
  MUST NOT about it rather than leaving it to the genre doc.
  **Prior art, and why it does not rescue this.** All three of `bitfield`'s
  own declared sources — RFC packet ASCII art, WaveDrom bitfield JSON,
  Mermaid packet-beta — have **no repetition construct at all**. The one
  notation in this space that solves it is
  **draft-mcquistin-augmented-ascii-diagrams** (`[Blocks]` plus a constraint
  expression over sibling field names, ABNF in its Appendix A.1), and that
  draft **expired in 2024 and was never WG-adopted**. Every mature answer —
  Kaitai Struct `repeat-expr`, DFDL `occursCount`, Scapy `count_from` — lives
  in a parser/schema notation that draws no picture, so borrowing one would
  import a schema language into a figure language. Designing this needs the
  locator problem (`ANNOTATION-LOCATOR-SPLIT`) solved first, because a count field must be
  NAMEABLE. All v0.2.
  **HALF CLOSED (`BITFIELD-REPETITION-CONSTRUCT`). The repetition construct exists:
  `index=`.** A field may now declare that it is one element of a run and give
  the run's index range, the model records it as a key rather than as bracket
  text, and the drawn elision is derived from it
  ([genres/bitfield.md](genres/bitfield.md), §12.7). Where both ends are
  literal the run is DETERMINATE, so the count is in the document and every
  later offset is computable again — the arithmetic this entry said was
  destroyed. `srh.fd` is the first adopter and no longer writes `[0]` and
  `[n]` as two fields; the first element, the elision row and the last element
  RFC 8754 draws are all derived from the one key rather than unwritable
  (`REPEATED-RUN-DRAWING`, 0.1 — until then only one element was drawn).
  **What is still open is the OTHER half, and it is one thing rather than
  four: a value cannot NAME another field.** `index="0..Last Entry"` leaves
  the last index as prose because the language cannot resolve `Last Entry` to
  the field of that name, so `srh.fd`'s run stays indeterminate — reached now
  from SYNTAX rather than from label text, which is the whole of what
  changed there. `dns.fd`'s QDCOUNT/ANCOUNT/NSCOUNT/ARCOUNT are the same gap
  in its purest form: four literal repeat counts with nothing linking them to
  what they count. `mpls.fd` is a third shape — a run terminated by a
  CONDITION (S=1) rather than by a count, which no index range can state
  either. All three are downstream of **`ANNOTATION-LOCATOR-SPLIT`**, the locator problem, and
  this entry is carried forward as the record of what `index=` did not
  solve. All v0.2.
- `BITFIELD-RANGE-WIDTH`: **a field width may not be a RANGE** — a **DEFECT**, not a
  documented limit (2026-08-07, `BITFIELD-REPETITION-CONSTRUCT`). A `field`'s width is a fixed integer
  or `*`, and `*` means "fill the remainder of the current row": a **drawing
  instruction**, not a fact about the wire. Real headers state ranges as
  facts — Ethernet payload 46–1500 bytes, IPv4 Options 0–40 octets, IPv4
  Padding 0–24 bits, TCP Options 0–320 bits, QUIC's variable-length integers
  1/2/4/8 octets — and the language cannot say any of them. **13 corpus
  fields carry range language in their label or description while declaring a
  fabricated fixed width.** The sanctioned interim expression is
  to use `*` where it is honest and otherwise to state the real extent in the
  label AND in a `class` meaning, which is in the model (§12.7) — never to
  substitute another invented number. Note that this is **not** the "at most
  one `*` per block" question: that rule is about the drawing,
  and relaxing it would not let a document state 46–1500. All v0.2.
- ~~`EDGE-GEOMETRY-CONSTRUCTS`: **`LAYOUT-ZONE-NAMESPACE` reserves `path` and `routing` language-wide, and both are
  ordinary general-purpose words**~~ — **CLOSED (`EDGE-GEOMETRY-CONSTRUCTS`)**
  (filed 2026-08-07 under `LAYOUT-ZONE-NAMESPACE`). **The reservation is gone because the
  reserved words are gone.** `EDGE-GEOMETRY-CONSTRUCTS` withdrew `path` and `routing` from the
  language outright (§1 `LAYOUT-ZONE-NAMESPACE`, §3, §10), so the layout zone now contains
  **`layout`, its `UNIVERSAL-CORE-KEYWORDS` opener, and `pin`, `LAYOUT-ZONE-NAMESPACE`'s one remaining member — and
  nothing else** (§10 (a′)), while both withdrawn spellings are
  **RELEASED**:
  they belong to no namespace, and a future genre may claim either as its
  own keyword under `GENRE-VOCABULARY-OBLIGATION` through the ordinary `NEW-CONSTRUCT-EVIDENCE-GATE` gate.
  **The sharpest case, stated because it is what made the exposure real.**
  POSIX / ISO 9945 spells a filesystem pathname `path`. A future tree or
  filesystem genre would therefore have been **REQUIRED by SYNTAX-STYLE
  RULE 4.1 — take the standard's own spelling — to use a word `LAYOUT-ZONE-NAMESPACE` had made
  permanently unavailable**, with no legal way to satisfy both rules. That
  conflict no longer exists.
  **The surviving half is the mild one.** `pin` is still reserved
  language-wide and still spent: no genre may ever define it, for any
  meaning. `EDGE-GEOMETRY-CONSTRUCTS` called that the mild case when it filed it, and closing
  the entry does not retract the reservation — it only shrinks it from
  three spellings to one.
  **The exposure as filed, kept as the record of what was foreseen (a
  deleted rejection is a trap).** *Filed as an
  exposure, not as an action.* `LAYOUT-ZONE-NAMESPACE` (§1) made every layout-zone keyword
  genre-independent, which means its spellings are spent: no genre may ever
  define `pin`, `path` or `routing` as its own keyword, for any meaning.
  `pin` is the mild case — few figure kinds want the word for anything else.
  The other two are not mild, and the reason is what FigDown IS.
  **FigDown is a general-purpose figures-as-text standard; it aims to cover
  ALL figures.** The corpus that exercises it today happens to be network and
  protocol figures, but that is a fact about the corpus, not about the
  language's scope, and nothing in this section should be read as scoping the
  project to one domain. **A general-purpose standard has just reserved a
  general-purpose word.** `path` is wanted by filesystem and directory trees,
  by state machines, by flow and decision diagrams, by geographic and route
  figures, by call graphs — by whole classes of figure this language has not
  been asked to draw yet. `routing` carries the same exposure one step
  behind, and is additionally recorded in `vocabulary-sources.tsv` as a
  loaded domain word kept with the reasoning on file (`EDGE-LINE-SHAPE-KEYWORD`).
  `UNSAFE-DEFAULT-ELIMINATION`'s retirement of `route` already turned on a version of
  this argument.
  **Measured exposure (2026-08-07).** Every `path`/`routing` directive line
  that exists: **13** in three in-repo `.fd` files
  (`examples/showcase/tcp-state-machine.fd`,
  `examples/reference/experimental/block-experimental.fd`,
  `examples/statechart/dhcp-client.fd`), plus the conformance fixtures that
  pin the constructs — 15 fixture files, 33 directive lines — and
  **zero** lines in downstream production adoption. Two consequences follow,
  and both belong in the record rather than in a decision today:
  1. **Today is the cheapest point in this project's life to change these
     spellings.** Nothing downstream depends on them, no compatibility
     promise covers them (they are EXPERIMENTAL, §10), and no migration entry
     is owed. **The cost begins rising the moment the freeze is published**
     and downstream adoption starts.
  2. **Renaming is near-free; DROPPING them is not.** The three in-repo
     figures use rigid edge geometry for state-machine transitions, so
     removing the constructs would change their rendered output. Any future
     proposal to drop rather than rename must budget for that.
  **Why this is filed now rather than settled now.** Settling it would mean
  designing the edge-geometry vocabulary ahead of the evidence, which is the
  `NEW-CONSTRUCT-EVIDENCE-GATE` gate's own objection to premature vocabulary. The maintainer's decision
  for 0.1 is that `path` and `routing` **stay** in the layout
  namespace as genre-independent EXPERIMENTAL members; the rename question is
  **deliberately deferred to before the freeze is published**, and this entry
  exists so that the decision can be made then without re-deriving any of it.
  **Trigger for revisiting — whichever comes first:** (a) any genre proposal
  that wants `path` or `routing` as its own keyword, (b) the design of the
  edge-geometry / annotation family (`ANNOTATION-LOCATOR-SPLIT` is its neighbour), or (c) freeze
  publication, which is the last moment the change is cheap. The choices then
  are three, and all three are decisions rather than defaults: rename the
  layout-zone member, grant the proposing genre a different spelling, or
  accept the reservation knowingly.
  *(End of the exposure as filed.)* **What actually happened:** trigger (c)
  arrived first and a fourth choice presented itself that the entry had not
  enumerated — remove rather than rename or keep. The drop budget point 2
  demanded was then **measured** rather than assumed, and it came in smaller
  than the entry expected: removal *improved* two of the three affected
  figures on the project's own layout metric and left the third unchanged
  (numbers in `EDGE-IDENTITY-AND-GEOMETRY`). The requirement the two constructs served is not
  closed with them; it is carried forward as **`EDGE-IDENTITY-AND-GEOMETRY`**, immediately below.
- `EDGE-IDENTITY-AND-GEOMETRY`: **the language cannot ask for an elbow, and has no way to name
  the edge it would ask about** (2026-08-07, `EDGE-GEOMETRY-CONSTRUCTS`). **This is a
  requirement on file, not a note that something was deleted.** FigDown
  has no construct for **orthogonal (elbow) edge routing**, and none for
  **controlling it per edge**. Both were expressible until 0.1 —
  `routing orthogonal|straight` figure-wide and `routing=` on a `path` — <!-- fence-check: skip -->
  and `EDGE-GEOMETRY-CONSTRUCTS` withdrew the pair with their host directive. The need did not
  go with them.
  **The shape of the answer is already known, and a future design does
  NOT need to re-run the survey.** A source-graded prior-art study of
  Visio (VSDX / the MS-VSDX XSD), draw.io / mxGraph, Graphviz and ELK —
  **216 graded claims, 77% quoted from primary sources** —
  found a **narrow stable intersection**, and routing sits inside it:
  **exactly two modes** (`orthogonal`, `straight` — "these two names, and
  only these two, appear in recognisable form in all four") and **exactly
  two scopes** (a figure-level default and a per-edge override; 3 of 4,
  Graphviz having only the graph level). Everything past those two names
  is *not* shared — four systems, four disjoint value sets, differing by
  about 6× in cardinality and disagreeing on whether the set is open or
  closed — so a design that adds a third mode is designing without prior
  art, and a design that stops at two is not. Full derivation:
  decisions/registry.md
  §8.
  **The blocker is edge identity, and it is why this is an open question
  rather than a pending re-add.** A per-edge override has to *address* an
  edge, and FigDown has **no edge-identity construct**. The mechanism it
  used instead — **restating the edge to refer to it**, `path a -> b` <!-- fence-check: skip -->
  matching the edge as written — is the one identity option that
  **cannot grow**: it cannot address parallel edges (which the language
  had to make a line error for the reference alone), it cannot carry more
  than one attachment per edge, and it leaves a future annotation or
  locator grammar (**`ANNOTATION-LOCATOR-SPLIT`**) with nothing to point at. The prior art
  rules on this directly: **Graphviz uses the same restate-to-refer
  mechanism and makes it sound only by forbidding the case that breaks
  it** — its `strict` graph, which "forbids the creation of multi-edges"
  **graph-wide**. That is a language-wide commitment FigDown has never
  made and never scoped; it declared parallel edges out of scope for one
  directive's reference instead. Edge identity is *inside* the stable
  intersection and *absent* from FigDown, which makes it the first thing
  to design, not the last: parallel edges, more than one attachment per
  edge, sub-element endpoints, per-edge annotation and `ANNOTATION-LOCATOR-SPLIT`'s locators
  are all downstream of it.
  **What is deliberately given up, said as a loss and not as a
  simplification.** The **per-edge routing scope was INSIDE the
  intersection** and is surrendered anyway, because it had no host line
  left to ride on once `path` went. It is to be restored *properly*, once
  edges are addressable — not bolted back onto a restated triple. Its
  measured cost today is **one working line**: of the 8 in-repo
  `routing=orthogonal` writings, **6 are provable no-ops** — including the
  line in the reference figure whose own comment advertised it as the
  demonstration of per-edge override — and only one of the 8 is a line
  that did work.
  **Measured baseline, recorded so a future "is it worth adding back?"
  has a control group.** At withdrawal: **13** directive lines across
  **3** in-repo `.fd` files; **33** directive lines across **15**
  conformance fixture files; **zero** lines of downstream production
  adoption; and **6 of 8** `routing=orthogonal` writings provable no-ops.
  A behaviour audit found **four defects, every one of them in a part
  nobody exercised** — including a **published artifact whose labels are
  clipped**, because canvas growth on the waypoint path was one-sided.
  **Render quality, measured on the project's own linter**
  (`tools/layout-lint.js`, lower is better): removing both constructs took
  `examples/statechart/dhcp-client` from **23 to 8** — the bare strip scores
  **6**, but that is the version whose transition labels print over the node
  boxes, which the linter does not measure, so the figure was opened out and
  the number was checked against the picture rather than instead of it — and
  `examples/reference/experimental/block-experimental` from **4 to 2**;
  `examples/showcase/tcp-state-machine` scored **2 either way** and was
  re-authored with content-zone means (`rank`, `flow`, declaration order,
  `pin`) after a rendered visual comparison. So the constructs were not
  paying for themselves on the figures that used them.
  **What is NOT in the intersection, so a future design does not spend
  effort re-deciding it.** (a) **Author waypoints** — modelled by only
  **2 of the 4** systems (mxGraph `points`, ELK `bendPoints`), and those
  two disagree on the behaviour that matters: when an endpoint moves,
  mxGraph **leaves the waypoints behind** and ELK **carries them**. Visio
  has no waypoint concept at all. A disagreement about what happens when
  a node moves is a disagreement about semantics, not spelling. (b) **The
  dock realisation FigDown shipped** — a fraction on the EDGE is
  **mxGraph-only**; **written-order attachment has zero prior art in any
  surveyed system** (mxGraph and Graphviz both attach by semantic role);
  and draw.io's own documentation states that a dock fraction denotes a
  **location** — `(0.5,0.5)` is the centre — while ray-through-centre
  projection is a **separately flagged mode**. FigDown collapsed two
  attested axes into one key and then attached by written order. The
  *concept* the keys named — attachment to a **named site declared on the
  node**, addressed by **semantic role** (tail/head), denoting a
  **location** — is unanimous across all four and is inside the
  intersection; only this realisation of it was outside.
  **Scope note, because it is easy to misread this entry.** FigDown is a
  general-purpose figures-as-text standard and aims to cover ALL figures.
  Elbow routing is wanted by flowcharts, state machines, block and rack
  diagrams, org charts, circuit and piping schematics — by whole classes
  of figure, not by any one subject domain. Nothing here scopes the
  requirement to the corpus that happened to exercise it.
  **Production evidence, 2026-08-20:** a downstream figure hit this blocker
  from the other side — a `bundle` cannot name the three parallel links it
  aggregates, because an endpoint pair stops identifying anything at N > 1.
  **Reopen trigger — either one suffices:** (a) an **edge-identity
  construct** is designed, which unblocks the per-edge scope and `ANNOTATION-LOCATOR-SPLIT`
  together; or (b) the **edge-geometry family** is designed as a family
  (`ANNOTATION-LOCATOR-SPLIT` is its neighbour). Until one of those, adding a routing mode
  back would be designing the second storey first. All v0.2.
- `INDEX-KEY-NAMESPACE-CONTENTION`: **`index=` is spent in `bitfield`'s namespace, and the per-row
  gutter may not reuse it** (2026-08-08, `BITFIELD-REPETITION-CONSTRUCT`). **Filed under SYNTAX-STYLE
  RULE 4.7, as 4.7's own text requires**: 4.7 is a tie-breaker, not a veto,
  and where it loses it converts to an open question filed against the
  choice. It lost here to RULE 4.1 — `index` is single-sourced and verified
  in the IEEE 1685-2022 schemas (see
  [genres/bitfield.md](genres/bitfield.md)), and 4.7 does not outrank an
  attested spelling. What the choice costs: `ROW-INDEX-GUTTER`'s gutter, which this
  document named `index=` from its filing until 0.1, must be spelled
  something else, and no genre may give `index` a second meaning while it is
  live (RULE 4.3). **The cost looks smaller than it did**, and the reason is
  in `ROW-INDEX-GUTTER` above: `ANNOTATION-FAMILY-SEQUENCING`'s gutter evidence is 100% ADDRESSES, and `address` is
  registered nowhere, so the evidence argues for
  `address=` independently of what `index` is doing. This entry does not rule
  on that — it records that the gutter must now be named on its own evidence
  rather than by reaching for the obvious word. Closes when the v0.2
  annotation session names the gutter. All v0.2.
- `INDEX-RANGE-STEP`: **`index=` has no STEP, and the reason it was deferred is supply,
  not demand** (2026-08-08, `BITFIELD-REPETITION-CONSTRUCT`). `index=0..7` walks the integers one at a
  time; there is no way to say "every other element".
  **Measured: 0 of 9 stepped element indices** across the corpus, verified by
  a targeted second pass, with **one false positive corrected** — a 16-byte
  element spanning four word-rows is not a step. Descending needs no step
  either: `first > last` carries it (`index=53..0`).
  **The caveat is filed WITH the entry, because a bare count here would be
  misleading.** The corpus **has never been taught the construct** — `index=`
  did not exist when it was written — and that is `STROKE-KEY-STATUS`'s failure shape
  exactly: `stroke=` was demoted on a count of 5 taken from a corpus that had
  been taught not to use it, and the cause was recorded as **supply, not
  demand**. A zero measured against a vocabulary nobody had is weak evidence
  and is recorded as such.
  **Reopen triggers, both chosen so that they do not require a user to ask
  for something they cannot know exists:** (a) re-measure after `index=` has
  been taught for one full release; or (b) any single figure whose element
  index skips values.
  **The extension is verified and reserved, so a future session does not
  re-derive it.** `index="0..7 step 2"` — the Kotlin/Ruby inclusive-range
  family's own spelling — needs **no new key**, and it rests
  on the determinacy rule of §12.7: determinacy is decided by parsing both
  ends, **never by quoting**, so a quoted value carrying a `step` clause is
  free of the KEY question. Were quoting ever made semantic on this key, that
  half would stop being free — which is why that rule is normative rather than
  an implementation detail. All v0.2.
  **CORRECTED (`RULE-POSITION-ENUMERATION`): this entry said "no new key and no
  MIGRATION", and the second half was false.** `parseIndexRange` accepted any
  non-empty string as a prose `<last>`, so `field "S" 8 index="0..7 step 2"`
  was **already a legal document** — model `{first:0, last:"7 step 2"}`,
  the drawn index `[7 step 2]`. `step` was therefore not an unclaimed slot but
  an OCCUPIED one, and shipping the step meaning later would have changed what
  an existing legal document MEANS. Worse, no engine could have separated
  "prose that happens to read like a step clause" from "the author meant the
  extension" — the `color=` failure shape (`COLOUR-KEY-STATUS`), which this project treats as
  its worst.
  **The reservation, not the measurement, is what makes an extension free.**
  0.1 makes a bare lowercase `step` token in a prose `<last>` a line
  error, exactly as `;` was reserved (`SEMICOLON-STATUS`) and for the same
  reason. The trigger, its limits, and the two migration rewrites are in
  MIGRATIONS 0.1. This entry's own measurement — 0 of 9 corpus figures
  with a stepped index — is what made the reservation cost nothing today, and
  that is the general lesson now on the record: **an extension is free only
  when the syntax carrying it is RESERVED, not merely UNUSED.** A future OQ
  that files a spelling for a later release owes a reservation in the same
  release, or it is filing a migration it has not priced.
- ~~`ERROR-RECOVERY-MODEL`: **error RECOVERY is unspecified, and it changes the error
  set**~~ — **CLOSED 2026-08-22**, filed 2026-08-09.
  **The subject that closes is THE SPECIFICATION, and no engine byte moved.**
  §8.3 is now the recovery model: *a line that reports an error performs none
  of its state effects, and no other line's parse changes because of it*, with
  the stage table that says how many errors each stage may report, the five
  state effects a failing line skips, the single exception (the compact
  `field` list), the four cascade shapes as SPECIFIED behaviour, the region
  open/close rule, and the precedence rules for a multi-fault line.
  **The key fact this entry did not see is that recovery was already FROZEN.**
  The `.errors.txt` goldens compare complete sorted error LISTS, so every
  multi-error fixture in `conformance/` had been pinning the engine's recovery
  since the day it landed. What was missing was never the obligation — it was
  the RULE that predicts the list. That is why closing this needed no
  migration: the resolution order this entry wrote for itself (model, then
  precedence, then fixtures) assumed the model would MOVE goldens, and it does
  not, because the model states what the goldens already say.
  **Both recorded inconsistencies dissolve into one stage rule, and the
  residue of the second is specified rather than repaired.** (a) is not an
  inconsistency at all: `numbering=zzz` is one of exactly two TOLERATED
  faults — checks the engine performs after the region is built — while
  `class=` on a `bitfield` is an OPTION-stage fault, and the option stage runs
  before the directive ever sees the line. §8.3.5 states both halves and
  enumerates the tolerated pair. (b) survives as a genuine asymmetry and is
  now PREDICTABLE rather than repaired: the uniqueness check runs first on
  `title`/`flow`/`layout`/`width`/`duplicate id`, and `pin` is the one
  construct whose value checks run ahead of it (§8.3.6). Repairing it would
  move goldens — the MIGRATIONS-entry change this entry correctly identified —
  so it is filed as **engine-backlog item 79** instead, with a third
  asymmetry the survey found and this entry never named: the layout-zone gate
  is applied to a connector line ahead of its option pass and to every other
  directive after it.
  **The fixture obligation is met, not deferred.**
  `920-errors-recovery-model` pins five recovery clauses that had no fixture
  of their own — the reference cascade, the failed `layout` that opens no zone
  and spends no slot, the tolerated fault that still opens a region beside a
  pre-dispatch refusal that leaves one open, and both directions of (b).
  **What is left is smaller than this entry and is NOT recovery.** §8.3.8
  names it: when two directive-stage faults compete on one line and neither is
  a uniqueness check, which is reported is unstated. It cannot change an error
  COUNT, and the message-text gap it sits beside is §8.2's, which this closure
  does not touch.
  **The question as filed, kept whole — a deleted rejection is a trap, and the
  paragraphs below are what the model above was derived from.** *Filed
  2026-08-09:* §8 says errors are reported in one pass and a
  document with errors renders nothing. It does not say what happens to the
  *rest* of a document after a line fails, and the answer is observable:
  a failed line abandons its declaration, so a later reference to it raises
  a **second, cascaded** error. An implementation that recovers by keeping
  the failed declaration reports one error where the reference engine
  reports two — on inputs whose fixtures are about something else entirely.
  **Two inconsistencies are recorded so a future design does not have to
  rediscover them.** (a) A bad option on a typed-block opener sometimes
  opens the block and sometimes does not (`numbering=zzz` does,
  `class=` on a `bitfield` does not, and the second therefore fails every
  child line as well). (b) Duplicate-versus-bad-value precedence resolves
  in **opposite directions** on two constructs that are otherwise siblings:
  `width` reports `duplicate width` and swallows the value error, `pin`
  reports the value error and never fires `duplicate pin`. Neither
  asymmetry has a stated reason; both are frozen in goldens.
  **What resolving this needs**, in order: a recovery model in §8 (abandon
  vs. keep, and whether a cascaded error is suppressed), then a precedence
  rule (first-error-wins or duplicate-wins, one of them, everywhere), then
  fixtures that make each an obligation rather than an observation. Doing
  it in that order matters, because changing recovery **changes existing
  goldens** and is therefore a MIGRATIONS-entry change under §13, not a
  bug fix. §8.3 states the position for v0.1: the goldens pin the
  reference engine's recovery, and this is the one place the suite is not
  reachable from the normative documents by reasoning alone. v0.2.
  *(That last sentence is the one the closure above retires: the suite is
  reachable, and the premise that a recovery model must move goldens was
  wrong in the direction that made this cheap.)*
- `COLSPAN-EMPTY-CELL-SPELLING`: **the table colspan is spelled by an INVISIBLE character, and `TABLE-ROW-SYNTAX`
  already argued against exactly this** (2026-08-09). The rule is
  that a pipe-row cell whose RAW segment is empty — zero characters between
  the two `|` — is a colspan-left. `| A || B |` is a spanned header;
  `| A |  | B |` (whitespace between the pipes) is three ordinary cells. The
  encoding is INJECTIVE and both readings are pinned as goldens, so nothing is
  ambiguous. Three things follow, and all three are the entry:
  **(a) It was documented nowhere normative.** `genres/table.md` said only
  "`||` colspan-left", which reads as a token an author types; the actual
  rule — an empty RAW segment — appeared only in `vocabulary-sources.tsv` and
  in a passing clause of a MIGRATIONS rewrite rule. The word "empty" did not
  appear in `table.md` at all, and §12.3 enumerated every place absence
  carries meaning **without listing table cells**. Closed: the
  rule is now stated in §12.3 and in `genres/table.md`, where an author meets
  it.
  **(b) The ABNF cannot express it.** `cell-content = *( … )` admits the empty
  production, so the grammar cannot distinguish the colspan from an empty
  cell; the distinction lives in prose and in the goldens only.
  **(c) A FORMATTER DESTROYS IT.** Padding `||` → `| |` turns a two-tier
  header with two colspans into four independent single-column headers: no
  error, a different model, a different figure. That is a **`RENDERING-DETERMINISM` stability
  break through an ordinary tooling pass**.
  **And the project made this argument against itself.** `TABLE-ROW-SYNTAX` — the decision
  that ADOPTED `||`/`^^` — rejected whitespace-as-alignment *in the same
  paragraph*, on the grounds that *"formatters like Prettier pad cells
  arbitrarily"* and that *"invisible characters carrying semantics is a
  classic failure mode (Makefile tabs) and a hallucination source for LLMs"*.
  Both arguments apply verbatim to the rule `TABLE-ROW-SYNTAX` adopted. Before 0.1
  this was acknowledged in **no** spec, design or conformance document and in
  none of `INDENTED-BLOCK-SUGAR`..S40.
  **This is filed, not fixed, and the reason is stated rather than implied.**
  Changing the colspan spelling is a language change in a FROZEN genre, and
  every alternative costs something: an explicit token (`>` or `<`, multimd's
  own alternative) spends a mark language-wide (RULE 4.5's price clause); a
  `span=` option on a `cell` line moves the span out of the row it describes
  and breaks the "the pipe row IS the table" property; forbidding
  whitespace-only cells makes the two spellings one and loses the ordinary
  empty cell. **What would resolve it**, in order: measure whether any
  real-world Markdown formatter in the corpus's tool chain actually rewrites
  FigDown fences (the hazard is real but its FREQUENCY is unmeasured); then,
  if it does, price the three alternatives above against a v0.2 migration.
  **A cheap partial mitigation is proposed and deliberately NOT landed:** a
  lint that reports a pipe row containing a whitespace-only cell in a
  document that also contains a `||` colspan, since that combination is the
  signature of a half-padded row. It is a diagnostic, not a language change,
  and it belongs in `tools/`, not in the engine — filed in
  `decisions/registry.md`. v0.2.
- `MULTI-CELL-SINGLE-MARK`: **several INDEPENDENT cells cannot be marked as one thing**
  (2026-08-11, `BLANK-CELL-MEANING`). `class=` attaches to one `cell` and to one `field`, so
  four adjacent cells that share a class draw **four separate rings**, not one
  frame around the four. A MERGED region does not have this problem — it is one
  cell, and under `SHARED-CELL-BOUNDARY` the engine draws its mark as one ring around the whole region — so the
  gap is precisely: the language can say "these squares are ONE CELL" and can
  say "this cell means X", and cannot say "these several cells, still
  themselves, are one thing". The workaround is to merge them, which asserts a
  structural fact that may be false, or to accept the repeated ring, which
  reads as N marks rather than one. **This is a language gap, not a rendering
  defect**, and it is filed on that basis and deliberately not designed:
  nothing in the corpus has yet asked for it, and a construct for "these
  several things are one thing" is exactly the kind that gets invented once and
  spelled three ways. **It is the same shape as `BITFIELD-REPETITION-CONSTRUCT`'s open half** (a
  `bitfield` cannot say that a run of fields is one repeating RECORD) and as
  **`CONTIGUOUS-RANGE-GROUPING`** (curly-brace grouping over a contiguous range); whoever designs
  one should be shown all three, because one construct plausibly answers them
  and three separate ones would be the drift this project keeps paying for.
  **What would reopen it:** a measured figure in the production corpus that
  needs the outer frame and cannot honestly merge. v0.2.
- `GLYPH-WIDTH-GROUND-TRUTH`: **glyph advance widths are checked only against the engine
  itself** (2026-08-11, `TEXT-ADVANCE-MEASUREMENT`). `gate:shape` verifies that emitted geometry is
  consistent with the character widths the engine used, but it takes `cw`
  **from** the engine under test, so it asserts self-consistency, not truth
  about glyphs. A wrong advance width — the CJK glyphs that 0.1 drew outside
  their box — is therefore invisible to every gate and stayed green through
  every release until it was caught by eye (MIGRATIONS 0.1). The open
  question is how a gate could pin an advance width against **ground truth**
  (an independent font metric, or a rendered reference) rather than against
  the engine's own table. Filed on that basis and deliberately not built: the
  corpus has measured no second instance, and a metrics oracle is a large
  dependency to add for one defect. **What would reopen it:** a second
  glyph-width defect that ships, or a downstream figure whose correctness
  turns on an advance width the engine gets wrong. v0.2.
- ~~`MESSAGE-ORDER-AND-STATE`: **the language cannot state message ORDER or per-participant
  LIFELINE STATE, and the two corpus figures that need both are drawn in
  scene genres that read them WRONG**~~ — **CLOSED
  (2026-08-17)**, filed 2026-08-16 under `SEQUENCE-GENRE-VOCABULARY`.
  **The subject that closes is THE LANGUAGE.** The `sequence` genre landed (`SEQUENCE-GENRE-VOCABULARY`) and the ladder drew. Both semantic
  impossibilities this entry names are expressible in FigDown today: a
  message's place in the ladder's row order **is** its place in time, so
  relative order is stated in syntax and not parked in a label ordinal, and a
  `state` occurrence sits on its own lifeline between the two messages it
  falls between, so *this participant was in state S at this point in the
  exchange* has a construct. The closing condition this entry wrote for
  itself — **a genre landing that brings a ladder layout path with it**, the
  cost `statechart` did not have and the one objection no scope ruling had
  dissolved — is the condition that was met, layout path included.
  **Neither killing condition occurred, and both are now moot.** The census
  `other` bucket was not re-classified, and the reader-misreading rate was
  not re-measured; nothing showed the 14 to be mostly misfiled, and nothing
  demonstrated that readers of `tcp-handshake.fd` recover the order and the
  states correctly today. A killing condition retires a question that is
  still being asked. This one was answered instead, so neither trigger has
  anything left to fire at.
  **The two evidence figures stay exactly as they are, and that is
  DELIBERATE.** `examples/showcase/tcp-handshake.fd` and
  `examples/showcase/arp-resolution.fd` remain `figdown 0.1 topology` plus a
  companion `table`, with their ordinal labels and their HONEST LIMIT
  comments intact. The `BITFIELD-REPETITION-CONSTRUCT` condition described below still holds **inside
  those two artifacts** — a reading agent asked how many links join the
  client and the server still answers **3** there — but it now holds as a
  **corpus and portability decision, not a language gap**: both are
  `figdown 0.1` documents that any released engine can render, and moving
  them to the new genre would spend that. The reasoning lives where the
  figures are shown, not in this register.
  **The residue is a portable SURFACE, and it is not this question's.**
  `sequence` is EXPERIMENTAL and requires `figdown 0.4`, so *which surface a
  portable figure may use* is still moving — but that is a **status**, and a
  status is tracked by
  [the genre's own status line](genres/experimental/sequence.md), which is
  where a status changes, not by an open syntax question, which is where a
  missing construct is recorded. The four post-landing findings are elsewhere
  for the same reason — they are engine and gate defects, not gaps in the
  language: the operand-aware restatement question, `in=` on a `lifeline`
  drawing a full-height column, class membership emitted nowhere in the SVG,
  and `sequence` figures left unmeasured by `gate:stability` are filed as
  **items 35–39 of
  engine-backlog.md** (with the one claim that
  running it disproved), not as reopenings of this entry.
  **THE REDIRECT SURVIVES THE CLOSURE — this is still the number, not
  `FLOWCHART-GENRE-DESIGN`.** `FLOWCHART-GENRE-DESIGN` is flowchart genre design, and the sequence-genre
  citations that had been miscited to it point HERE, to this entry, closed or
  open. Closing a question does not vacate its number: a citation corrected
  to `MESSAGE-ORDER-AND-STATE` stays correct, and this is still the entry that answers what the
  sequence genre was filed as.
  **The question as filed, kept whole — a deleted rejection is a trap, and
  the paragraphs below are the evidence the genre was built from.** *Filed
  2026-08-16 under `SEQUENCE-GENRE-VOCABULARY`:* This is the
  `sequence` genre question, filed under its own number at last. Relative
  order between two edges is not expressible by any composition of existing
  constructs, and neither is *this participant was in state S at this point in
  the exchange*. The in-repo evidence is two files:
  `examples/showcase/tcp-handshake.fd` and
  `examples/showcase/arp-resolution.fd`, both `topology` plus a companion
  `table`, both carrying their own HONEST LIMIT comment, and both parking the
  order in `1:`/`2:`/`3:` label ordinals — which `MEANING-RECOVERY-SOURCE` treats as naming, not as
  semantics. **The failure is not "unstated"; it is wrong.** Under §12.7 an
  `edge` is a relationship, so a reading agent asked how many links join the
  client and the server answers **3** where the truth is one association
  carrying three segments in time: a confidently wrong number with nothing
  attached to warn the reader, which is the `BITFIELD-REPETITION-CONSTRUCT` condition. The same corpus
  carries the tie case — `arp-resolution.fd` has two edges both prefixed `1:`
  and the ordinal convention cannot say they are simultaneous. No existing
  genre closes it: `flowchart` has order but no participants, `statechart`
  states *no transition order* and is about one machine's modes, a `timing`
  lane carries values over time with no construct for *lane A sends to lane
  B*, and a `table` step column is prose under §12.7. **Measured demand:** 14
  of 2,177 classified production images (**0.6%**) from 8 distinct source
  documents, against `flowchart` 219 (10.1%), `topology` 95 (4.4%) and the
  `state` bucket 25 (1.1%); in-repo, 2 of 59 non-fixture `.fd` files. So `NEW-CONSTRUCT-EVIDENCE-GATE`'s
  WHETHER is met on semantic impossibility while `GENRE-EARNING-THRESHOLD`'s WHEN puts it **last** —
  the lowest measured demand of any genre candidate this project has weighed.
  The design record is
  sequence-genre-draft.md; the vocabulary
  is settled by `SEQUENCE-GENRE-VOCABULARY` (source standard, total order, `operand`, `lost=`,
  `gap`, `group`, `in=`) and **whether and when it lands is not**. **What
  would close it:** a genre landing that brings a ladder layout path with it —
  the cost `statechart` did not have, and the one objection no scope ruling
  has dissolved. **What would kill it:** a re-classification of the census
  `other` bucket showing the 14 are mostly misclassified, or a measured
  demonstration that readers of `tcp-handshake.fd` recover the order and the
  states correctly today. **This is the number, not `FLOWCHART-GENRE-DESIGN`** — `FLOWCHART-GENRE-DESIGN` is
  flowchart genre design and had been miscited for the sequence genre across
  the corpus; those citations point here. v0.2.
  *(End of the question as filed.)*
- `TIMING-NAMED-INSTANT`: **the `timing` genre cannot state that an INSTANT matters**
  (2026-08-17, from the maintainer's review of
  `examples/patterns/timing-b.fd`). That figure draws four labelled data
  segments — A, B, C, D — on a `data` lane whose segment boundaries coincide
  with the edges of `valid` on the lane above it. Reading the figure **is**
  reading that coincidence, and the datasheet convention for showing it is a
  vertical guide drawn through every lane at the boundary, so the eye reads
  down a column instead of travelling along one row and back. FigDown's whole
  timing surface is `signal` (a lane string), `gap` (a time break) and `data=`
  (the labels for a lane's `=` cells). **None of the three names a point on the
  time axis.** So the figure cannot say *this boundary is the one that
  matters*, and a reader is left to infer the alignment from the fact that two
  lanes happen to be drawn under one another — which is exactly the inference
  `MEANING-RECOVERY-SOURCE` says a figure may not leave to a convention. The genre's vocabulary is
  single-sourced from **WaveDrom**, which is a tool and not a standard;
  WaveDrom's own spelling for this is `node`/`edge` — anchor characters placed
  in the lane string, plus a relation drawn between two anchors — so prior art
  exists for the SPELLING, and none of it is a source word under RULE 4.1.
  **The `PRESENTATION-AS-MEANING-CARRIER` boundary is the whole of the design constraint.** A NAMED event —
  *this instant is T1* — is meaning: it is a fact about the subject, it
  survives a strip of the layout zone (`GUI-WRITEBACK-STRUCTURE`), and a reading agent can quote it.
  A bare vertical rule is presentation: it draws the alignment without
  asserting it, and under §5 and `PRESENTATION-AS-MEANING-CARRIER` a reader is entitled to discard
  presentation entirely. Any landing must therefore put the NAME in the
  language and let the ENGINE derive the rule. A construct that only draws a
  line would spend a keyword and leave the figure saying precisely what it says
  today, which is nothing.
  **Nearest neighbour, and not the same question: `TIMING-MEASUREMENT-ANNOTATIONS`.** That entry is a
  named timing PARAMETER spanning TWO signal events (setup, hold, cycle),
  rendered with dimension arrows and edge-alignment guides. This one has arity
  one: a single instant, named, referenced by every lane at once, with nothing
  measured between two of anything. The two share one prerequisite — the
  signal-event locator, designed and deliberately not built under the
  **`ANNOTATION-LOCATOR-SPLIT`** disposition (the annotation family is two families: annotations,
  *a body attached to a target*, and the locator grammar for naming a
  sub-element AS a target). If that grammar lands, a named instant may fall out
  of it as the degenerate one-endpoint case rather than earn a construct of its
  own, and that is the likeliest disposition of this entry — filed here so the
  case is counted when the locator session runs, not so it is designed twice.
  **Measured demand, and it is honestly partial.** In-repo: 1 of 3 `timing`
  documents (`timing-b`; `timing-a` is a clocked handshake that reads by cycle
  count, and `examples/reference/experimental/timing.fd` is a vocabulary
  demonstrator, not a subject figure), so 1 of 57 non-fixture `.fd` files. In
  production, the census counts **142 of 2,177 classified images (6.5%; 7.2%
  weighted)** as timing-waveform — but that is the GENRE's demand, and this
  construct's has never been counted at all.
  **What would close it:** (a) that count — over the corpus and over those 142
  production figures, how many can only be READ by aligning two or more lanes
  at one instant. `NEW-CONSTRUCT-EVIDENCE-GATE`'s WHETHER, measured rather than asserted, is what turns a
  convention into a construct. And (b) a source word under RULE 4.1: a survey
  of what the IEEE timing-diagram conventions, or the Z.120-family timing
  notations, actually call such a marker. **No such source has been read for
  this entry, and nothing here asserts what any of them contains.** They are
  named as places to look. A claim about one of them owes
  [standards-claims.tsv](standards-claims.tsv) a row before it is written, and
  a source this repository cannot reach is recorded there as `unread` rather
  than left unsaid.
  **What would kill it:** the demand measuring as label prose being sufficient.
  The figures that need an instant today carry the timestamp in a lane label or
  in the host document's text, and if the count shows that no reading is lost
  that way, the construct is convenience — and `GENRE-EARNING-THRESHOLD`'s WHEN then puts it behind
  everything that was measured. v0.2.
- `MESSAGE-OCCURRENCE-GRANULARITY`: **a `sequence` message is ONE occurrence in this genre's model, so
  its ARRIVAL has no place in the order** (2026-08-19, from a review of the
  three-way-handshake ladder). FigDown's `message` line denotes the whole
  communication; **OMG UML 2.5.1 §17.4.3.1 gives a Message two occurrences**, a
  sending and a receiving one (`standards-claims.tsv` S175, verified). The
  simplification is not new and it is not undeclared — `vocabulary-sources.tsv`
  records it on the `message` row as a **DECLARED SIMPLIFICATION**, and
  sequence-genre-draft.md §34 row 8 excluded
  it with UML §17.2.3.3 named. It is filed as a question now because two
  consequences of it have been named, and one of them is not a lost nicety.
  **(a) Crossing messages force the model to assert an order the subject does
  not state.** When both ends are active and two packets are in flight at once,
  neither send precedes the other in the protocol — but under `SEQUENCE-ORDER-MODEL` the
  declaration order of `message` lines **is** the time order of every occurrence
  in the figure, so the document must pick one and the picked one is a fact the
  source does not carry. `SEQUENCE-ORDER-MODEL` took the total order deliberately, with its cost
  named (`par` becomes load-bearing); this entry records that the cost lands
  hardest exactly where a message's two ends are separated in time, which is the
  case the model cannot see.
  **(b) The causal event is the ARRIVAL, and it is the one with no position.** A
  participant changes state because a message **reached** it, not because one was
  sent to it. `state` occurrences sit between whole messages, so *"A changed
  state between its send and B's receipt"* is unsayable — the sentence the draft
  already recorded, here restated as what it costs a reader rather than as what
  the genre declined to borrow.
  **First evidence.** A three-way-handshake ladder whose **source drawing encodes
  propagation with diagonal arrows**: the slope is the separation between send
  and receive, drawn, and it is the one thing the transcription cannot keep
  (2026-08-19 review). The genre draft's own sub-domain for this row is the same
  document family — delayed segments and simultaneous open.
  **A candidate direction is RECORDED, NOT PROPOSED**, so that a later session
  starts from it instead of re-deriving it, and so that the two rulings it would
  have to survive are checked now rather than after someone has written the
  syntax: **message ids, plus an arrival occurrence taking its own place in
  declaration order.**
  - **It keeps the total-order axiom rather than weakening it.** One message
    would contribute **two** occurrences and both would sit in the one
    declaration order. `SEQUENCE-ORDER-MODEL`'s rule is not relaxed — it is applied to a finer set
    of events — so the reason `SEQUENCE-ORDER-MODEL` gave for the rule (a source admitting several
    equally legal drawings is a source this language will not accept) is
    untouched.
  - **It does not refute `SEQUENCE-TIME-GAP`.** An arrival denotes **ORDER, not QUANTITY**: it
    says *this is where the message landed among the other events*, and it says
    nothing about how much time the drawing's vertical distance stands for.
    `SEQUENCE-TIME-GAP`'s reopen condition — the genre landing a construct that makes vertical
    position denote a quantity — is therefore **not met by this**, and `gap`
    stays refused on exactly the ground it was refused on.
  **It depends on message identity, and that dependency is the whole of why this
  is an open question and not a proposal.** An arrival has to name the message it
  is the arrival of, and this language has no construct that names a message. So
  this entry waits on the identity model, and behind it on the same
  blocker **`EDGE-IDENTITY-AND-GEOMETRY`** records for edges — one reference mechanism, designed once,
  or three inventions.
  **Census status: PENDING, and stated plainly rather than left as an
  omission.** The frequency of crossing-message and arrival-caused-state figures
  has **not been counted**, because the classified image population is not
  currently reachable from this repository. Nothing in this entry asserts a rate,
  and the draft's `1 sub-domain` is a sample observation, not a census. **What
  would close it:** that count, plus a spelling that survives the two checks
  above. All v0.2.
- `MULTICAST-MESSAGE-DELIVERY`: **`sequence` cannot say ONE SEND, MANY RECEIVERS, and the workaround
  states two messages where the fact is one** (2026-08-20, from downstream
  production authoring).
  A station transmits once and every station on the medium receives it. The fact
  is **one transmission with a receiver set**; reaching several lifelines is a
  property of the one event, not a count of events. A `message` has one tail and
  one head, and the language has no spelling for a set on either end.
  **The workaround is a synthetic medium lifeline, and its cost is that the model
  OVERSTATES ITS SOURCE.** An extra `lifeline` stands for the medium, with one
  message in and one message out. The picture is right; the model now carries
  **two point-to-point messages in a stated total order**, so a reading agent
  asked which came first will answer — confidently — about an ordering the
  protocol never asserted. The medium is also a `lifeline`, so it counts as a
  participant in the exchange, and it is not one. This is the `BITFIELD-REPETITION-CONSTRUCT` condition:
  a wrong reading with nothing attached to warn the reader.
  **This is the first PRODUCTION evidence against a measured exclusion.**
  sequence-genre-draft.md §34 row 5 excluded
  fan-out on **1 of 27 RFC figures, 0 of 14 corpus, 1 of 14 breadth**, with the
  interim recorded as N messages plus a `class` naming them one broadcast. That evidence
  moves the corpus half of that count off zero and does it **twice, in two
  unrelated figures of one review batch** — which is what makes this an entry
  rather than a note on the draft.
  **A CONDITION is written into this entry, and it binds whoever spells this.**
  Any spelling must first pass the **RULE 4.1 single-source check, as `SEQUENCE-SOURCE-STANDARD`
  applied it to this genre**. Three things are known and the third is not:
  (a) this genre's ground is OMG UML 2.5.1 clause 17, whole, by `SEQUENCE-SOURCE-STANDARD`;
  (b) the genre's own vocabulary study found **no first-class broadcast Message
  there, and none in Mermaid either**, which is why the row was an exclusion and
  not a coverage gap; (c) **ITU-T Z.120's model has not been read for this
  question, and nothing here asserts what it contains** — a claim about it owes
  [standards-claims.tsv](standards-claims.tsv) a row before it is written, and a
  source this repository cannot reach is recorded there as `unread` rather than
  left unsaid. **If neither source carries the construct, there are exactly two
  honest outcomes**: land it as a **DECLARED divergence** from this genre's
  ground, in the `SEQUENCE-ORDER-MODEL` manner and with the divergence written into the genre
  document; or **refuse it** and keep the interim. A third — spelling it and
  calling it a borrow — is the `VOCABULARY-SOURCE-ATTRIBUTION` failure this project has already repaired
  once.
  **Census status: PENDING**, for the reason recorded at `MESSAGE-OCCURRENCE-GRANULARITY`: the classified
  image population is not currently reachable, so the production frequency of
  fan-out figures is uncounted and no rate is asserted here. **What would close
  it:** that count, and a source verdict under the condition above. All v0.2.
- **A presence condition cannot reference a field** — `present="C = 1"`
  names `C`, and the language cannot resolve that to the `field "C"` three
  lines up: a bitfield field name is a **label**, not an id, and `class=` /
  `in=` are the only reference-shaped keys FigDown has. This is the same
  **locator** gap already filed as **`ANNOTATION-LOCATOR-SPLIT`**; it is cross-referenced here
  rather than duplicated. It is the reason `present=`'s value is deliberately
  opaque prose with no expression grammar (§12.7, `PRESENCE-CONDITION-EXPRESSION`).
- *Candidate recorded (2026-07-27)*: **class line-width extension** —
  one downstream evidence figure distinguishes active/standby links by
  stroke thickness; `class` `fill=`+`style=` approximates the semantics
  today but cannot express thickness. Candidate addition: stroke width as
  an additional `class` presentation vocabulary item (`stroke-width=`).
  Low priority; no corpus frequency measured.
- *Candidate recorded (2026-07-29, needs audit)*: **multiplicity /
  replication count on a scene node** ("×3 identical workers") — today
  a replication count lives in label prose, so a reading agent counts one
  participant and cannot distinguish "one instance" from "N identical
  instances". Prior art exists nearby: relation cardinality in ERD notations
  is already surveyed (`ERD-EXPRESSION-LEVEL`), but node-level replication is a different
  construct — cardinality labels an edge, replication counts a node
  itself. Frequency evidence needed before `NEW-CONSTRUCT-EVIDENCE-GATE` gate.
- *Candidate recorded (2026-07-29, needs audit)*: **machine-readable units
  and scale for numeric values** — today a unit is a substring of a column
  header or label text; a machine reader must parse free text to extract it.
  Maintainer's own skepticism recorded honestly: FigDown is a figure
  standard, not a data schema, and label text is a legitimate meaning carrier
  under `MEANING-RECOVERY-SOURCE`. This candidate needs strong frequency evidence before it clears
  `NEW-CONSTRUCT-EVIDENCE-GATE` — it must demonstrate that unit ambiguity causes actual semantic loss
  in real figures, not merely that structured units would be convenient.
- *Sequence-genre sharpening note (2026-07-29, for `GENRE-EARNING-THRESHOLD` §6 sequence
  candidate)*: beyond the ergonomics of a ladder syntax, the 2026-07-29
  needs audit surfaced a deeper semantic point. In the generic scene model
  there is NO construct asserting relative order between edges: today's
  practice numbers the labels ("1: …", "2: …"), which is a naming
  convention, and `MEANING-RECOVERY-SOURCE` disqualifies convention-in-prose as a meaning carrier.
  A sequence genre must therefore define message ORDER as first-class
  semantics — not merely provide a familiar spelling. Note also that the
  reserved dynamic keywords (`page`/`set`/`pulse`) model scene deltas, not a
  message time axis: they record what changes between states, not that
  message A precedes message B in a single scene. They do not close this
  gap. This sharpens the design requirement for the v0.2 sequence genre:
  temporal/causal ordering between messages must be stated in syntax, not
  inferred from label numbering.
- *Stroke style as a multi-axis semantic channel — candidate scoped-and-
  doubted (2026-07-29, engine-verified, cross-reference `PRESENTATION-AS-MEANING-CARRIER`)*: dashed-versus-
  solid is one of the most universal conventions in engineering figures, and
  authors reach for `style=dashed` believing they have stated something. By
  §5 and `PRESENTATION-AS-MEANING-CARRIER` they have not — presentation may render meaning but never carry
  it alone, so a reading agent is entitled to discard it. That makes bare
  `style=dashed` a foot-gun: meaning is silently lost at authoring time.
  Engine-verified facts: `field "K" 4 present=""` parses — the bitfield genre
  already has conditional presence as a FIRST-CLASS SEMANTIC construct, with a
  dashed border as its rendering (and a derived caption
  when a condition is stated); `node a "A" present=""` and <!-- fence-check: skip -->
  `edge a -> b present=""` are both line errors — the scene model has no <!-- fence-check: skip -->
  equivalent key.
  Engine-verified sanctioned expression today: `class standby "Present only
  during failover" style=dashed` plus `class=standby` on the node or edge —
  the class label carries the condition, dashed is its presentation, and a
  legend derives automatically (§2.7). Authors and agents MUST be told
  plainly that bare `style=dashed` asserts nothing.
  **Multi-axis analysis (2026-07-29 sharpening)**: stroke style is ONE visual
  channel carrying SEVERAL INDEPENDENT semantic axes: (a) presence /
  conditionality — always present vs conditional; (b) realization —
  physical vs logical/virtual (the defining distinction of overlay-network
  figures, where an overlay tunnel is drawn dashed over its physical
  underlay); (c) attachment — real relation vs annotation leader;
  (d) status — actual vs planned/prospective. These axes are INDEPENDENT and
  can conflict: two conventional figures may use dashed for opposite meanings
  and both be correct in their own domain. Therefore NO fixed
  stroke-to-meaning mapping can ever be right, and the standard must never
  bake one in. **Candidate direction revised**: `class` is not a transitional
  workaround — it is the PERMANENT general answer, because only the author
  can say which axis is in play and what the value means on that axis.
  Generalizing `present=` to scene elements would name just ONE axis and
  risks teaching authors that dashed means conditional universally — the
  opposite of the lesson. Record as scoped-and-doubted: if a scene-level
  `present=` is ever introduced it must be explicitly limited to the presence
  axis, must not be treated as "the dashed flag", and must coexist with
  `class`-declared meanings on the other axes. Maintainer's current leaning:
  keep the language smaller and let `class` carry all axes.
  **First-party evidence (this session, described generically)**: the
  project's own examples were found to carry seven dashed edges with zero
  `class` declarations — the physical-versus-logical distinction that is the
  whole point of an overlay figure was riding on stroke style alone. Same
  failure family as the arrangement-in-geometry violation recorded in `MEANINGFUL-ARRANGEMENT`/
  `AUTHOR-INTENT-AUDIT`: meaning parked in presentation. The examples are being fixed to
  declare classes. The pattern is a recurring authoring failure mode worth a
  future lint heuristic: an element carrying `style=` or `fill=` with no
  `class=` MAY be meaning-in-presentation (a heuristic, since decorative
  styling is also legitimate).
- *Audit confirmations (2026-07-29)*: the 2026-07-29 needs audit (see `AUTHOR-INTENT-AUDIT`
  in requirements-notes.md) verified the following as cleanly expressible
  today with no semantic gap: containment, category membership, hierarchy,
  adjacency-without-link, boundaries, ownership zones, ordered steps, loops,
  error categories, pipelines, event→action, precedence DAGs, bit-level
  layouts, structs, lookup tables, conditional/variable-length fields with the
  `BITFIELD-CONDITIONAL-OFFSETS` branch rule, dual bit-numbering conventions, proportions, thresholds,
  fill levels, timing lanes, and legends. These are settled; they do not
  require further evidence gathering.

## 10. Keyword registry, conformance modes, extensions

**Keyword registry (v0.1).** 22 top-level keywords, 6 typed-block child
keywords, plus the table-row line-start token `|`. (22 until 0.1,
when the three `flowchart` role keywords landed — the first genre-owned set
under `GENRE-VOCABULARY-OBLIGATION`, taking the count to 25; 0.1 removed one again by merging
`size` into `pin`, `ELEMENT-GEOMETRY-DIRECTIVE`; and 0.1 removed two by WITHDRAWING `path`
and `routing`, `EDGE-GEOMETRY-CONSTRUCTS` — 24 − 2 = 22, back to the 0.1 figure by a
different route.) **Every figure in that history read one lower until 0.1**, because `chart` (c′) had no registry entry at all — a
keyword the engine has accepted at the top level of every scene genre and
of `table`, and which the `GENRE-KEYWORD-ALLOWLIST` paragraph below already
named. A closed language's registry is obliged to list what the language
accepts, experimental entries included; the totals here are now
re-derived from the engine's per-genre allowlists rather than carried
forward. Since `GENRE-NAMESPACE` the registry
is partitioned by NAMESPACE (§1, `GENRE-NAMESPACE`/`UNIVERSAL-CORE-KEYWORDS`/`LAYOUT-ZONE-NAMESPACE`): the **universal core** belongs
to every genre and no genre may redefine it; the **layout namespace** is
owned by no genre either (`LAYOUT-ZONE-NAMESPACE`); everything else belongs to a
genre. Since `CONSTRUCT-STATUS-TIERS` every entry also carries a **STATUS**.

**Status — the two values (`CONSTRUCT-STATUS-TIERS`).**

| Status | What it means |
|---|---|
| **NORMATIVE** | Inside the v0.1 **conformance surface** and inside the **compatibility promise**. A conforming implementation MUST support it; it changes only through a migration entry (`VERSION-MIGRATION-MODEL`). |
| **EXPERIMENTAL** | The reference engine accepts it and documents using it keep working — but it is **outside the conformance surface** and **outside the compatibility promise**. An implementation MAY support it. It may change or be withdrawn in a later `0.x` without a migration entry, and a document that uses it is not a portable v0.1 document. |

EXPERIMENTAL is not a deprecation and not a warning about correctness; it
is a statement that the construct has **not converged**. The engine has no
warning channel — an experimental construct parses silently and renders —
so **this marking is the only signal an author or an authoring agent
gets**. It is documentary by design (`CONSTRUCT-STATUS-TIERS`): a status column a reader can
consult, not a diagnostic the parser emits. An agent generating a
portable figure SHOULD restrict itself to the NORMATIVE surface.

*(a) The universal core (3) — `UNIVERSAL-CORE-KEYWORDS`, never redefined by any genre. All
NORMATIVE:*

| Keyword | Status | Role |
|---|---|---|
| `figdown` | NORMATIVE | the header; must be readable before the genre is known |
| `title` | NORMATIVE | the document's caption |
| `layout` | NORMATIVE | opens the layout zone (§3) |

These three are the document's **structure**, which is what a reader can
resolve before the genre is known. `layout` belongs here rather than with
the zone's own members in (a′) because it is the zone **opener** — the
marker that says where the zone begins — not a directive inside it.

**What "core" means (`UNIVERSAL-CORE-KEYWORDS`, restated).** Core means: *wherever
this keyword appears, its meaning is fixed, and no genre may redefine it.*
It does **NOT** mean "must appear in, or be usable by, every genre". The
earlier reading made `UNIVERSAL-CORE-KEYWORDS` contradict the per-genre minimum sets, because
`bitfield` and `table` documents have no `pin` and no `layout`
zone at all and are complete without them. `UNIVERSAL-CORE-KEYWORDS` is a **fixity** guarantee,
not a **ubiquity** requirement. The 0.1 repartition changes only
the ENUMERATION — five keywords to three — and leaves that distinction
exactly as it stands, because it is what keeps `UNIVERSAL-CORE-KEYWORDS` consistent with the
per-genre minimum sets.

*(a′) The layout namespace (1) — `LAYOUT-ZONE-NAMESPACE`, owned by no genre:*

| Keyword | Status | Role |
|---|---|---|
| `pin` | NORMATIVE | an element's declared geometry: `at=` places it, `width=`/`height=` extend it (§3) |

**THIS TABLE IS THE NORMATIVE ENUMERATION OF THE LAYOUT NAMESPACE
(`GENRE-NAMESPACE`).** §3's default — a reading agent ignores every member
of the layout namespace, wherever in the document it appears — is
actionable only if a reader can ENUMERATE the namespace, so this table is
the list a reading agent applies, not a summary of one. It currently has
exactly one row, `pin`, which is what a reader may rely on until a
migration entry says otherwise; a keyword absent from this table is not a
member, and `layout` itself is the zone's opener under (a) rather than a
member. `tools/namespace-check.js` asserts that this table and the reference
engine's accepted set (`LAYOUT_DIRECTIVES`) name the same keywords, and
fails loudly when either moves.

**Every member of this namespace is genre-independent** (§1, `LAYOUT-ZONE-NAMESPACE`): no genre may define, redefine or extend a keyword inside the
layout zone, and `GENRE-VOCABULARY-OBLIGATION` does not reach into it. **The
namespace has exactly one member.** `path` and `routing` were the other
two — core until 0.1, EXPERIMENTAL from `CONSTRUCT-STATUS-TIERS`'s demotion, and
**WITHDRAWN from the language by `EDGE-GEOMETRY-CONSTRUCTS`** (retired-keyword
table below; the requirement they served is §9 **`EDGE-IDENTITY-AND-GEOMETRY`**). The clause
that governs the namespace is unchanged; only its membership is.
**Status and belonging were orthogonal while both lived**, and the
distinction is kept here because it governs the next experimental member
the zone acquires: EXPERIMENTAL is a statement about **stability**,
genre-independence one about **belonging**, and a construct can be both.
Until 0.1 this section said the opposite of the second: *"a future
genre MAY redefine `path` or `routing` under `GENRE-VOCABULARY-OBLIGATION`, and an agent may no
longer assume their meaning is genre-independent."* That sentence was
**WITHDRAWN** (`LAYOUT-ZONE-NAMESPACE`). It read a demotion in status as a release of
ownership, and it put a crack in `GENRE-NAMESPACE`'s default that a reading agent
ignores every member of this namespace — a default that holds only if no
genre semantics can ever appear in it, and which (since `GENRE-NAMESPACE` restated it
over membership rather than over the zone's textual extent) is applied by
recognising the members, so one list must be correct under every genre. `CONSTRUCT-STATUS-TIERS`'s
demotion of `path`/`routing` stood until `EDGE-GEOMETRY-CONSTRUCTS` removed them outright.
**A withdrawal DOES release the spelling**, which a demotion never did:
`path` and `routing` belong to no namespace now, so a future genre may
claim either under `GENRE-VOCABULARY-OBLIGATION` through the `NEW-CONSTRUCT-EVIDENCE-GATE` gate (§9, `EDGE-GEOMETRY-CONSTRUCTS`, closed).
`pin` absorbed `size` (`ELEMENT-GEOMETRY-DIRECTIVE`): one directive, one model
object, and `size` is a retired spelling with a named diagnostic.

*(b) The scene BUCKET (10) — a count of distinct spellings, and NOT a
namespace (`SUBJECT-VOCABULARY-SCOPE`).* Every one of these is declared separately
by each scene genre that accepts it, so a spelling on four allowlists is
four declarations, not one shared word (§1, `GENRE-VOCABULARY-OBLIGATION`). The registry enumerates
spellings because a closed language has to say what exists; the column
that says WHO declares each is the genre documents, and nothing may read
this table as evidence that a word is shared.

| Status | Keywords | Count | Declared by |
|---|---|---:|---|
| NORMATIVE | `node` | 1 | `block`, `topology`, `flowchart` (`statechart` spells it `state`) |
| NORMATIVE | `edge` | 1 | `block`, `topology` (`flowchart` spells it `flowline`, `statechart` `transition`) |
| NORMATIVE | `group` | 1 | `block`, `topology` |
| NORMATIVE | `external` | 1 | `block`, `topology`, `flowchart` |
| NORMATIVE | `class` `flow` `rank` | 3 | all four — and NOT subject vocabulary: styling declaration and layout intent, so no genre's domain holds a competing meaning (§1) |
| EXPERIMENTAL | `threshold` `band` | 2 | `block` |
| EXPERIMENTAL | `bundle` | 1 | `topology` |

`plane` was the eleventh member and is **WITHDRAWN from the language**
(`PAINT-ORDER-CONSTRUCT`, §2.4); its registry row survives below as a withdrawn
diagnostic, and it is not counted, exactly like `path` and `routing`.
The 16 keyword/genre cells `SCENE-KEYWORD-MEMBERSHIP` withdrew are visible in this table as
absences — `group` under `flowchart` and `statechart`, `external` under
`statechart`, `threshold`/`band` outside `block`, `bundle` outside
`topology` — and each has a named per-genre diagnostic that gives the
ground rather than a spellcheck. `statechart` declares **no** subject
vocabulary at all, and the empty set is its declaration.

*(b″) `flowchart`'s OWN namespace (3) — the first exercise of `GENRE-VOCABULARY-OBLIGATION`
(`FLOWCHART-ROLE-KEYWORDS`):*

| Status | Keywords | Count |
|---|---|---:|
| EXPERIMENTAL | `process` `decision` `terminator` | 3 |

These three are legal **only** under `figdown 0.1 flowchart`; under any
other genre they are `"<keyword>" is not allowed in genre <g>`, from the
same allowlist that rejects `node` inside `bitfield`. They are `node` with
a `role` field, they share the node id namespace, and they take exactly
`node`'s option keys. Their status is their genre's. See
[genres/experimental/flowchart.md](genres/experimental/flowchart.md) §Roles for the vocabulary, the
derived geometry, the exclusion list and the `terminator` verification
debt.

*(b‴) The per-genre NODE and CONNECTOR spellings (5) — `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`,
0.2; two more:*

| Status | Keyword | Genre | Replaces |
|---|---|---|---|
| EXPERIMENTAL | `flowline` | `flowchart` only, and `figdown 0.2` only (`KEYWORD-RENAME-SCOPE`) | `edge` |
| EXPERIMENTAL | `transition` | `statechart` only | `edge` |
| EXPERIMENTAL | `state` | `statechart` only | `node` |
| EXPERIMENTAL | `message` | `sequence` only, and `figdown 0.4` only | `edge` |
| EXPERIMENTAL | `lifeline` | `sequence` only, and `figdown 0.4` only | `node` |

`message` and `lifeline` need no version gate of their own, unlike
`flowline`: the GENRE requires `figdown 0.4`, so neither spelling is
reachable from an earlier document and neither replaced a spelling any
document was written with. `state` is now declared by **two** genres and
they are **two declarations, not one shared word** (§1, `GENRE-VOCABULARY-OBLIGATION`) — under
`statechart` slot 1 DECLARES an id and the directive is `node` renamed;
under `sequence` slot 1 REFERENCES a lifeline and the directive is a state
occurrence with no id of its own. It is the first spelling in the language
whose two declarations differ in GRAMMAR and not only in reading, which is
why the engine carries a genre-conditional option row for it and why this
table lists it once with the genre column doing the work.

Each has `edge`'s or `node`'s grammar, option keys and model exactly, and
REPLACES it in that genre rather than joining it. **This is (b) read
correctly**: the four scene genres never shared one connector, they
declared four connectors that happened to agree until `GENRE-CONNECTOR-SPELLING` — which is why
the split was expressible at all, and why `SUBJECT-VOCABULARY-SCOPE` later had to say so in §1.
The version gate on `flowline` is the cost of having authored the rename
against a shared surface: under `figdown 0.1 flowchart` the connector is
still `edge`, because the 0.1 language cannot lose a spelling
(MIGRATIONS 0.2). This row was missing from the
partition sum below until 0.3, so every keyword total in this
project was three low for five releases.

*(b⁗) `sequence`'s OWN subject vocabulary (2) — 0.4, `SEQUENCE-GENRE-VOCABULARY`:*

| Status | Keywords | Count | Declared by |
|---|---|---:|---|
| EXPERIMENTAL | `fragment` `operand` | 2 | `sequence` only |

Both name referents OMG UML 2.5.1 clause 17 defines — `CombinedFragment`
(§17.12.3) and `InteractionOperand` (§17.12.14) — so they are subject
vocabulary in (b)'s sense and not node/connector spellings. **`sequence`
declares a THIRD subject word, `state`** (`StateInvariant`, §17.12.25),
and it is not in this table because it is already counted once in (b‴) as
`statechart`'s node spelling. Two genres declaring one spelling is two
declarations of ONE entry in this registry, never two entries — the
enumeration is of spellings the language has, and the column that says who
declares each is the genre documents.

**Three spellings are REFUSED by this genre and the refusals are part of
the registry**, because a closed language has to say what does not exist as
well as what does: `gap` (the vertical axis is non-proportional, so the
line would assert nothing — `SEQUENCE-TIME-GAP`), `group` (no lifeline-grouping construct
in clause 17, and `band` = membership is already locked into the scene
genres — `SEQUENCE-PARTICIPANT-GROUPING`) and the option key `lost=` (the model it wanted is ITU-T
Z.120 §4.3's under UML's spelling — `UNDELIVERED-MESSAGE-MARKING`). A refusal is NOT a withdrawal:
the genre never declared any of the three, so no document loses a line and
none of them appears in the withdrawal tables below. `flow` and `rank` are
absent too, as a consequence rather than a ruling — both of this genre's
axes are declaration-ordered, so a key that reordered a drawing would make
it disagree with its source (`DECLARATION-ORDER-SEMANTICS`).

*(b′) `bundle` and `plane` were NORMATIVE until 0.1 and were demoted by
`CONSTRUCT-STATUS-TIERS`,* on the same evidence the genre statuses rest on. Measured over the
50-document in-repo corpus, `bundle` appears in **4** documents and
`plane` in **3**, and **every one of the seven is a `topology`
document**. Both appear **zero** times in `block`, `bitfield` and `table`
documents — so under the per-genre minimum-set reading neither is in any
normative genre's vocabulary, and `topology` is itself EXPERIMENTAL.
`bundle` is `topology`'s own domain vocabulary (`DOMAIN-VOCABULARY-PREFERENCE` §4) and is demoted
*with* its genre, not against it; `plane` was a generic marker with no
owning genre, exactly the position that demoted `threshold` and `band`.

**That same measurement is what settled both of them.**
`bundle`'s zero uses under `block` are the evidence `SCENE-KEYWORD-MEMBERSHIP` withdrew that
cell on, and its four uses under `topology` are the evidence `topology`
kept it on — one measurement, read per genre, answering both ways at
once, which is what per-genre declaration is for. `plane`'s three uses
were all `topology` too, and `PAINT-ORDER-CONSTRUCT` withdrew the keyword from the language
rather than reassign it, because a marker with no owning genre and no
referent in any domain has nothing to be reassigned TO: the two most
realistic figures that used it are byte-identical without it (§2.4).

**The option key moves with its declaration point, and it moved twice.**
`plane=` was demoted with the `plane` keyword and withdrawn
with it, on the same rule both times: `plane=` could only
name a plane some `plane` line declared, so with no `plane` line legal
anywhere it has nothing left to name. `z-index=` follows too, being
accepted by `plane` alone (the same rule that carried `extend=` out with
`band`). What does **not** move is the implicit `base` plane or the
model's `planes` array: every document still has
`planes[0] = {id:"base", z:0}` and every element still reports
`plane: "base"` (§12.5), so a normative-surface reader needs no new case,
and paint order is document order.

*(c) The three nested-genre namespaces (3 openers + 6 children + `|`).*
Each opener is a top-level line in ANY genre — that is composition (§4,
`GENRE-COMPOSITION`) — and each child keyword is valid only inside its own region. A
child's status is its genre's:

| Genre | Status | Opener (top level) | Children (region only) |
|---|---|---|---|
| `bitfield` | NORMATIVE | `bitfield` | `field` `break` |
| `table` | NORMATIVE | `table` | `\|` (row token) `cell` `width` |
| `timing` | EXPERIMENTAL | `timing` | `signal` `gap` |

*(c′) `chart` (1) — EXPERIMENTAL, and not a genre.* It opens no region and owns
no children, which is why it is its own row rather than a fourth line of
(c):

| Keyword | Status | Role |
|---|---|---|
| `chart` | EXPERIMENTAL | `chart <table-id>` — draws a `table` already in the document as a chart. Legal at the top level of `block`, `topology`, `flowchart` and `table` (`GENRE-KEYWORD-ALLOWLIST`, below). **Outside the v0.1 conformance surface**; defined in §4.4, vocabulary row in [genres/table.md](genres/table.md) |

Its status is its own, not a genre's: `chart` is accepted under three
NORMATIVE genres and is still EXPERIMENTAL, because what has not converged is
the construct — one option key, one legal value, and a data binding
(rows→X, columns→Y) that no other keyword in the language uses. An agent
writing a portable v0.1 figure leaves it alone.

3 + 1 + 10 + 3 + 5 + 2 + 3 + 1 = **28 top-level**; 2 + 2 + 2 = **6 children**
(the `|` token is a line-start token, not a keyword, and is counted
separately — which is why `table` shows three entries in the row above and
contributes two here). The fifth term is (b‴), the per-genre node and
connector spellings `flowline` `transition` `state` `message` `lifeline`,
and the sixth is (b⁗), `sequence`'s own subject vocabulary.
`CONSTRUCT-STATUS-TIERS` changed no total — entries moved column only; 0.1 is the first
release since that ADDS keywords, three of them, all EXPERIMENTAL, taking the
total to 25, and 0.1 is the first that REMOVES one: `ELEMENT-GEOMETRY-DIRECTIVE` merged
`size` into `pin`, so 25 − 1 = 24. **0.1 removes two more, and it
is the first removal that is a WITHDRAWAL rather than a merge or a
rename**: `EDGE-GEOMETRY-CONSTRUCTS` took `path` and `routing` out of the language, so
24 − 2 = 22. **0.2 adds three** — `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING` gave `flowchart`
`flowline` and `statechart` `state` and `transition`, so 22 + 3 = 25 —
and **0.3 removes one, the second WITHDRAWAL**: `PAINT-ORDER-CONSTRUCT` took `plane`
out of the language, so 25 − 1 = **24**. **0.4 adds four**:
`sequence` lands with FIVE keywords — `lifeline` `message` `state`
`fragment` `operand` — and `state` is not among the four, because that
spelling already exists in this registry as `statechart`'s node word.
`sequence` DECLARES it independently and the two declarations differ in
grammar, but the registry enumerates spellings, so 24 + 4 = **28**. This
is the first release in which one entry carries two genres' independent
declarations with two different grammars, and the count is stated here
rather than left to be recomputed.
`LAYOUT-ZONE-NAMESPACE`'s repartition, like `CONSTRUCT-STATUS-TIERS`'s,
moved entries between rows without changing any total. A retired or
withdrawn keyword is not counted here — its spelling stays registered so
it fires a named diagnostic (the tables below), but it is no longer a
keyword of the language. Of the 28
top-level keywords **13 are NORMATIVE** — the core of three (a), `pin` (a′), the
seven normative scene spellings (b), and the `bitfield` and `table` openers
(c) —
and **15 are EXPERIMENTAL**: `threshold` `band` `bundle`
`timing` `process` `decision` `terminator` `chart` `flowline` `transition`
`state` `lifeline` `message` `fragment` `operand`. (Every withdrawal so
far took EXPERIMENTAL spellings only, so NORMATIVE has
never moved; it is the EXPERIMENTAL side that carries every change, and it
carried the `chart` that was missing until 0.1, the `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`
trio that was missing until 0.3, and the whole `sequence`
vocabulary.)
Reserved for the dynamic profile: `page set pulse` — the three the §6 <!-- fence-check: skip -->
sketch uses. `step` was reserved until 0.1 and is now **released**
(`CONSTRUCT-STATUS-TIERS`): it appeared in no sketch and no genre claimed it, so it is an
ordinary unknown keyword again and an author may have the word.

**Genre status (`CONSTRUCT-STATUS-TIERS`), the v0.1 record.** The v0.1 NORMATIVE genre surface is
**`block`, `bitfield`, `table`**. **`topology`, `flowchart` and `timing` are
EXPERIMENTAL**: the header accepts them, their documents parse and render
exactly as before, and no `.fd` needs rewriting — but they sit outside the
conformance surface and outside the compatibility promise, and each genre
document says so at the top. The table below is that v0.1 snapshot,
preserved as history rather than updated in place — it is not a claim
about today's genre surface (see the current table immediately after it).

| Genre | Status |
|---|---|
| `block` `bitfield` `table` | NORMATIVE |
| `topology` `flowchart` `timing` | EXPERIMENTAL |

**Genre status, current (`GENRE-STATUS-RECORD`).** All eight genres the tree holds today,
each row sourced from that genre's own status line and, for the two added
after v0.1, from core §11's ABNF `genre` production:

| Genre | Status | Since | Minimum `figdown` |
|---|---|---|---|
| `block` | NORMATIVE | 0.1 (`CONSTRUCT-STATUS-TIERS`) | 0.1 |
| `bitfield` | NORMATIVE | 0.1 (`CONSTRUCT-STATUS-TIERS`) | 0.1 |
| `table` | NORMATIVE | 0.1 (`CONSTRUCT-STATUS-TIERS`) | 0.1 |
| `topology` | EXPERIMENTAL | 0.1 (`CONSTRUCT-STATUS-TIERS`) | 0.1 |
| `flowchart` | EXPERIMENTAL | 0.1 (`CONSTRUCT-STATUS-TIERS`) | 0.1 |
| `timing` | EXPERIMENTAL | 0.1 (`CONSTRUCT-STATUS-TIERS`) | 0.1 |
| `statechart` | EXPERIMENTAL | 0.2 (`STATECHART-GENRE-SCOPE`) | 0.2 |
| `sequence` | EXPERIMENTAL | 0.4 (`SEQUENCE-GENRE-VOCABULARY`) | 0.4 |

`gate:vocab` CHECK E cross-checks this current table, two-way, against core
§11's ABNF `genre` production — a genre present in one and missing from the
other FAILS the gate, so this table cannot go stale silently the way the
v0.1 table above did.

**The honest v0.1 state of `GENRE-NAMESPACE` (updated 0.1, `GENRE-KEYWORD-ALLOWLIST`; 0.1).** `GENRE-NAMESPACE` is
**enforced** as a per-section top-level **allowlist**. Pure `bitfield` /
`table` / `timing` reject scene keywords (`node`, `edge`, …) except `class`
(and experimental `chart` under `table`). The three scene genres still
share one scene vocabulary (plus nested typed openers and experimental
scene/layout keywords) — and `flowchart`'s allowlist is
the scene set **plus its own three role keywords**, which is what makes
`GENRE-VOCABULARY-OBLIGATION` real rather than merely permitted. Corpus measurement already matched this split: across pure
`bitfield`/`table`/`timing` documents, ten of the eleven scene keywords
appeared **zero** times; `class` is the exception and stays on those
allowlists. `GENRE-NAMESPACE` also exists so that a FUTURE genre may bring its own
namespace without a major version bump. The `GENRE-KEYWORD-ALLOWLIST` narrowing is documented in
`MIGRATIONS` 0.1; documents that mixed scene keywords under a pure
`bitfield`/`table`/`timing` header must rewrite (multi-section `MULTI-FIGURE-DOCUMENTS`, scene host
with nested region, or two files).

Keyword names follow one discipline: **one lowercase word, borrowed
standard terminology, scope-precise, mutually disambiguating** — no
invented abbreviations. The keyword and option-key namespaces are
disjoint, so a name may be reused across them without ambiguity: the
`table` child keyword `width` (a line, one value per table column) and the
`width=` option on `pin` (one node's px extent) are the SAME spelling
and that is correct rather than tolerated — they denote
the same concept, a horizontal extent, and a language that borrowed
`width` from SVG/CSS for one of them and invented an abbreviation for the
other would be the defect (`UNSAFE-DEFAULT-ELIMINATION`, single-source vocabulary). The parser can
never confuse them either — a keyword is only ever a line's first token,
an option key only ever the left side of a `key=`.
That the parser cannot confuse them does not mean a *reader* cannot:
`fill` was both a keyword and an option key, and `route` sat one typo
from the then-`routing` keyword that was also an option key on the same
line.
Both were renamed (`fill` → `band`, `route` → `path`,
and `line` → `guide`, which collided with the model's own `line` field;
`guide` was itself renamed `threshold`, `THRESHOLD-KEYWORD-SPELLING`). That whole
`route`/`path`/`routing` chain ends, where `EDGE-GEOMETRY-CONSTRUCTS` withdrew
`path` and `routing` from the language; all three spellings are now
line-start diagnostics with no live half anywhere.
Reuse across namespaces is permitted, but it must not cost the reader.

**One DECLARED EXCEPTION to the paragraph above (maintainer
ruling).** The word `fill` is once more in two
namespaces: it is a RETIRED KEYWORD, whose line-start diagnostic
`fill has been renamed: use band` this section still requires, and it is
the primary OPTION KEY, `fill=`. That is exactly the collision `UNSAFE-DEFAULT-ELIMINATION` exists
to prevent, and it is accepted knowingly rather than left unremarked. The
reasoning: the exposure being closed is larger than the collision being
opened. `color=` set the FILL while CSS, SVG and every diagram language
built on them use `color` for the TEXT colour — so an author, and this
project's primary author is an authoring agent carrying exactly those
priors, wrote `color=` meaning the label and got a legal, wrong figure,
silently, on any of thirteen directives. The collision it costs is
strictly milder than the one 0.1 removed: there, both spellings
were LIVE, and a reader meeting `fill` had to know which namespace was
in play. Here only one is live. `fill` at line start is *always* an
error carrying its own migration, `fill=` is *always* the option key, and
the two can never appear in a document that parses. The exception is
directional and does not generalise: a retired spelling MAY be reused in
the other namespace, a live one may not.

**A SECOND DECLARED EXCEPTION (maintainer ruling): `gap`.**
The rule as written has a live counterexample inside this very document
set, and leaving it unremarked would make the rule read as violated rather
than excepted. The `timing` child keyword `gap <cycle>` (a time break) and the
scene option `gap=<px>` on `group` (member spacing) are **both live**, and
they denote **different concepts** — the only such pair in v0.1. **It
stayed a pair**: `sequence` was proposed a third `gap`, a
discontinuity on its own time axis, and `SEQUENCE-TIME-GAP` REFUSED it — the axis there
is non-proportional, so the line would assert nothing. The exception
therefore has two arms and not three, which is the answer the ruling was
asked for. (The other
cross-namespace pairs — `class`/`class=`, `width`/`width=`
— denote the SAME concept on both sides and are covered
by the paragraph above, not by this exception. `routing`/`routing=` was a
third such pair until 0.1, when `EDGE-GEOMETRY-CONSTRUCTS` withdrew both halves at once,
and `plane`/`plane=` a fourth until 0.3, when `PAINT-ORDER-CONSTRUCT` did the same.
Both withdrawals took the pair whole, which is the coupling (b′) states:
neither half can outlive the other.)
The reasoning is `GENRE-KEYWORD-ALLOWLIST`, and it is a **stronger** disjointness than the
`fill`/`route` cases ever had. There the two spellings could appear in one
document and only the parser's token position told them apart. Here they are
**section-disjoint**: `gap <cycle>` is legal only inside a `timing` region, and
a pure `timing` section rejects `group` outright (`GENRE-KEYWORD-ALLOWLIST` allowlists, §1), so the
scene `gap=` has nothing to attach to; conversely a scene genre's `gap=`
lives on a `group` line, which a `timing` region does not admit. The two can
be co-present in one FILE only across a section or region boundary that the
reader has already had to cross, and never on the same line, in the same
region, or under the same allowlist. Directionality is preserved: this
exception rests on section disjointness, not on one side being retired, and
it does not license a third pair without the same argument.

**A THIRD ARM of the same exception (`REGISTERED-KEY-VALUE-GRAMMAR`): a genre MAY give a REGISTERED
OPTION KEY a second value grammar, on the same test.** The two arms above
are one spelling in two NAMESPACES. This one is one spelling inside ONE
namespace — the option-key namespace — carrying two disjoint value
grammars, which the paragraph below would otherwise read as *"a collision
INSIDE one namespace … remains a defect no genre boundary can excuse."*
It is permitted **only** where all four of the `gap` test's conditions
hold: the two registrations are **section-disjoint** (neither accepting
directive is legal under the other's genre), **both are live**, **each is
documented completely in its own genre document** under `GENRE-DOCUMENT-CONTRACT`, and this
registry's own row names **both** acceptors. A genre MUST NOT give a
second value grammar to a key whose existing acceptor is legal in the
same section, and MUST NOT do it silently: an undeclared second grammar
is the defect, not the second grammar.
The instance that forced the rule is `type=` — one registered key,
`bar3d` on `chart` and a mandatory twelve-member interaction-operator
enum on `sequence`'s `fragment`, the two enums sharing
no value. The language had a stated rule for a genre reusing a KEYWORD
(`GENRE-VOCABULARY-OBLIGATION`, §1) and no stated rule at all for this, which is why the case went
unrecorded in the release that created it, while the parallel KEYWORD
case in the same release (`state`, under `statechart` and under
`sequence`) got three paragraphs. **Neither half is renamed**: `chart`'s
spelling is the word Vega, Chart.js and ECharts use (`UNSAFE-DEFAULT-ELIMINATION`) and
`fragment`'s is the source standard's own attribute name, so both survive
RULE 4.1 and a rename would spend a borrowed spelling to buy tidiness.
The arm is nevertheless a permission and not an invitation: a second
value grammar costs every reader of this registry a lookup, and a genre
that can spell a key of its own SHOULD.
The same rule retired `render`: it read as a command, it
collided with the *renderer* and the render options of §7, and the zone
it opened admits only geometry — so it became `layout`, the cross-tool
word for that half of a diagram language. `layout` and `layer` were a
near-miss pair — five shared characters, one a bare keyword and the
other id-taking — and 0.1 removed it: the `layer` half is gone
(`PLANE-KEYWORD-SPELLING`), and `layout`/`plane` shared nothing; (`PAINT-ORDER-CONSTRUCT`)
there is no `plane` either, so the zone's opener has no near-miss left in
the language at all.
**`GENRE-VOCABULARY-OBLIGATION` does not waive that rule.** A genre MAY reuse another genre's
spelling with a different meaning (§1), but `UNSAFE-DEFAULT-ELIMINATION`'s test still applies: the
reuse must not cost the reader. Across genres the disambiguator is the
header — required, first significant line, exactly one genre per document —
which is what `line`, `fill` and `route` did not have. A collision INSIDE
one namespace, or with a field name of the canonical model (§12.5), remains
a defect no genre boundary can excuse.

**Retired keywords and positionals (the 0.1 batch, `TIMING-LANE-ALPHABET`–`CHART-BLOCK-NAMING`).** A
retired KEYWORD is registered exactly like a retired option key: the
spelling stays known to the parser so it produces a named migration
message at line start instead of the generic `unrecognized line`. Four
keywords, one positional flag and one enum value moved in one release,
each on the single-source-vocabulary test (`UNSAFE-DEFAULT-ELIMINATION`): the spelling FigDown
used was claimed by a standard for a different concept, or by three
standards for three concepts.

| Retired | Live spelling | Why it went |
|---|---|---|
| `boundary` | `external` | it declares an external I/O endpoint — this spec's own words (§2.8) — while the ECB analysis pattern's «boundary» is an INTERNAL interface object (CORRECTED 0.3.z: this said "UML's"; the stereotype is not in UML 2.5.1 or ISO/IEC 19505-2 — see decisions/registry.md), C4's `System_Boundary` is a dashed grouping container FigDown already spells `group`, and BPMN's Boundary Event is a third meaning. Three standards, three concepts, none of them this one. |
| `layer` (+ `layer=`) | `plane` (+ `plane=`) | in mxGraph — the geometry model FigDown adopted — a layer is a CONTAINMENT PARENT that establishes coordinates, so `layer=overlay` reads as "reparent and re-origin this element", which FigDown does not do; Inkscape layers are `<g>` and may carry a transform; OGC WMS layers each carry an SRS; CSS `@layer` is cascade priority with no visual meaning; SVG has no layer concept at all. `plane` is claimed by no standard for a conflicting meaning, and it also closes the `layout`/`layer` near-miss recorded above. |
| `wrap` (`bitfield` child) | `break` | in CSS and typography `wrap` is AUTOMATIC reflow — a MODE — while this directive is an EXPLICIT row break, an EVENT. CSS Fragmentation §4.3 calls that "a forced break … explicitly indicated by the style sheet author"; HTML spells it `br`. |
| ~~`optional` (bare flag on `field`)~~ | ~~`conditional`~~ → **`present=`** | RFC 2119 defines OPTIONAL as optional to IMPLEMENT, which is not what a wire-format field marker means; the wire-format sense is "present only if" (RFC 2784). **REVERSED at 0.1 (`PRESENCE-FLAG-SPELLING`): `optional` became the live spelling again and `conditional` the retired one** — `conditional` has zero attestation as a wire-format field marker, RFC 2784's own diagram spells the concept `(optional)`, and "optional" appears in 34 downstream field LABELS against 0 for "conditional". **BOTH are retired at 0.1 (`PRESENCE-CONDITION-EXPRESSION`): the construct is now the option key `present=`, whose VALUE is the presence condition.** The row is struck through rather than deleted because a document written at any point in that history still needs to find it. |
| `plot` (+ `kind=`) | `chart` (+ `type=`) | `plot` reads as an imperative — the reason `render` was retired at 0.1 — while every other block opener is a noun, and ECharts, Chart.js and Mermaid all name the object a chart. `kind=` was RETIRED on `node` and LIVE on `plot` at the same time, inside one namespace; 0.1 closed that, and Vega, Chart.js and ECharts spell the chart-type key `type`. |
| `bars3d` (the one `type=` value) | `bar3d` | value-level spelling only; the enum still has exactly one member. |

**0.1 adds one keyword rename and one deletion (`THRESHOLD-KEYWORD-SPELLING`, `CHART-LEVEL-KEY`).**

| Retired | Live spelling | Why it went |
|---|---|---|
| `guide` | `threshold` | an **inverted** name, which `UNSAFE-DEFAULT-ELIMINATION` rates worse than an unfamiliar one. In Illustrator, Inkscape, Figma and draw.io a *guide* is an author-only construction line that is **never rendered**; FigDown's is drawn output, and no counter-example was found where "guide" names rendered output. `guide` was also recorded `source = FigDown` — a **coinage**, which `SIZE-AND-DIRECTION-KEY-NAMING` makes a last resort requiring a justification that was never on file; the rename is the one chance to convert it into a borrow. `threshold` comes whole from **Grafana**, which supplies the entire family in one vocabulary: its **"Show thresholds"** render option offers *"As lines"*, *"As filled regions"* and *"As filled regions and lines"* — FigDown's marker + region pair, split the same way — with **IETF RED/AQM** as the secondary source (RFC 2309: *"Two RED parameters, minth (minimum threshold) and maxth (maximum threshold)"*; RFC 7567: *"an AQM algorithm configured with a threshold"*), which is exactly what the corpus's WRED figures transcribe. Two independent counts agree that **77.8%** of 126 marks and **78%** of 63 distinct marker names are thresholds; target/mean/reference marks: **0**; standalone watermarks: **0**. `level` was rejected on three collisions (75 downstream registers spell `_LEVEL`/`_LVL` meaning a profile *index*; the corpus's own threshold tables head the ordinal column `Level (high → low)` and the value column `Threshold`; ISO 80000-3 defines *level* as the **logarithm** of a quantity ratio), and `watermark` was rejected for covering no capacity bound, no floor, no base level and none of the WRED staircase marks — "threshold" outnumbers it 1355:42 in the corpus prose. FigDown already called it a threshold in every place it explained it. |
| `level=` (on `chart`) | *(none — DELETED)* | not a rename. Zero uses corpus-wide, zero 3-D bar charts, zero requests; one in-repo example and two fixtures. It was also the only construct in the language whose caption the **engine** wrote rather than the author, and its `parseFloat` grammar uniquely accepted `1e3`, breaking the otherwise-uniform `\d+(\.\d+)?` numeric grammar (SYNTAX-STYLE §8). |

`band` also changed shape (`BAND-LABEL-STATUS`) — a mandatory quoted label,
written first — but that is an addition to a directive, not a retired
spelling, so it is recorded in §5 and MIGRATIONS rather than here.

**0.1 retires one keyword, and it is the first MERGE rather than a
rename (`ELEMENT-GEOMETRY-DIRECTIVE`).**

| Retired | Live spelling | Why it went |
|---|---|---|
| `size` | `pin` (`width=`/`height=` on the `pin` line) | not a rename: the keys did not change spelling, their CARRIER did. One element's declared geometry was split across two directives that shared an id, a domain, a zone and a rigidity rule, so every author who pinned a node and then resized it wrote two lines and every consumer read two objects — and the model carried two arrays for one fact. `pin` won the spelling over `geometry` on attestation and on the one property this zone must convey, removability (RULE 4.1; `geometry` has zero rows in `vocabulary-sources.tsv` and no keyword-level attestation in any line-oriented diagram language). The borrow from DOT is downgraded EXACT → MODERATE in the same pass, because Graphviz's `pin` is position-only and FigDown's now also carries extent. |

Because it is a merge, the diagnostic names a whole line rather than a
word — an author holding `size a width=120 height=60` needs to be told <!-- fence-check: skip -->
that the keys survive unchanged and only their carrier moved. The domain
split arrived with it: `at=` takes nodes, groups and externals, while
`width=`/`height=` take nodes only (§3).

**0.1 removes two keywords, and it is the first WITHDRAWAL — not a
rename, not a merge, not a deletion of a dead corner (`EDGE-GEOMETRY-CONSTRUCTS`).** A
withdrawal takes the construct out of the language with **no replacement
spelling**, so the diagnostic cannot say "use X instead"; it must say what
went, why, and what to write instead in the constructs that remain. Both
were EXPERIMENTAL (`CONSTRUCT-STATUS-TIERS`) and outside the v0.1 conformance
surface and the compatibility promise, so no promise is broken and no
migration entitlement is owed — MIGRATIONS carries the entry all the same,
because a document written last week still has to be told.

| Withdrawn | Live spelling | Why it went |
|---|---|---|
| `path` (+ `points=` `tailport=` `headport=` `routing=`) | *(none — WITHDRAWN)* | a source-graded prior-art study of Visio, draw.io/mxGraph, Graphviz and ELK (216 graded claims, 77% quoted from primary sources) found the directive's two halves on **opposite sides** of a narrow stable intersection, and neither half survivable as written. **Author waypoints are outside it**: only 2 of the 4 systems model them, and those 2 disagree on what happens when an endpoint moves (mxGraph leaves them behind, ELK carries them, Visio has no waypoint concept at all) — a disagreement about behaviour, not spelling. **The dock CONCEPT is inside it** — a named site declared on the node, addressed by semantic role, denoting a location, unanimous across all four — but **FigDown's realisation was not**: a fraction on the EDGE is mxGraph-only, it collapsed two attested axes (location vs perimeter projection, which draw.io flags separately) into one key, and it attached by **written order**, which has zero prior art in any surveyed system. Restoring either half needs an **edge-identity construct** the language does not have (§9, `EDGE-IDENTITY-AND-GEOMETRY`). Measured footprint at withdrawal: 13 in-repo `.fd` directive lines in 3 files, 33 conformance directive lines in 15 fixture files, **zero** downstream adoption; four behaviour defects, all in parts nobody exercised, including a **published artifact with clipped labels**. |
| `routing` | *(none — WITHDRAWN)* | the **shape is right and the evidence is not**: two modes and two scopes are inside the stable intersection, so this is the one piece of the family prior art would have kept. It goes anyway, on demand and implementation — **6 of the 8 in-repo `routing=orthogonal` writings are provable no-ops**, downstream adoption is zero, and the per-edge scope (also inside the intersection) has no host line left once `path` goes and nothing to address even if it had one. **This is a deliberate loss, recorded as one** (§9, `EDGE-IDENTITY-AND-GEOMETRY`): it is to be restored properly once edges are addressable, not re-attached to a restated triple. |

The keyword spellings stay registered so a line-start `path` or `routing`
still produces a named message rather than the generic `unrecognized
line`, and `route` — retired to `path` — now points at a
spelling that is itself withdrawn, so its message says so. None of the
three is counted as a keyword of the language (the totals above).

Two more 0.1 retirements are recorded with the option keys below
(`unit=` → `word=`, `via=`/`src=`/`dst=` → the `points=`/`tailport=`/`headport=`
spellings that 0.1 then withdrew,
`labels=` → `data=`) and one with the timing lane alphabet (the digits `2`–`9`,
`TIMING-LANE-ALPHABET` — write `=` and name the cell in `data=`).

Experimental (outside the v0.1 conformance surface), the complete list in
its current spellings — keywords `threshold` `band` `bundle`
`timing` `chart` `flowline` `transition` `state`, option keys
`offset` `extend` `data`
`type`, genres `topology` `flowchart` `statechart` `timing`.
(`plane` and its keys `plane=` and `z-index=` were on this list until 0.3, when `PAINT-ORDER-CONSTRUCT` withdrew all three from the language.) (`stroke=` was on this list
until 0.1 and is now NORMATIVE, `STROKE-KEY-STATUS`; `color=` was on it too and is
now retired language-wide, `COLOUR-KEY-STATUS`; the keywords `path`/`routing` and the
option keys `points`/`tailport`/`headport`/`routing` were on it until 0.1 and are WITHDRAWN, `EDGE-GEOMETRY-CONSTRUCTS`.)
Each registered set (keywords, option keys, shape/style enums, edge
operators, numbering values, timing lane characters, merge markers) is
closed; additions follow the change policy (`NEW-CONSTRUCT-EVIDENCE-GATE` gate) and land as
migration entries.

**Reserved spellings in the option-key namespace (`LANE-ALPHABET-KEY-RESERVATION`, normative).** One
registration is forbidden outright, and the reason is a cross-namespace
collision the parser cannot detect. A `timing` `signal` line is lexed in
LANE MODE: a bare token containing `=` stays POSITIONAL there, so
`signal d x=01.` is a name and a lane, not a name and an option. But lane
mode is consulted *second* — a token whose key is REGISTERED is taken as
an option first, before the lane-mode guard is reached. The lane alphabet
is `[01pnx=.]`, which contains the letters `p`, `n` and `x` (the digits
`2`-`9` left it, `TIMING-LANE-ALPHABET` — they never were legal first characters
of a key, so the reservation is unchanged). Therefore:

> A single-letter option key drawn from the timing lane alphabet — today
> `p`, `n` and `x`, the only lane characters that are legal first
> characters of a key — MUST NOT be registered in the option-key
> namespace, in any genre, including a future one.

Registering one would not produce an error anywhere: every existing lane
containing that letter followed by `=` would silently reparse as an
option, and the figure would change. Nothing in the language expresses
this constraint, so it is enforced mechanically —
`node conformance/run.js` refuses to run while such a key is registered
(the `LANE-ALPHABET-KEY-RESERVATION` guard). The genre-namespace rule `GENRE-VOCABULARY-OBLIGATION` does not waive it: a genre
may redefine a spelling's MEANING, but it cannot re-lex another genre's
lanes. See [genres/experimental/timing.md](genres/experimental/timing.md).

**Option-key registry (v0.1).** 46 keys. 0.1 registered six new
spellings — `plane` `type` `data` `points` `tailport` `headport` — and kept
their predecessors `layer` `kind` `labels` `via` `src` `dst`, plus `unit`
(replaced by `word`), registered as language-wide retired diagnostics; the
0.1 pass had done the same for `w`/`h` → `width`/`height` and
`dir` → `extend`. **A registration counts here whether or not it has a live
acceptor**, which is the convention `w` `h` `via` `src` `dst` `level` have
always been counted under — so 0.1's four WITHDRAWALS
(`points` `tailport` `headport` `routing`, `EDGE-GEOMETRY-CONSTRUCTS`) move from EXPERIMENTAL to
NORMATIVE (diagnostic) and the total does not change, and 0.3's two
(`plane` `z-index`, `PAINT-ORDER-CONSTRUCT`) do the same. This section previously said 33: that figure predated
0.1 and had also omitted `offset`, `z-index` and `unit` from the
namespace table below, so it was never the registry's real size. **It then
said 42 in an earlier release, and that figure went stale**, which registered `description` (`DESCRIPTION-KEY-SPELLING`, `note` kept as a
diagnostic) and `present` (`PRESENCE-CONDITION-EXPRESSION`) without recounting: 42 + 2 = 44. **It said
44 until 0.1**, which registered `index` (`BITFIELD-REPETITION-CONSTRUCT`, the `bitfield`
repetition key): 44 + 1 = 45. The
figure above is re-derived row by row from the key table below, which has
46 rows for 45 distinct keys — the two NORMATIVE rows spelled `fill` are ONE key
with two registrations. The set
is closed: a `key=`
token whose key is not listed here is the line error
`unknown option "<key>="`. Applicability is enforced **per directive** —
a registered key on a directive that does not accept it is also a line
error (`<directive> does not take <key>=`), so the flat list alone is
not the whole rule. The right-hand column is the complete set of
directives that accept the key.

**Namespaces (`GENRE-NAMESPACE`).** An option key belongs to the namespace of the
directives that accept it. All 46 are classified; the fourth column of the
table below is the same classification key by key. The last row is the one
kind of registration that belongs to NO directive: a spelling retired from
the language altogether, kept registered only so its rename — or its **withdrawal** — gets a named message wherever it appears.
The **layout, experimental** row emptied and is deleted
rather than left at zero: `EDGE-GEOMETRY-CONSTRUCTS` withdrew all four of its keys with their
only host directive, and they appear in the last row now.

| Namespace | Keys | Count | Status |
|---|---|---:|---|
| **layout** (`LAYOUT-ZONE-NAMESPACE` — owned by no genre; 0.1) | `at` `width` `height` (all three on `pin`) | 3 | NORMATIVE |
| **scene** (`block`/`topology`/`flowchart`) | `shape` `in` `gap` + the retired diagnostics `label` `taillabel` `headlabel` `from` `to` | 8 | NORMATIVE |
| **scene, experimental** (on the demoted `band`/`threshold`, `CONSTRUCT-STATUS-TIERS`; both are `block`'s alone since `SCENE-KEYWORD-MEMBERSHIP`) | `extend` `offset` | 2 | EXPERIMENTAL (`plane` and `z-index` were the other two until 0.3, `PAINT-ORDER-CONSTRUCT` — see the retired row) |
| **`bitfield`** | `word` `numbering` `description` `present` `index` | 5 | NORMATIVE (0.1: `note` → `description`, `DESCRIPTION-KEY-SPELLING` — a rename **out of this namespace**, and the vacated spelling is live again language-wide since 0.3 (`DRAWN-ANNOTATION-FORM`) without returning here: `field` still refuses `note=`, §10 audience table. The positional flag `optional` became the key `present`, `PRESENCE-CONDITION-EXPRESSION`. 0.1: `index`, the repetition range, `BITFIELD-REPETITION-CONSTRUCT`) |
| **`timing`** | `data` | 1 | EXPERIMENTAL (inherited from the `timing` genre) |
| **`table`** | *(none of its own)* | 0 | NORMATIVE |
| **cross-namespace** (§5 presentation + `class` + `note` + `id`) | `fill` `stroke` `style` `class` `note` `id` | 6 | NORMATIVE (0.1: `style` lost its `field`/`cell`/`signal` acceptors — `STYLE-KEY-SCOPE` — and is now cross-namespace across the scene directives only. 0.3: `note` REVIVED as the drawn annotation, `DRAWN-ANNOTATION-FORM` — its acceptors span the scene namespace, `flowchart`'s and `statechart`'s own connector spellings and the `UNIVERSAL-CORE-KEYWORDS` core `title`, so no single namespace holds it. **0.5: `id` FILED here, `CONNECTOR-IDENTITY-KEY`/`REGISTERED-KEY-VALUE-GRAMMAR`** — its acceptors are `edge`, `flowline`, `transition` and `message`, which is the scene namespace plus `flowchart`'s, `statechart`'s and `sequence`'s own connector spellings, exactly the shape that made `note` cross-namespace. It was registered at `CONNECTOR-IDENTITY-KEY` and classified by no row of this table until `GRAMMAR-LAYERING-MODEL`, which is why the paragraph above read *"All 45"* against a registry of 46) |
| **experimental** (outside the conformance surface) | `type` (on `chart` and on `sequence`'s `fragment`) | 1 | EXPERIMENTAL |
| **retired language-wide** (no acceptor at all) | `w` `h` `dir` · `text` `z` · `kind` `layer` `labels` `unit` `via` `src` `dst` · `level` (DELETED not renamed) · `color` (`COLOUR-KEY-STATUS` — retired with NO replacement) · `points` `tailport` `headport` `routing` (`EDGE-GEOMETRY-CONSTRUCTS` — **WITHDRAWN** with the `path` directive, NO replacement) · `plane` `z-index` (`PAINT-ORDER-CONSTRUCT` — **WITHDRAWN** with the `plane` directive, NO replacement) | 20 | NORMATIVE (diagnostic) |

**`note` moved BETWEEN two rows of this table (`DRAWN-ANNOTATION-FORM`), and
that is the only kind of move this registry has not seen before.** Every
earlier movement was one-way — a live key retired into the last row, never
out of it. `note` goes the other way: it left the `bitfield` namespace when `DESCRIPTION-KEY-SPELLING` renamed it `description=`, sat in the language-wide
retired row for eleven dev states, and is live again as the **DRAWN
annotation**, in the cross-namespace row, under SYNTAX-STYLE **RULE 4.9**.
The move is legal precisely because of *why* `DESCRIPTION-KEY-SPELLING` retired it: `DESCRIPTION-KEY-SPELLING` retired
the spelling **in order to reserve the name** for the annotation construct
§9 `ANNOTATION-LOCATOR-SPLIT` was tracking, and this release **cashes that reservation rather
than reversing a decision**. The retirement predates the first stable
release and no published document ever carried the old meaning, so the
three RULE 4.9 obligations are all satisfiable — and the third of them,
*reverse the retirement diagnostic in the same release*, is why the message
`note= has been renamed: use description=` no longer exists anywhere in the
live engine (MIGRATIONS `0.2 → 0.3`; the message survives in
`archive/0.1/figdown.html`, which §13.5 forbids changing and where it is
still true). The key is **GATED on the declared version** — `note=` under
`figdown 0.1` or `figdown 0.2` is a line error naming `0.3` — and the gate's
reason is specific to this key rather than generic: the spelling has a prior
meaning on the record, so accepting it silently under an older header would
repaint an author's tooltip as ink, §13.0.1's named hazard.

**No total in this section moves.** The registry still holds **45 keys in
46 rows**, and it is still **39 NORMATIVE and 6 EXPERIMENTAL**: the row was already
counted, and already counted NORMATIVE, because a registration counts here
whether or not it has a live acceptor. What changed is which namespace owns
it (retired language-wide 19 → **18**, cross-namespace 4 → **5**) and
whether an author may write it (they may, at `figdown 0.3`). The count of
keys an author may write on some directive therefore goes **21 → 22**; it is
the first time that figure has risen without the registry's own size rising
with it.

The status column follows the same two-value rule stated for keywords
above. All three surviving §5 paint/dash keys — `fill=`, `stroke=` and
`style=` — are NORMATIVE. `stroke=` was demoted by `CONSTRUCT-STATUS-TIERS` on a
count of 5 in-repo uses and promoted back by `STROKE-KEY-STATUS` once the count was
re-measured (56+ in-repo, 567 downstream edge-colouring sites); the
fourth key of the old set, `color=`, is retired outright by `COLOUR-KEY-STATUS` and the
language has no label-colour key at all (§5; the derived default is
`LABEL-COLOUR-SOURCE`). None of the three may ever be meaning's only carrier (`GUI-WRITEBACK-STRUCTURE`/`PRESENTATION-AS-MEANING-CARRIER`).
`gap=` (a `group` layout option) stays normative. `extend=` and `offset=`
are demoted only because they are accepted solely by the demoted `band`
and `threshold` — a keyword and its only acceptors move together (b′),
which is also the rule that carried `plane=` and `z-index=` out of the
language with `plane` (`PAINT-ORDER-CONSTRUCT`). The
option-key totals therefore move with the keywords: **42 NORMATIVE and 4
EXPERIMENTAL**, 46 in all. (`id` is the forty-sixth, added, `CONNECTOR-IDENTITY-KEY`:
one key, four acceptors, NORMATIVE — a connector is not an experimental
construct, though two of its four spellings sit in experimental genres.) The four EXPERIMENTAL keys are `extend` `offset`
`data` `type`; `EDGE-GEOMETRY-CONSTRUCTS`'s four withdrawals and `PAINT-ORDER-CONSTRUCT`'s two crossed the line the
other way, because a withdrawn spelling is the opposite of unconverged —
it is gone, and a fixed diagnostic message is all that is left to
implement. (Five earlier counts in this paragraph did not add
up. It read "19 NORMATIVE and 9 EXPERIMENTAL, 28 in all" against a table that summed
to 30; the arithmetic was corrected, in the same pass that
added `width`/`height`/`extend` and the three retired language-wide keys.
The "22 NORMATIVE and 11 EXPERIMENTAL, 33 in all" that replaced it left `offset`,
`z-index` and `unit` out of the namespace table altogether and counted the
live `color=` as a retired diagnostic; 0.1 recounted key by key
against the engine's registry, which is where the 42 comes from. The
"29 NORMATIVE and 13 EXPERIMENTAL" that stood until 0.1 was itself one off its
own table, which summed to 30/12. The "32 NORMATIVE and 10 EXPERIMENTAL, 42 in all"
that stood until 0.1 went stale, which registered
`description` and `present` without recounting. The count above is
re-derived row by row
from the key table, where the two NORMATIVE rows spelled `fill` are ONE key
with two registrations and are counted once.)

Two keys straddle their namespace by registration, and the reason is
recorded here rather than in the table: `at` is core on `pin` and carries
a diagnostic-only registration on the scene directive `threshold` (renamed
`offset=`, when the directive was still spelled `guide`);
`width`/`height` are
live on `pin` (on the retired `size` until 0.1, `ELEMENT-GEOMETRY-DIRECTIVE`) and carry a
diagnostic-only registration on the
scene `node`. Neither is a redefinition —
each is one key with one meaning, registered on directives in two
namespaces. Each is counted **once**, in the namespace of the registration
that names it in the table above: `at` and `width`/`height` under layout.
The `UNIVERSAL-CORE-KEYWORDS` core of three owned no option key at all until 0.3 —
`figdown`, `title` and `layout` took none — which is why the row that used
to be spelled *core* is now spelled *layout*. No key moved and no count
changed then. **`title` gained its FIRST option key:
`note=`, the figure-level drawn annotation (`DRAWN-ANNOTATION-FORM`).** That does not restore
a *core* row and does not move `note` out of the cross-namespace row: the
key is classified by the namespace of its registrations taken together, and
`title` is one acceptor of ten spread across the scene namespace and two
genre namespaces. **Nor does it strain `UNIVERSAL-CORE-KEYWORDS`.** `UNIVERSAL-CORE-KEYWORDS` is a **fixity** guarantee
about what a core keyword MEANS — *wherever `title` appears, it names the
document, and no genre may redefine that* — not a bar on a core keyword
taking an option. `figdown` and `layout` still take none, and the reason is
their own: `figdown` must be readable before the genre is known, and
`layout` is a zone opener rather than a directive. `title`'s meaning under
`figdown 0.3` is byte-for-byte what it was under `figdown 0.1`; what is new
is a second channel written on the same line.
`kind` used to be the third such key — retired on `node`, live on `plot`
— and 0.1 ended the straddle by retiring it language-wide (the
live half is now `type=` on `chart`).

The **cross-namespace** keys reach the genres unevenly, and the honest
statement is the per-key number rather than a single count. Measured over
the eight genres' top-level allowlists (`REGISTERED-KEY-VALUE-GRAMMAR`): **`fill` 8, `stroke` 8,
`style` 8, `note` 8, `class` 7** — reach being where the key may be
WRITTEN on some directive legal at that genre's top level, which is not
the same as where it does something.
*(This paragraph said "shared by all six genres today", twice,
until `REGISTERED-KEY-VALUE-GRAMMAR`. There have been EIGHT genres, so
the count was stale by two — and "all eight" would be wrong too, which is
why the corrected form is per key.)* They are
deliberately neither core nor layout. `UNIVERSAL-CORE-KEYWORDS` fixes only the document's
structure and `LAYOUT-ZONE-NAMESPACE` only the layout zone, while
`fill`/`stroke`/`style`/`class` are the figure's vocabulary, so a
future genre MAY define its own meaning and defaults for them under `GENRE-VOCABULARY-OBLIGATION` — at
the cost of documenting them in its own vocabulary table, and subject to
`UNSAFE-DEFAULT-ELIMINATION` (§10 above): reuse must not cost the reader. The three layout keys
`at`/`width`/`height` are the contrast: they sit inside the zone `LAYOUT-ZONE-NAMESPACE` closes,
so no genre may redefine them, ever. All four are NORMATIVE
(`STROKE-KEY-STATUS` promoted `stroke=`; `COLOUR-KEY-STATUS` retired `color=`, which
was the fifth).

**`note=` joined this row and is the one member no genre's
own directive set admits (`DRAWN-ANNOTATION-FORM`).** *(This sentence read "WITHOUT being
shared by all six genres" until `REGISTERED-KEY-VALUE-GRAMMAR`, and it was misleading twice over:
the count was stale, and `note=` IS writable under every genre —
`title "T" note="x"` is accepted under `figdown 0.3 timing`, verified, because
`title` is a core keyword legal in every genre and this very paragraph
names it as an acceptor three sentences on. What is true is the narrower
claim now stated: no typed-block genre accepts it on a directive of its
own.)* Its
acceptors are the scene directives, `flowchart`'s and `statechart`'s own
role and connector spellings, and the `UNIVERSAL-CORE-KEYWORDS` core `title`; the three typed-block
genres accept it on no directive of theirs, and `field` refuses it by name.
It is filed here anyway because the classification asks which namespace
*owns* a key, and no single one does: a key accepted in the scene
namespace, in two genre namespaces and on a core keyword cannot be filed
under any of them without misstating where it lives. The `GENRE-VOCABULARY-OBLIGATION` licence reads
the same for it as for the other four — a future genre MAY give `note=` its
own defaults, at the cost of documenting it in its own vocabulary table and
subject to `UNSAFE-DEFAULT-ELIMINATION` — with one thing already fixed: what a `note` MEANS (an
aside, never structure, §12.7) is not a genre's to redefine, because a
reading agent resolves it before the genre is consulted.

The fourth column is the `CONSTRUCT-STATUS-TIERS` status. It is the **key's** status, and it
answers one question: *may an author write this key and stay inside the
v0.1 conformance surface?* A key is NORMATIVE when at least one directive that
**live-accepts** it is normative — the parenthetical names that directive.
A key is EXPERIMENTAL when the key itself is demoted, or when every directive
that live-accepts it is demoted.

`plane=` was the one key that fell under the FIRST of those two clauses
while normative directives still accepted it — `node`, `group`,
`external`, `edge` and `class` all live-accepted it and all five are
NORMATIVE, so the second clause would have kept the key normative. It was
demoted in its own right, by (b′): its only legal values came from
`plane` lines, and in a document that may not write one the key could
name nothing but the implicit `base`. A key with exactly one legal value
is not an option, so the keyword and its declaration point move together
— and they moved together again, out of the language (`PAINT-ORDER-CONSTRUCT`,
§2.4). **The clause is kept with no live instance**, because it is the
rule the next such key will be judged by and the example is what makes it
readable; the state it describes — a demoted key on five normative
acceptors — is exactly the state a reader will find in `migrations.md`
for eight releases of this project's history.

**Retired registrations are the third case, and they are NORMATIVE
(diagnostic).** A retired key has no live acceptor at all: the host
directive *rejects* it and emits a named migration message
(`from=`/`to=` on `band`, `label=`/`taillabel=`/`headlabel=` on `edge`,
`width=`/`height=` on `node`). A retired key that
left the LANGUAGE rather than a directive fires wherever it appears
instead —
`w=` `h=` `dir=` `text=` `z=`, the seven of 0.1
(`kind=` `layer=` `labels=` `unit=` `via=` `src=` `dst=`), `color=` and the four WITHDRAWN (`points=`
`tailport=` `headport=` `routing=`, `EDGE-GEOMETRY-CONSTRUCTS` — the first registrations whose
message names no successor, because there is none). What such a registration obliges an
implementation to do is produce that one fixed message, and those
messages are pinned by the **normative** conformance corpus — so the
obligation is inside the conformance surface even when the host
directive is demoted. `from=`/`to=` are therefore NORMATIVE (diagnostic) on
the demoted `band`, exactly as `label=`/`taillabel=`/`headlabel=` are on
`edge`. This is also the only reading consistent with what EXPERIMENTAL
means: a retired spelling is the opposite of unconverged — it is gone,
and only its error text remains.

| Key | Value | Accepted by | Status |
|---|---|---|---|
| `at` | `(x,y)` canvas px — a PAREN point (RULE 1.1a) | `pin` · `threshold` (diagnostic only) | NORMATIVE |
| `offset` | `0..100%`, the `%` mandatory | `threshold` | EXPERIMENTAL |
| `class` | id of a declared `class` | `node` (incl. `process`/`decision`/`terminator`) `group` `edge` `field` `cell` | NORMATIVE |
| `fill` | `#rgb`/`#rrggbb`/CSS name/`transparent` | `node` (incl. `process`/`decision`/`terminator`) `group` `band` `class` `bitfield` `table` `timing` `field` `cell` `signal` | NORMATIVE |
| `fill` on `edge` `threshold` `bundle` | RETIRED 0.1 — no interior, so it named the same channel as `stroke=` (§5, `INTERIOR-LESS-ELEMENT-PAINT`) | those three (all reject, naming `stroke=`) | NORMATIVE (diagnostic) |
| `color` | RETIRED language-wide at 0.1 (`COLOUR-KEY-STATUS`) — and it is the one retirement that is NOT a rename in either direction. In one era it set the FILL; in another it set the LABEL; v0.1 has no label-colour key at all. The message names BOTH eras and refuses to choose, which no live key could do | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `data` | comma-separated names for the lane's `=` data cells (WaveDrom's own key) | `signal` | EXPERIMENTAL |
| `extend` | `up`\|`down`\|`left`\|`right` | `band` | EXPERIMENTAL |
| `dir` | RETIRED language-wide at 0.1 — see below | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `gap` | px | `group` | NORMATIVE |
| `from` | `0..100%` — RETIRED, see below | `band` (rejects) | NORMATIVE (diagnostic) |
| `in` | id of the containing/target element | `node` (incl. `process`/`decision`/`terminator`) `threshold` `band` | NORMATIVE (on `node`) |
| `index` | the field's repetition RANGE, `<first>..<last>`, separator EXACTLY two dots and whitespace around it not significant (§12.2). `<first>` is ALWAYS a literal integer; `<last>` is a literal integer or opaque prose; a literal end above 9007199254740991 is a line error (§12.2, §12.5). Tri-state (§12.3): key absent = no repetition claim; `index=""` = repeats, indices not stated; a fully-literal range = determinate. **Determinacy is decided by parsing both ends as integers, NEVER by quoting** — RULE 2.3 makes quotes inert here, so `index="0..7"` is determinate too. Classic `field` form only (`BITFIELD-REPETITION-CONSTRUCT`) | `field` | NORMATIVE |
| `labels` | RETIRED language-wide at 0.1 — renamed `data=` (WaveDrom's own key is `data`, "an array of signal labels", one per value cell) | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `layer` | RETIRED language-wide at 0.1 — renamed `plane=` (`PLANE-KEYWORD-SPELLING`) — and `plane=` was itself **WITHDRAWN** at 0.3 (`PAINT-ORDER-CONSTRUCT`), so the message states the whole chain `layer=` → `plane=` → withdrawn and names NO live replacement. Naming `plane=` alone would send an author to a spelling that is itself a line error; the precedent for the repair is `route` → `path` at 0.1 | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `description` | authored documentation prose, quoted (`QUOTING-RULES`). Draws NO ink beyond an SVG `<title>` tooltip — a fact a human must SEE belongs in the label or a `class` meaning | `field` | NORMATIVE |
| `id` | the connector's HANDLE: a bare id (`LINK-OPERATOR-IN-IDS`/`QUOTED-IDS`), unique within the section and joining the section's node/group/`external`/region namespace, so a repeat is `duplicate id` and not a new diagnostic. **Requires `figdown 0.5`** — the key does not exist under `0.1`–`0.4` and writing it there is a line error naming the version (§13.7.2a: the gate engages on ADDING A SPELLING, and this key is the first gated under that rule rather than on a prior meaning; it has none). OPTIONAL everywhere: an anonymous connector is the language's default and asserts nothing less (§12.7). The spelling carries NO meaning — not a category, not an ordinal, not a relation to a similarly-spelled id (`CLASS-EMPTY-MEANING`'s rule for `class`, applied here). What consumes it: a `bundle` member ([experimental.md](experimental.md) §E5), `data-edge` in the artifact (§7), and a diff's pairing key | `edge` `flowline` `transition` `message` | NORMATIVE |
| `note` | the DRAWN annotation: authored prose the human reader must SEE, quoted (`QUOTING-RULES`) and rendered as a UML-style folded-corner box beside its carrier. **Requires `figdown 0.3`** — under `0.1`/`0.2` the spelling is still the retired one that meant `description=`, and writing it there is a line error naming the version. Retired at 0.1 (`DESCRIPTION-KEY-SPELLING`) *in order to reserve the name*, revived at 0.3 (`DRAWN-ANNOTATION-FORM`) under SYNTAX-STYLE RULE 4.9, which is why this is a reservation cashed and not a decision reversed. The author does not place the box (`DOMAIN-CONVENTION-DIRECTIVES`, §2.9); on `field` it is refused at every version, naming the audience division below | `node` (incl. `process`/`decision`/`terminator`) `state` `group` `edge` `flowline` `transition` `title` | NORMATIVE |
| `present` | the field's presence CONDITION as authored prose, quoted and MANDATORY. Tri-state (§12.3): key absent = no presence claim; `present=""` = conditional, condition not stated; `present="C = 1"` = conditional, condition stated. Quotable, never parsable (§12.7) | `field` | NORMATIVE |
| `numbering` | `lsb0`\|`msb0` (REQUIRED, §4.1) | `bitfield` | NORMATIVE |
| `plane` | **WITHDRAWN from the language at 0.3 (`PAINT-ORDER-CONSTRUCT`)** with the `plane` keyword that declared its only legal values (b′). Was the id of a declared plane, on `node` `group` `external` `edge` `bundle` `threshold` `band` `class`. NO replacement: the correct action is to delete the key, and doing so does not change the rendered output — paint order is document order (§2.4). `external` took this key and no other, so it now takes none | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `points` | WITHDRAWN language-wide at 0.1 (`EDGE-GEOMETRY-CONSTRUCTS`) — removed with the `path` directive, **not renamed**, so there is no spelling to migrate to. Author waypoints are outside the stable prior-art intersection (§9, `EDGE-IDENTITY-AND-GEOMETRY`) | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `routing` | WITHDRAWN language-wide at 0.1 (`EDGE-GEOMETRY-CONSTRUCTS`) — the mode set (`orthogonal`\|`straight`) and its two scopes ARE inside the intersection, and the key goes anyway on evidence and implementation, with **no replacement** (§9, `EDGE-IDENTITY-AND-GEOMETRY`) | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `shape` | §2.1 shape enum (6 values; `cloud` RETIRED at 0.1 — see below) | `node` (incl. `process`/`decision`/`terminator`, where it overrides the DERIVED geometry and never the role — `FLOWCHART-ROLE-KEYWORDS`) | NORMATIVE |
| `src` | RETIRED language-wide at 0.1 — renamed `tailport=` (`ENDPOINT-DOCKING-KEYS`), and `tailport=` was itself **WITHDRAWN at 0.1 (`EDGE-GEOMETRY-CONSTRUCTS`)**. There is **no replacement**: delete the line | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `dst` | RETIRED language-wide at 0.1 — renamed `headport=` (`ENDPOINT-DOCKING-KEYS`), and `headport=` was itself **WITHDRAWN at 0.1 (`EDGE-GEOMETRY-CONSTRUCTS`)**. There is **no replacement**: delete the line | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `stroke` | colour — the OUTLINE of a shape and the WHOLE of a line (§5) | `node` (incl. `process`/`decision`/`terminator`) `group` `edge` `bundle` `threshold` `band` `class` `bitfield` `table` `timing` `field` `cell` `signal` | NORMATIVE |
| `style` | `solid`\|`dashed`\|`dotted` | `node` (incl. `process`/`decision`/`terminator`) `group` `edge` `bundle` `threshold` `band` `class` — **`field` `cell` `signal` LEFT this list at 0.1** (`STYLE-KEY-SCOPE`): on a `field` it could erase the dash that is conditional presence's only carrier while the model kept it (`PRESENTATION-AS-MEANING-CARRIER`), and the three moved as one minimum set | NORMATIVE |
| `tailport` | WITHDRAWN language-wide at 0.1 (`EDGE-GEOMETRY-CONSTRUCTS`) — removed with the `path` directive, **not renamed**. Role-addressed attachment is inside the stable intersection; this realisation (a fraction on the EDGE, two attested axes collapsed into one key, attachment by written order) was not, and restoring it needs an edge-identity construct first (§9, `EDGE-IDENTITY-AND-GEOMETRY`) | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `headport` | WITHDRAWN language-wide at 0.1 (`EDGE-GEOMETRY-CONSTRUCTS`) — same reasoning as `tailport` | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `text` | RETIRED 0.1 — was renamed `color=`, which 0.1 then retired too (`COLOUR-KEY-STATUS`); v0.1 has no label-colour key | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `to` | `0..100%` — RETIRED, see below | `band` (rejects) | NORMATIVE (diagnostic) |
| `type` | **one registered key with TWO live value grammars, and they share no value.** On `chart`: the chart-type enum, one value, `bar3d` (spelled `bars3d` until 0.1). On `sequence`'s `fragment` (`SEQUENCE-GENRE-VOCABULARY`): the interaction-operator enum, **twelve** values (`alt` `opt` `loop` `par` `strict` `seq` `critical` `neg` `assert` `ignore` `consider` `break`), and **MANDATORY** — §12.5's model row says `always`. The two are section-disjoint (`chart` is not legal under `sequence` and `fragment` is not legal outside it), which is the third arm of the `gap` DECLARED EXCEPTION above | `chart` · `fragment` | EXPERIMENTAL |
| `unit` | RETIRED language-wide at 0.1 — renamed `word=` (`BITS-PER-ROW-KEY-NAMING`) | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `via` | RETIRED language-wide at 0.1 — renamed `points=` (`WAYPOINT-KEY-SPELLING`), and `points=` was itself **WITHDRAWN at 0.1 (`EDGE-GEOMETRY-CONSTRUCTS`)**. There is **no replacement**: delete the line | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `word` | bits per row | `bitfield` | NORMATIVE |
| `width` | px, strictly positive; NODES only (a group, an `external` or a typed block is a line error, §3) | `pin` · `node` (diagnostic only) | NORMATIVE |
| `height` | same as `width` | `pin` · `node` (diagnostic only) | NORMATIVE |
| `w` | RETIRED language-wide at 0.1 — see below | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `h` | RETIRED language-wide at 0.1 — see below | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `z-index` | **WITHDRAWN from the language at 0.3 (`PAINT-ORDER-CONSTRUCT`)** with its only host directive `plane`. Was an integer paint order, CSS's spelling in full (RULE 4.2). NO replacement | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `z` | RETIRED 0.1 — renamed `z-index=` (`Z-ORDER-KEY-NAMING`) — and `z-index=` was itself **WITHDRAWN** at 0.3 (`PAINT-ORDER-CONSTRUCT`), so the message states the whole chain `z=` → `z-index=` → withdrawn and names NO live replacement, the same repair the `layer` row carries | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `kind` | RETIRED language-wide at 0.1 — on a `node` use `shape=`, on a `chart` use `type=`; one spelling was retired on `node` and live on `plot` at once, inside one namespace | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |
| `label` | RETIRED, see below | `edge` | NORMATIVE (diagnostic) |
| `taillabel` | RETIRED, see below | `edge` | NORMATIVE (diagnostic) |
| `headlabel` | RETIRED, see below | `edge` | NORMATIVE (diagnostic) |
| `level` | RETIRED language-wide at 0.1 — **DELETED, not renamed** (`CHART-LEVEL-KEY`): it drew a reference plane through a 3-D bar chart, with zero uses corpus-wide, zero 3-D bar charts and zero requests; it was the only construct whose caption the ENGINE wrote rather than the author, and its `parseFloat` grammar uniquely accepted `1e3` where every other number in the language is `\d+(\.\d+)?` | *(no acceptor; every directive rejects)* | NORMATIVE (diagnostic) |

**`description=` and `note=` divide by AUDIENCE, and the division is
written here rather than left to be inferred (`DRAWN-ANNOTATION-FORM`).** Two
keys carrying authored prose invite exactly one mistake — reading them as
a long form and a short form of one channel — and a later reader who makes
it will try to merge them. They are not two spellings of one thing:

| key | audience | draws | acceptor |
|---|---|---|---|
| `description=` | **the machine** | no — an SVG `<title>` tooltip and nothing else | `field` — unchanged |
| `note=` | **the human** | **yes** | the ten directives listed above |

Writing **both on one element is legal and meaningful** — a long
machine-facing description beside a short drawn aside is the case the
division exists to serve — and **neither is a fallback for the other**.
An engine may not substitute one when the other is absent, an author may
not reach for `description=` to get ink, and a reader may not treat a
`note=` as a tooltip that happens to be visible.

**The machine-readable-only channel is an ASSET, not an untidiness.** This
standard's thesis is *one source, two readers*, and a channel addressed to
exactly one of them is what that thesis predicts, not a defect in it: it
lets an author carry a register's full IP-XACT description, a conformance
caveat or a provenance line without spending ink a human reader did not ask
for. Merging the two would force every machine-facing fact onto the page,
and `description=`'s own instruction — *a fact a human must SEE belongs in
the label or a `class` meaning* — would lose the counterpart that makes it
an instruction rather than a prohibition. That
counterpart has a name, and the instruction reads: put it in the label, in
a `class` meaning, or in a `note=`.

`in=` is **NORMATIVE**, not split: it is registered on the normative `node`
and on the demoted `threshold`/`band`, and the parenthetical in the status
cell names the
normative registration that earns the status. Writing either key on a
demoted directive is outside the conformance surface, but that follows
from the **directive** being demoted, not from any second status of the
key: `threshold` and `band` carry their status to everything written on
them. Recording it as a status of the key would make `fill`, `style`
and `class` split too — all three are likewise accepted by
`threshold`/`band`/`timing` — and would leave the rule above with nothing to
decide. That is a registration fact, not two meanings; `UNSAFE-DEFAULT-ELIMINATION` and the "no
redefinition" rule are untouched.

`kind=` used to be the one key whose cell really did carry two halves,
because its two registrations were of different KINDS rather than merely
of different status: a retired, rejecting diagnostic on `node` and a live
option on the experimental `plot`. 0.1 ended that (`CHART-BLOCK-NAMING`): a
spelling retired in one half of a namespace and live in the other is the
defect `UNSAFE-DEFAULT-ELIMINATION` exists to prevent, so `kind=` left the language entirely and
the live half became `type=` on `chart`. No key carries two halves now.

**Eleven** registrations carry **no effect**; each exists only so
that a retired or misplaced spelling produces a named migration
diagnostic instead of the generic `unknown option`:
`width=`/`height=` on `node`
(→ a `pin` line), `label=`/`taillabel=`/`headlabel=` on `edge`
(→ inline `[…]` labels), `from=`/`to=` on `band`
(→ the positional range), `at=` on `threshold`
(→ `offset=<0..100>%`) and `fill=` on `edge`, `threshold` and
`bundle` (→ `stroke=`, 0.1, `INTERIOR-LESS-ELEMENT-PAINT`: no interior, so the two keys named
one channel) —
2 + 3 + 2 + 1 + 3 = 11 (key, directive) pairs; the count is per
registration. It was twenty-four until 0.1, when `color=` moved to
the language-wide list below and took its thirteen per-directive
registrations with it — a key that left the LANGUAGE fires wherever it
appears and needs no per-directive registration at all. It was twenty-five
until 0.1, when `kind=` made the same move. (This count read
**seven**, and those two read twenty and twenty-one, until 0.1: the
`at=` and the three `fill=` registrations were named elsewhere in this
section — in the straddle note above and in the key table — but had never
been entered here.)
One retired **VALUE** sits alongside them and is counted separately,
because it is not a (key, directive) registration: `shape=cloud`
(retired 0.1, `SHAPE-ENUM-VOCABULARY` — the one member of a purely geometric enum
that named a domain, which `SHAPE-ENUM-VOCABULARY`/`EXTERNAL-EDGE-ENDPOINTS` forbid). It follows the same rule —
a named diagnostic instead of the generic `unknown shape` — and is
pinned by `910-errors-retired-shape-cloud` in the NORMATIVE corpus. It
does not change the eleven above. `type=bars3d` (renamed `bar3d`) is a second retired value and is counted the same way.

Retired **KEYS** sit alongside them for the same reason, and are also
counted separately — **eighteen** of them: `w=`, `h=` and `dir=`, `text=` and `z=`,
`kind=` `layer=` `labels=` `unit=` `via=` `src=` `dst=`,
`level=` (0.1 — DELETED rather than renamed, so its message names
no replacement), `color=` (`COLOUR-KEY-STATUS` — retired with no
replacement, and the one message that must name TWO eras) and the four
WITHDRAWN (`points=` `tailport=` `headport=` `routing=`,
`EDGE-GEOMETRY-CONSTRUCTS`). They
are not (key, directive) pairs because they left the LANGUAGE, not a
directive — no directive accepts them, so the message fires wherever the
key appears, the `colw` → `width` precedent. `911-errors-retired-option-keys`
pins the 0.1 three in the NORMATIVE corpus. (This list read
**fourteen** until 0.1, having stopped at 0.1: `note=` and
the four withdrawals were added to the language-wide row of the namespace
table above — which said **19** — but never here. It read
**nineteen** until 0.3, when `note=` LEFT this list
in the one direction no key had taken before: `DRAWN-ANNOTATION-FORM` revived it as a live
key, so it no longer fires anywhere and no longer has a message to count.
Eighteen is the engine's own table, key for key.) Moving to this list
DOES change the per-registration count, and twice: `kind=` on `node` left
it, and `color=`'s thirteen per-directive registrations left
it — which is the whole of the 24 → 11 drop. (`w=`/`h=` on
`node` were two of the count before 0.1 and `width=`/`height=` on
`node` are the same two now, so that move was neutral.)

Eight of the eleven messages are pinned by a case in the
NORMATIVE corpus — `203-node-rejected-options` (2),
`255-edge-retired-options` (3) and `905-errors-retired-text-option`, whose
last three lines pin the interior-less `fill=` trio — which is why they
are NORMATIVE (diagnostic). (That fixture also pins the retired
`text=`/`color=` family, which is on the language-wide
list below rather than in this count.) The three exceptions are
`from=`/`to=` on `band` (their only fixture, `373-band-errors`) and `at=`
on `threshold` (`376-threshold-offset`), both tagged
EXPERIMENTAL because their subject is a demoted directive. The status
rule still makes them NORMATIVE (diagnostic) — a retired spelling is gone,
which is the opposite of unconverged — but the pin is not currently in
the normative corpus, and this sentence says so rather than claiming a
coverage that does not exist.
`width=`/`height=` are live on `pin` (on `size` until 0.1);
`type=` is live on the experimental `chart`, outside
the v0.1 conformance surface (`level=` was its sibling there until 0.1 deleted it, `CHART-LEVEL-KEY`) — **and on `sequence`'s
`fragment` as well** (`SEQUENCE-GENRE-VOCABULARY`), where it is MANDATORY and carries a
twelve-member enum disjoint from `chart`'s single value. *(This sentence
read "live only on the experimental `chart`" until `REGISTERED-KEY-VALUE-GRAMMAR`,
which is nine dev states after `fragment` took the key. It was a FALSE
statement about the registry, not a stale emphasis: the "Accepted by"
column is defined four paragraphs above as "the complete set of directives
that accept the key", and it named one of the two. Nothing moved to correct
it — no construct, no diagnostic, no golden; the sentence and the two
registry cells were wrong and are now right.)* `fill=` was registered for the same
reason until 0.1 and left the registry with the `fill` → `band`
keyword rename; it came BACK, as the live spelling
`color=` was renamed to. That is the declared `UNSAFE-DEFAULT-ELIMINATION` exception recorded
above: `fill` is a retired keyword and a live option key at the same
time, and the two can never meet in a document that parses.
The other half of that trade closed the opposite way: `color=` is retired
again and for good (`COLOUR-KEY-STATUS`), so the key whose hazard bought the exception is
no longer in the language at all.

**Conformance mode.** v0.1 has a single mode: **strict**. Unknown
keyword, unknown option, malformed line, or unsupported registered
value → line error; a document with errors MUST NOT render. A *lenient*
mode and an `x-` extension namespace were sketched in earlier drafts
but had zero implementation, zero tests and no enablement path; both
were deferred to v0.2, and **`LANGUAGE-EXTENSION-POLICY` (2026-08-23) closes
both, permanently**. **The language stays CLOSED at 1.0**: no lenient
mode, no `x-` keyword, no `x-` option key, no private-use range. The
language's extensibility mechanisms are the four `EXTENSIBILITY-MECHANISMS` named — the
version header plus the compatibility rules (§13), the keyword-initial
line grammar (new capability = new keyword, gated by `.github/CONTRIBUTING.md` §2),
the `key=value` option slots, and the profile mechanism (new domain =
new profile, the core untouched) — and a vendor namespace is not among
them.

**The `x-` prefix is a NAMING BAN, not an extension namespace
(`LANGUAGE-EXTENSION-POLICY`).** The single sentence this section carried until 0.5 stated two
different things, and they are separated here because they have
opposite fates:

- **KEPT, permanently and unconditionally.** Standard FigDown keywords
  and option keys **MUST NOT** begin with `x-`. Nothing in the registry
  may be spelled that way, in this version or any later one.
- **RETIRED.** The prefix is **not** reserved *for* anything: the
  promise of a future experimental/vendor extension mechanism is
  withdrawn. `x-foo` does not become legal by waiting, and an engine
  MUST NOT admit it under any mode.

**What `x-` means, family-wide.** Since `PUBLICATION-MANIFEST-PROFILE` the publication manifest
([`figdown-manifest.md`](figdown-manifest.md) §2 rule 2) spends `^x-`
for keys **delegated to a publisher**. So across the whole FigDown
family the prefix means **"not FigDown's"**, and a standard keyword
beginning `x-` would make the family's most visible prefix mean two
opposite things in two file formats — the hazard
[`syntax-style.md`](syntax-style.md) RULE 4.7 names (a spelling spent
in one namespace is spent everywhere).

**Where an extension lives instead**, in order of preference: express
it with what exists (`class=` plus `meaning=` is the declared,
machine-readable category channel, §2.7); propose it through
[`.github/CONTRIBUTING.md`](../.github/CONTRIBUTING.md) §2; record it beside the figure in a
publication manifest, spelled `x-<owner>-<key>`
([`figdown-manifest.md`](figdown-manifest.md) §2 rule 2); or fork
honestly — a forked engine that accepts constructs `figdown X.Y` does
not accept **MUST NOT** read or write the token `figdown` in a header,
because that is a conformance claim (`INDEPENDENT-IMPLEMENTATION-CRITERION`) its documents cannot make.

**The reservation is enforced, which is what makes it one** (`SEMICOLON-STATUS`: *a
reservation that is not enforced is not a reservation*). A keyword or
an option key beginning `x-` gets its **own named diagnostic**, not the
words a typo gets — two call sites, keyword position and option-key
position, pinned by `conformance/cases/909-errors-reserved-x-prefix`
and by `900-errors-unknown-keyword`'s third line (`RESERVED-PREFIX-ENFORCEMENT`, 2026-08-23).
**The ban does not reach identifiers**: `node x-foo "A"` and
`class x-bar "meaning"` parse and render, because the reservation is
about the language's own vocabulary and an id is the author's.

**Teachability check (`AGENT-TEACHING-COST`).** Target: the full AI authoring guide fits
in ≤120 lines (the always-loaded `skill/figdown/SKILL.md` is the
measurement). **Every** addition to
the top-level keyword set must survive the `NEW-CONSTRUCT-EVIDENCE-GATE` gate — corpus evidence
AND semantic impossibility with the existing vocabulary. There is no
numeric budget: the earlier wording named a "~20 keyword" threshold that
the registry had already passed at 22, which made the rule
self-breaching, and a number in a normative document invites gaming the
count rather than answering the gate. The gate applies to the first
addition and to the hundredth alike.

## 11. Grammar sketch (ABNF)

> Completeness note (`GRAMMAR-SKETCH-COMPLETENESS`=(b), freeze prep): every nonterminal used below is
> defined here or imported from RFC 5234 core rules (`SP` `WSP` `ALPHA`
> `DIGIT` `DQUOTE` `VCHAR` `CRLF`); the `UTF8-*` productions are RFC 3629
> §4's, copied verbatim. Complete means DERIVABLE, so 0.1 corrected
> the grammar in two respects: `comment`/`comment-line` excluded `SP`
> (RFC 5234's `VCHAR` is `%x21-7E`), which made every commented line in
> this document underivable; and `qchar`, `cell-content`, `option-value`
> and `bare-token` were ASCII-only, which contradicted §1's UTF-8 rule,
> the fixture `114-lex-utf8-strings` and an engine that accepts
> `data=甲,乙` unquoted.
> The sketch is still a **sketch** of the
> line grammar — per-directive arity, genre allowlists (`GENRE-KEYWORD-ALLOWLIST`), and typed-block
> child scopes are specified in §1–§10 and the genre documents, not in this
> ABNF alone. Until freeze tags it fully normative, treat closed-grammar
> behaviour of the reference engine + §8 as the authority on errors.

```abnf
document       = *insignificant header *line
insignificant  = comment-line / blank-line
header         = "figdown" SP version SP genre eol
version        = "0.1" / "0.2" / "0.3" / "0.4" / "0.5"
                                          ; wire major.minor token only
genre          = "block" / "topology" / "flowchart"
               / "bitfield" / "table" / "timing"
               / "statechart"             ; version "0.2" and up (STATECHART-GENRE-SCOPE)
               / "sequence"               ; version "0.4" and up (SEQUENCE-GENRE-VOCABULARY)
               ; "0.5" adds no genre: it adds the option key `id=` (CONNECTOR-IDENTITY-KEY)
line           = directive-line / table-row / insignificant
directive-line = keyword *(SP argument) [SP comment] eol
argument       = qstring / option / bare-token
option         = option-key "=" option-value
option-key     = lower-alpha *(lower-alpha / DIGIT / "-")
               ; must be in the closed option-key registry (§10)
option-value   = *( VCHAR / UTF8-char )   ; one unquoted token (no SP).
               ; `=` IS allowed inside a value — the option key is the
               ; text before the FIRST `=` and the value is everything
               ; after it, so `description="a=b"` and a timing lane written as a
               ; bare token (`x=01`, which REQUIRES `=`) are both
               ; expressible. Earlier revisions excluded %x3D here and in
               ; bare-token, which made the timing genre inexpressible.
               ; A qstring region may appear anywhere inside the token
               ; when the value contains spaces, `#`, or escapes.
               ; A point-valued option uses paren points:
               ;   at=(x,y)                                          (§3)
               ; A comma-delimited list value is ONE whitespace-free
               ; token; a quoted element protects its own comma (§1).
               ; Non-ASCII needs no quotes: `data=甲,乙` on a timing lane
               ; parses, so the UTF-8 rule reaches unquoted values too.
bare-token     = 1*( VCHAR / UTF8-char )  ; non-space run; ops, ids, lanes, …
               ; `=` is permitted: a timing `signal` lane is a bare token
               ; over the closed alphabet [01pnx=.].
keyword        = lower-alpha *(lower-alpha / DIGIT / "-")
id             = (ALPHA / "_") *(ALPHA / DIGIT / "_" / "-")
qstring        = DQUOTE *(qchar / escape) DQUOTE
qchar          = %x20-21 / %x23-5B / %x5D-7E / UTF8-char
               ; any VCHAR or SP except " and \ (those use escape), plus
               ; any non-ASCII character: a document is UTF-8 (§1), and
               ; `title "中文標題"` is pinned by 114-lex-utf8-strings.
escape         = "\" ("n" / DQUOTE / "\")
table-row      = "|" cell-content *("|" cell-content) "|" eol
cell-content   = *( %x09 / %x20-7E / UTF8-char )
               ; raw GFM cell text (escapes: \| \^^;
               ; multi-line only via HTML <br> forms — §4.2 / 0.1)
comment        = "#" *( WSP / VCHAR / UTF8-char )
               ; trailing on a directive line. SP is INSIDE a comment —
               ; a comment runs to end of line and this spec's own
               ; examples are ordinary prose.
comment-line   = *WSP "#" *( WSP / VCHAR / UTF8-char ) eol
blank-line     = *WSP eol
lower-alpha    = %x61-7A; a-z
eol            = CRLF / LF; NOT bare CR — see §1 and the
                                          ; note below the block
; Non-ASCII, verbatim from RFC 3629 §4 (UTF-8 is the document encoding, §1)
UTF8-char      = UTF8-2 / UTF8-3 / UTF8-4
UTF8-2         = %xC2-DF UTF8-tail
UTF8-3         = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail )
               / %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
UTF8-4         = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail )
               / %xF4 %x80-8F 2( UTF8-tail )
UTF8-tail      = %x80-BF
; Imported from RFC 5234: SP WSP ALPHA DIGIT DQUOTE VCHAR CRLF
; LF = %x0A ; CR = %x0D
```

Two rules above are normative in substance, not merely sketch:
`*insignificant header` says the header is the first **significant**
line — comments and blank lines MAY precede it — and `SP genre` carries
no brackets, so the genre token is REQUIRED (§1).

**`eol` dropped `CR`.** The rule read `CRLF / LF / CR`
("any common newline form") from the first draft, and bare CR was never
implemented: a CR-only file reaches the reference engine as a single line
and fails on the second directive's first token. No fixture pinned it, and
every case source in `conformance/cases/` is LF — all of them then, all of
them still. Rather than add a terminator no document uses,
the grammar was corrected to what is accepted — which is also what §1 now
states in prose. A `document` production does not show the BOM: §1 strips a
leading U+FEFF before this grammar applies.

`title` needs no exception here: it is a
`directive-line` whose one argument is a `qstring`, like every other
directive that takes a string. Until that entry it consumed the rest of
its line, and this sketch never expressed that.

The normative point is not this exact ABNF but that the grammar MUST be
mechanically implementable without consulting the reference PoC.

**This is a requirement, not a claim that it is met.** It
holds for the GRAMMAR and for the model, including the `sections` wrapper
(§12.5). It does **not** hold for two things, and §8.2 and §8.3 name them
rather than leave the requirement reading as an achievement: error
messages for cases no fixture covers, and ~~error RECOVERY on inputs where
a failure cascades~~ — **the second was closed by `ERROR-RECOVERY-MODEL` (2026-08-22)**: §8.3
now states the recovery model as a rule, and what survives of it is the
narrower residue §8.3.8 names, the order of two competing directive-stage
faults on ONE line, which cannot change an error count.
`conformance/README.md` carries the same boundary.

## 12. The semantic model (normative)

### 12.1 Status and purpose

This section is **normative**. The abstract model below is the meaning
of a FigDown document, and the JSON form of §12.5 is its **canonical
binding**.

§3 already requires that "a conforming parser MUST produce the same
semantic model for the same source". Until this section existed, that
sentence had no referent: the model lived only inside
[conformance/normalize.js](../conformance/normalize.js) as a testing
projection, so what a second implementation was effectively told was
"match my parse tree", not "here is the meaning contract". Sections 1–11
describe how a document is **written**; this one describes what it
**means** once parsed, which is why it comes last.

The standard exists so that AI agents exchange figure meaning through
`.fd` files (`COMPLETENESS-DEFINITION`). Two agents can only share a language if the shared
artifact is the model, not the grammar: a writing agent commits to
producing a source whose model says what it meant, and a reading agent
is entitled to everything in the model and to nothing outside it. A
renderer is a third party to that exchange. Accordingly:

- A conforming parser MUST produce the model of §12.2–§12.4 for a
  document that parses without errors, and MUST produce no model at all
  for a document with errors (§8: a document with errors renders
  nothing).
- A conforming implementation SHOULD be able to emit the canonical JSON
  binding of §12.5; the golden fixtures in
  [conformance/](../conformance/README.md) are the test.
- A reading agent MAY rely on §12.7 and MUST NOT infer more.

### 12.2 The abstract model

A model is one **document** object. Every field below is listed with its
type, whether it is always present, and what it means. "Present when …"
fields are **omitted** when the condition does not hold — see §12.3.

**Document.** Keys in this order:

| key | type | present | meaning |
|---|---|---|---|
| `header` | object | always | §12.2 *Header* |
| `title` | string | when a `title` line is written | the figure's title (§1); the text is semantic whether or not a renderer draws it (§7). `title ""` is written, so the key is present and empty (§12.3) |
| `note` | string | when the `title` line carries `note=` | the FIGURE-LEVEL drawn annotation (§2.9, 0.3, `DRAWN-ANNOTATION-FORM`) — an aside about the figure as a whole, with no element to attach to. It is a document key rather than a member of a `notes` array because there is at most one: `title` is single-valued per section (`REPEATED-DIRECTIVE-HANDLING`, §8.1), so its option is too. Reading rule in §12.7; it is prose, never structure |
| `flow` | `"right"`\|`"down"`\|`"left"`\|`"up"` | always | the reading axis (§3), materialized from the genre default when no `flow` line is written |
| `classes` | array of *Class* | always (MAY be empty) | §2.7 |
| `planes` | array of *Plane* | always, exactly one entry | wholly NORMATIVE since 0.3: it holds the implicit `base` and nothing else, because `PAINT-ORDER-CONSTRUCT` withdrew the only directive that could declare a second (§2.4). Spelled `layers` until 0.1 (`PLANE-KEYWORD-SPELLING`) |
| `nodes` | array of *Node* | always (MAY be empty) | §2.1 |
| `groups` | array of *Group* | always (MAY be empty) | §2.2 |
| `externals` | array of *External* | when the document declares at least one `external` | §2.8. Spelled `boundaries` until 0.1 (`EXTERNAL-ENDPOINT-NAMING`) |
| `edges` | array of *Edge* | always (MAY be empty) | §2.3 |
| `ranks` | array of *Rank* | always (MAY be empty) | §3 |
| `pins` | array of *Pin* | always (MAY be empty) | §3, layout tier. The `sizes` array merged into this one at 0.1 (`ELEMENT-GEOMETRY-DIRECTIVE`) |
| `thresholds` | array of *Threshold* | always (MAY be empty) | EXPERIMENTAL — but **NOT empty on the whole normative surface**, see below; §2.6, [experimental.md](experimental.md) §E3; per-key shape in §E5. Spelled `guides` until 0.1 (`THRESHOLD-KEYWORD-SPELLING`) |
| `bands` | array of *Band* | always (MAY be empty) | EXPERIMENTAL — same caveat; §2.6, [experimental.md](experimental.md) §E3; per-key shape in §E5 |
| `bundles` | array of *Bundle* | always (MAY be empty) | EXPERIMENTAL — same caveat; §2.5, [experimental.md](experimental.md) §E2; per-key shape in §E5 |
| `regions` | array of *Region* | always (MAY be empty) | §4, in document order |
| `lifelines` | array of *Lifeline* | when the document declares at least one `lifeline` | EXPERIMENTAL — `sequence` only (`SEQUENCE-GENRE-VOCABULARY`). The participant columns, in declaration order, which IS their left-to-right order |
| `messages` | array of *Message* | when the document declares at least one `message` | EXPERIMENTAL — `sequence` only. Each carries `line`, and the figure's TIME AXIS is `messages` ∪ `states` sorted on it |
| `states` | array of *State occurrence* | when the document declares at least one `state` under `sequence` | EXPERIMENTAL — `sequence` only. Distinct from a `statechart` `state`, which is a *Node* |
| `fragments` | array of *Fragment* | when the document declares at least one `fragment` | EXPERIMENTAL — `sequence` only |
| `operands` | array of *Operand* | when the document declares at least one `operand` | EXPERIMENTAL — `sequence` only |

**The five `sequence` collections are OMIT-WHEN-ABSENT and are APPENDED
after `regions`**. They follow `externals`' rule rather than
`nodes`' for one reason and it is the same one `DRAWN-ANNOTATION-FORM` gave for appending
`note`: no `figdown 0.1`, `0.2` or `0.3` document can carry any of them, so
appending and omitting means **not one golden written before `figdown 0.4`
moves a byte**, and a consumer written against an earlier `0.x` reads every
document it could read before.

**A message is NOT an *Edge*, and the separation is normative.** An Edge is
a relation between two Nodes and this model says its order is not meaning
(§12.4 rule 6, §12.6); a Message is an OCCURRENCE whose place in the total
order IS the figure's content. Projecting messages into `edges` would tell a
consumer it may reorder them. In a `sequence` document `nodes`, `edges` and
`groups` are all empty.

**Correction: `thresholds`, `bands` and `bundles` are NOT
empty everywhere on the normative surface.** The three rows above said
"empty on the normative surface" until 0.1, and it
was **false**. Measured against `conformance/cases/`: `115-lex-option-
before-label` and `304-presentation-on-every-element` carry a non-empty
`thresholds`, `bands` AND `bundles`; `214-label-absent-vs-id` and
`215-label-empty-string` carry a non-empty `bundles`. Four normative model
goldens, and an implementation that emits `[]` for all three fails them.

What is true, and is what the clause was reaching for: **no document an
author is TAUGHT to write puts anything in these arrays**, because the
three keywords are EXPERIMENTAL and the authoring surface does not teach them.
The fixtures that populate them are cross-cutting LEXICAL cases — "an
option may precede a label on every directive", "every element takes the
presentation keys" — whose subject is a rule that quantifies over the
whole keyword registry, so they enumerate the registry, experimental rows
included. See [experimental.md](experimental.md) §E0 for what this costs
the skip-experimental promise; the promise is now stated with its
exceptions rather than unconditionally.

**Header.** `header` carries the wire-grammar version and the genre.

| key | type | present | meaning |
|---|---|---|---|
| `version` | string | always | the wire-grammar version **as the document declared it** — `"0.1"`, `"0.2"`, `"0.3"` or `"0.4"` (§1; `"0.3"` since 0.3 and `"0.4"` since 0.4, §13.7). It stopped being a constant at `STATECHART-GENRE-SCOPE`: while exactly one version existed, "emit the literal `\"0.1\"`" and "copy the source token" were the same instruction, and they are not any more. **An engine MUST emit the token the header declared**, never the newest version it implements and never the one it would have preferred — the declared version is the contract the author wrote against, and §13.7 forbids a reader inferring it from anything else. It is NOT the dev increment (0.1), which appears in a rendered artifact's `data-engine-version` and never in the model (§12.6). Stated normatively at 0.1; it was a hard-coded constant in `conformance/normalize.js` until `STATECHART-GENRE-SCOPE` |
| `genre` | string | always | one of the genre tokens legal at `version` (§1) — the six of `figdown 0.1`, those plus `statechart` at `figdown 0.2` and `figdown 0.3` (0.3 added an option key, no genre), or those plus `sequence` at `figdown 0.4` (`SEQUENCE-GENRE-VOCABULARY`). REQUIRED in the source since 0.1, so it can be absent only from a document that failed to parse. Since `GENRE-NAMESPACE` it also names the NAMESPACE the document's top-level keywords belong to — a consumer resolves every non-core keyword against it (§1 `GENRE-NAMESPACE`, §12.7) |

**Class** — `class` (§2.7).

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | always | the class id; its own namespace |
| `meaning` | string | always | the human- and machine-readable meaning. The FIELD is mandatory on the directive, so the key is never absent — but its VALUE may be the empty string, which is a written value meaning *no meaning is claimed* (§2.7, `EMPTY-LABEL-STATE`/`CLASS-EMPTY-MEANING`). `""` MUST be recorded as `""`; normalizing it to an omitted key would destroy the distinction `EMPTY-LABEL-STATE` exists to preserve |
| `fill` `stroke` `style` | string | when written | presentation defaults the class offers its members (§5) |
| `line` | number | always | 1-based source line |

**Plane** — the implicit `base`, and nothing else. The
element is **wholly NORMATIVE**: `PAINT-ORDER-CONSTRUCT` withdrew the `plane` keyword, so no
document can declare one, and `planes` is always present and always holds
exactly `planes[0] = {id:"base", z:0}`. Every reader meets that one
object and no other, whether or not they stay on the normative surface.

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | always | `"base"` — the only plane id the language can produce |
| `z` | number (integer) | always | `0`. Paint order among elements is DOCUMENT ORDER: a later line paints on top |

A *Plane* carries **no** `line`: the engine records none, and the one
plane is never declared. The `label` key left the element with the
keyword — only a `plane` line could write one — and so did the positional
`z` default (`PLANE-Z-INDEX-DEFAULT`), which counted declared planes. The element and its
model field were named `Layer`/`layer` until 0.1 (`PLANE-KEYWORD-SPELLING`); a model
key rename is normative (`NORMATIVE-SEMANTIC-MODEL`), so a second implementation emits
`plane`.

**Node** — `node`, and (under `flowchart` only) `process` / `decision` /
`terminator` (§2.1; [genres/experimental/flowchart.md](genres/experimental/flowchart.md) §Roles).

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | always | unique in the node/group/external/block namespace |
| `label` | string | when written | §12.3 — an absent label is absent, never the id; `""` is written and stays `""` |
| `role` | string | when a role keyword was written | `"process"` \| `"decision"` \| `"terminator"` (`FLOWCHART-ROLE-KEYWORDS`). **NOT materialized**: a bare `node` has NO role, and that absence is a fourth state, not a defaulted `"process"` — `UNSAFE-DEFAULT-ELIMINATION` §3, since a flowchart node may be a datastore, an annotation, a state or a wait. Legal only under genre `flowchart` |
| `shape` | string | always | the §2.1 geometric enum; materialized default `"box"`, or DERIVED from `role` when one is written (`process`→`box`, `decision`→`diamond`, `terminator`→`rounded`). A `shape=` on a role line changes THIS field and never `role` (§12.7) |
| `group` | string | when `in=` is written | the containing group's id |
| `plane` | string | always | materialized default `"base"` |
| `fill` `stroke` `style` | string | when written | §5 |
| `class` | **array of string** | when written | **references** to declared classes, never their resolved values (§12.6). Always an array, even for one class: `class=hot` emits `["hot"]`, because `class=` is multi-valued (`class=a,b`) and a consumer must not have to branch on arity (§12.5) |
| `note` | string | when written | the DRAWN annotation attached to this node (§2.9, 0.3, `DRAWN-ANNOTATION-FORM`). Prose, never structure — §12.7. The same key and the same rule apply to the `flowchart` roles `process`/`decision`/`terminator` and to the `statechart` `state`, which are Nodes in the model |
| `line` | number | always | 1-based source line |

**Group** — `group` (§2.2).

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | always | unique in the shared id namespace |
| `label` | string | when written | §12.3 |
| `gap` | number | when written | member spacing in px (presentation) |
| `fill` `stroke` `style` | string | when written | §5 |
| `class` | **array of string** | when written | class references, always an array (§12.5) — `class=hot` emits `["hot"]` |
| `note` | string | when written | the DRAWN annotation attached to this group (§2.9, `DRAWN-ANNOTATION-FORM`) — prose, never structure (§12.7) |
| `line` | number | always | 1-based source line |

Group membership is recorded **on the member**, as `node.group`. A
*Group* holds no child list.

**External** — `external` (§2.8). The element and the top-level array
were named `Boundary`/`boundaries` until 0.1 (`EXTERNAL-ENDPOINT-NAMING`).

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | always | unique in the shared id namespace |
| `label` | string | when written | no id fallback: an unlabelled external endpoint draws no text, and so does `""` (§12.3). An external carries NO presentation key at all since 0.1: `color=` was the only one it took and it is retired (`COLOUR-KEY-STATUS`), and NO option key at all since 0.3: `plane=` was the last one and `PAINT-ORDER-CONSTRUCT` withdrew it |
| `line` | number | always | 1-based source line |

**Edge** — `edge` (§2.3).

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | **when written** | the connector's HANDLE, from `id=` (§10, `CONNECTOR-IDENTITY-KEY`, `figdown 0.5`). Unique within the section and part of the shared node/group/`external`/region namespace. It is a handle and nothing else: a reader MUST NOT read a category, an ordinal or a relation to a similarly-spelled id out of its SPELLING (§12.7). Its ABSENCE is the ordinary case and asserts nothing — an anonymous connector is not less real, less connected or less asserted than a named one (§12.3) |
| `a` | string | always | the FIRST endpoint **as written** |
| `op` | `"--"`\|`"->"`\|`"<-"`\|`"<->"` | always | the operator **as written**; the direction of the relationship is derived from it (§12.4) |
| `b` | string | always | the SECOND endpoint **as written** |
| `tail` `mid` `head` | string | when written | the three label positions (§2.3) |
| `plane` | string | always | materialized `"base"`, and now that is its only value (§2.4) |
| `stroke` `style` | string | when written | §5. There is **no** `fill`: an edge has no interior, so `fill=` is a line error naming `stroke=` (§5, `INTERIOR-LESS-ELEMENT-PAINT`) |
| `class` | **array of string** | when written | class references, always an array (§12.5) — `class=hot` emits `["hot"]` |
| `note` | string | when written | the DRAWN annotation attached to this connector (§2.9, `DRAWN-ANNOTATION-FORM`) — prose, never structure (§12.7). It is **not** a fourth label position: a label is ON the edge and a note is BESIDE it, and the note is the only channel that reached an edge at all until 0.5, when `id=` gave it a handle as well (`CONNECTOR-IDENTITY-KEY`) — and the two are different things: a note is CONTENT drawn beside the connector, an id is a name other constructs use to reach it and is never drawn. The same key applies to the `flowchart` `flowline` and the `statechart` `transition`, which are Edges in the model |
| `line` | number | always | 1-based source line |

**Rank** — `rank` (§3).

| key | type | present | meaning |
|---|---|---|---|
| `ids` | array of string | always | two or more node ids sharing a rank |
| `line` | number | always | 1-based source line |

**Pin** — `pin` (§3, layout tier). ONE object per pinned element, carrying
the whole of its declared geometry. The separate `sizes` array and its
*Size* element were **removed** (`ELEMENT-GEOMETRY-DIRECTIVE`) when `size` merged into
`pin`: one directive, one model object. A consumer that read `sizes[]` reads
`pins[]` and tests for the extent keys.

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | always | the pinned node, group or external endpoint |
| `x` `y` | number | both when `at=` is written, otherwise neither | px in the element's positioning context (§3, `PIN-COORDINATE-SCOPE`); MAY be negative or fractional. `at=` is a POINT, so the pair never splits |
| `width` `height` | number | each when written | positive px matching `\d+(\.\d+)?`. **NODES only** — a group, an `external` endpoint or a typed block carrying either is a line error (§3). Spelled `w`/`h` in both the source and the model until 0.1, and carried by a separate `size` directive until 0.1 |
| `line` | number | always | 1-based source line |

At least one of `x`/`y`, `width`, `height` is always present: a `pin` line
declaring none of them is a line error, so an entry is never empty. A
second `pin` for the same id is a line error, which since the merge is one
rule covering the position and the extent together.

**Threshold**, **Band** and **Bundle** — the three elements whose
directives are EXPERIMENTAL (§10). Their per-key tables moved to
[experimental.md](experimental.md), with the rest of
those constructs' definitions. Their top-level arrays stay in the Document
table above, because a closed model has to say what exists; a document that
stays inside the v0.1 conformance surface writes none of the three, so
`thresholds`, `bands` and `bundles` are `[]`.

**Two Document keys LEFT the model (`EDGE-GEOMETRY-CONSTRUCTS`)**, and they are
the first to do so: the `paths` array and the top-level scalar `routing`,
withdrawn with the `path` and `routing` directives that were their only
source. A model produced by a conforming implementation no longer carries
either key under any circumstance, and there is no successor key — the
requirement is filed as §9 **`EDGE-IDENTITY-AND-GEOMETRY`**, not deferred to a renamed field.
There was a fourth EXPERIMENTAL element, **Path**, whose per-key table had moved
to [experimental.md](experimental.md); it went with the
directive.

**Lifeline**, **Message**, **State occurrence**, **Fragment** and
**Operand** — the five `sequence` elements (`SEQUENCE-GENRE-VOCABULARY`). The
genre is EXPERIMENTAL and so are all five; a document inside the v0.1
conformance surface writes none of them and omits all five arrays.

**Lifeline** — `lifeline` (this genre's *Node*).

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | always | the participant's id; shares the document-wide id namespace |
| `label` | string | when written | the participant's name. Absent is ABSENT, never the id (§12.3) |
| `in` | string | when `in=` is written | the `fragment` or `operand` id this participant exists inside (sense 1) |
| `fill` `stroke` `style` | string | when written | §5 |
| `class` | array of string | when written | §2.7 |
| `note` `description` | string | when written | drawn aside, and never-drawn authored prose (§2.9, §12.7) |
| `line` | number | always | 1-based source line |

**Message** — `message` (this genre's *Edge*, and NOT an Edge).

| key | type | present | meaning |
|---|---|---|---|
| `a` `b` | string | always | the two endpoints; each MUST be a declared `lifeline` id |
| `op` | `"->"` \| `"<-"` \| `"<->"` | always | direction. **`"--"` cannot appear**: it is a line error in this genre, because a Message carries a sending AND a receiving event occurrence. `"<->"` is a sustained two-way exchange whose individual messages are not enumerated |
| `id` | string | **when written** | the connector's HANDLE, from `id=` (§10, `CONNECTOR-IDENTITY-KEY`, `figdown 0.5`) — the same key the other three connector spellings carry, with the same meaning. It states NO order between two occurrences and NO identity between them: the total order is `line`, and two messages are two messages however they are named (reading rule 12) |
| `label` | string | when written | the message text, from the trailing quoted string or the inline `-[mid]->` form. Writing both is a line error, so the key never holds two, and there is no `mid` key: on a message the mid position IS the label |
| `tail` `head` | string | each when written | the end labels, as on an Edge |
| `in` | string | when `in=` is written | the `fragment` or `operand` id this message occurs inside |
| `stroke` `style` | string | when written | §5. No `fill`: a line has no interior |
| `class` | array of string | when written | §2.7 |
| `note` `description` | string | when written | §2.9, §12.7 |
| `line` | number | always | 1-based source line — **and it is content here, not an address**: the total order is `messages` ∪ `states` sorted on it, and nothing else records it |

**State occurrence** — `state` under `sequence` (UML 2.5.1 `StateInvariant`).
It is NOT the `statechart` `state`, which is a *Node* with an id.

| key | type | present | meaning |
|---|---|---|---|
| `lifeline` | string | always | the declared `lifeline` this state is OF. Slot 1 REFERENCES; it declares no id, because nothing in this genre refers to a state occurrence |
| `name` | string | always | the state's name. MANDATORY in the source, which is why it is not spelled `label` — an omitted label is legal wherever `label` appears and is a line error here |
| `in` | string | when `in=` is written | the `fragment` or `operand` id this occurrence sits inside |
| `fill` `stroke` `style` | string | when written | §5 |
| `class` | array of string | when written | §2.7 |
| `note` `description` | string | when written | §2.9, §12.7 |
| `line` | number | always | 1-based source line, and content — see *Message* |

**Fragment** — `fragment` (UML 2.5.1 `CombinedFragment`).

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | always | shares the document-wide id namespace with `lifeline` and `operand` |
| `label` | string | when written | the frame's caption |
| `type` | string | **always** | one of `alt` `opt` `loop` `par` `strict` `seq` `critical` `neg` `assert` `ignore` `consider` `break` — UML `InteractionOperatorKind` (§17.12.15.3) taken whole. MANDATORY in the source: UML defaults the attribute to `seq` and FigDown declines the default, a declared divergence |
| `in` | string | when `in=` is written | the enclosing `fragment` or `operand`. **Nesting is capped at ONE level**: a fragment may sit inside an operand of one enclosing fragment and no deeper |
| `stroke` `style` | string | when written | §5. No `fill`: a fragment is a frame over the messages it holds |
| `class` | array of string | when written | §2.7 |
| `note` `description` | string | when written | §2.9, §12.7 |
| `line` | number | always | 1-based source line. A fragment is a DECLARATION and carries no time position of its own: its extent is the span of the occurrences that name it |

**Operand** — `operand` (UML 2.5.1 `InteractionOperand`).

| key | type | present | meaning |
|---|---|---|---|
| `id` | string | always | shares the document-wide id namespace |
| `label` | string | when written | the compartment's guard |
| `in` | string | **always** | the `fragment` this compartment belongs to. MANDATORY, and it MUST resolve to a `fragment` — never to another operand, and never to a lifeline |
| `stroke` `style` | string | when written | §5 |
| `class` | array of string | when written | §2.7 |
| `note` `description` | string | when written | §2.9, §12.7 |
| `line` | number | always | 1-based source line; extent as for *Fragment* |

**Two whole-document rules the five share**, both checked by the reference
engine and both about the MODEL rather than the drawing:

- **Contiguity.** The occurrences naming one container must be a
  CONTIGUOUS run in declaration order. An operand denotes the ordered run
  of the occurrences it contains, so an occurrence that is not in it cannot
  happen between two that are; a non-contiguous membership denotes nothing.
- **No restatement.** Two consecutive `state` entries with the same
  `lifeline` and the same `name` are a line error. A state that has not
  changed is never restated, and a change is derived from an adjacent pair.

**Region** — the nested genre regions of §4, discriminated by `genre`.
Regions appear in `regions` in document order. (`regions`/`genre` were
spelled `blocks`/`type` until 0.1; the array held the same objects.
The pair now says what a region IS: `GENRE-NAMESPACE` `GENRE-COMPOSITION` defines a nested `bitfield`,
`table` or `timing` as a region governed by THAT genre's namespace, and the
discriminator names exactly that genre. `chart` (§4.4) is the one value
that is not a header genre; it is EXPERIMENTAL and outside the
conformance surface, so a normative-surface reader never meets it. The
value was `"plot"` until 0.1, `CHART-BLOCK-NAMING`.)

*bitfield* (§4.1, [genres/bitfield.md](genres/bitfield.md)):

| key | type | present | meaning |
|---|---|---|---|
| `genre` | `"bitfield"` | always | discriminator — the nested genre governing this region |
| `id` | string | always | shares the node/group/external namespace |
| `label` | string | when written | §12.3 |
| `word` | number | always | bits per row; materialized default `32` (spelled `unit` until 0.1) |
| `numbering` | `"lsb0"`\|`"msb0"` | always | REQUIRED in the source — no default (`UNSAFE-DEFAULT-ELIMINATION`) |
| `fill` `stroke` | string | when written | §5; a block takes no `style=`/`plane=` |
| `fields` | array | always, never empty | field and break items, interleaved in document order (a `bitfield` with no field is a line error) |
| `line` | number | always | 1-based source line |

A `fields` member is either a **field** — `name` (string), `width`
(number, or the string `"*"` for the variable-length form), `present`
(**string**, when written), `index` (**object**, when written),
`fill` `stroke` `class` `description` (when
written), `line` — or a **break marker**, which carries exactly
`break: true` and `line` (spelled `wrap` until 0.1, `ROW-BREAK-NAMING`). A field
carries **no `style`** (`STYLE-KEY-SCOPE`): the dash is conditional
presence's only carrier and nothing else may set or clear it.

**`index` is an OBJECT, and the shape is the ruling (`BITFIELD-REPETITION-CONSTRUCT`).**
The model MUST record enough that a reader can tell a DETERMINATE run from an
indeterminate one **without parsing prose**, so the authored text is not what
is stored:

| written | `index` in the model | what a reader concludes |
|---|---|---|
| *(key absent)* | *(member absent)* | no repetition claim |
| `index=""` | `{}` | it repeats; no index is stated |
| `index=0..7` or `index="0..7"` | `{"first": 0, "last": 7}` | **determinate**, 8 elements |
| `index=53..0` | `{"first": 53, "last": 0}` | determinate and descending, 54 elements |
| `index="0..Last Entry"` | `{"first": 0, "last": "Last Entry"}` | base known, count NOT in the document |

**Determinacy is therefore a JSON TYPE TEST on `last`** — number ⇒
determinate, string ⇒ prose — never a quoting test and never a regex over the
author's text (§12.7). `first` is a number whenever it is present, because
`<first>` is always a literal integer in the source. Nothing derived is
materialized: the element COUNT is not a model member, for exactly the reason
§12.4 rule 1 and `DECLARATION-ORDER-SEMANTICS` give for the bit ranges — a reader computes it from
`first` and `last`, which the model does carry.

**Three properties of the written range are normative, and each was left
unstated until 0.1:**

1. **Whitespace around `..` is not significant.** `index="0 .. 7"` is
   `index=0..7`; the ends are trimmed. It is reachable only in the QUOTED
   form, because an unquoted option value is one whitespace-free token (§1).
   The source is Ada (ISO/IEC 8652), which writes `1 .. 10`. Pinned by
   conformance case `420-bitfield-index-tristate`.
2. **The separator is EXACTLY two dots.** A dot run longer than two is a
   line error, not a separator followed by prose. Until 0.1 the rule
   was stated only as "one `..`, and both ends present", and `index=0...7`
   satisfied its letter — splitting into two parts and recording
   `{"first": 0, "last": ".7"}`, a model the source does not say.
3. **Each literal end MUST be at most 9007199254740991.** §12.5 binds a
   number to an ECMAScript `Number`, so a larger integer would be stored
   rounded, and `|last − first| + 1` would then report a count the source
   does not state. §12.7 requires the model to be recoverable **from the
   source**; a silently rounded index is the case that rule exists to
   exclude, so an end above the bound is a **line error** and never a value.
   The bound applies to `first`, and to `last` when `last` is literal — a
   prose `last` is a string and carries no arithmetic.

`present` replaced a boolean (`PRESENCE-CONDITION-EXPRESSION`). The source construct was
a bare positional flag — `optional` (…0.1), `conditional`
(0.1…0.1, `PRESENCE-FLAG-SPELLING`), `optional` again (`PRESENCE-FLAG-SPELLING`) — and the
model carried `optional: true`, *present only when true*. It is now a
**string carrying the presence condition**, and the test is ABSENCE, never
truthiness: the key is present exactly when the author wrote it, and
**`present: ""` is a written value** meaning "conditional, condition not
stated" (§12.3). `description` was spelled `note` until the same release
(`DESCRIPTION-KEY-SPELLING`).

*table* (§4.2, [genres/table.md](genres/table.md)):

| key | type | present | meaning |
|---|---|---|---|
| `genre` | `"table"` | always | discriminator — the nested genre governing this region |
| `id` | string | always | shared id namespace |
| `label` | string | when written | §12.3 |
| `fill` `stroke` | string | when written | §5 |
| `heads` | array of array of *Cell* | always | header tiers, top-down (`h1`, `h2`, …) |
| `aligns` | array of string | always | per column: `"left"`, `"center"`, `"right"` or `"none"` |
| `rows` | array of *Row* | always (MAY be empty) | data rows, in document order |
| `width` | object | when a `width` line is written | `widths` (array; each member is `"auto"`, `{px: n}` or `{pct: n}`) and `line` |
| `marks` | array of *Mark* | when NON-EMPTY | per-cell annotations |
| `highlights` | array | when NON-EMPTY | each member is `row` (number) + `line` |
| `line` | number | always | 1-based source line |

**`marks` and `highlights` are keyed on EMPTINESS, not on absence.** They are the two keys in the whole model whose presence
rule is "the array has at least one member" rather than §12.3's "the
author wrote something". The distinction is invisible in v0.1 — a `cell`
line either contributes a member or is a line error, and an erroring
document emits no model at all — so the two rules currently select the
same documents. It is stated anyway because it is the rule an
implementation must **code**: emit the key when the array is non-empty,
never `[]` and never `null`. The rows read "when at least one … line is
written" until 0.1, which described the same behaviour by the
wrong test; the emptiness test is what `conformance/normalize.js`
implements and what the goldens pin. Contrast the document-level
`thresholds` / `bands` / `bundles` / `ranks` / `pins` / `regions`, which
are **always present** and are `[]` when empty: presence is per key, not a
model-wide convention, and §12.2 is the only place that says which is
which.

A *Cell* carries `v` (string, the cell text) and `merge` (`"left"` for a
colspan into the cell on its left, `"up"` for a rowspan into the cell
above), present only when the cell is merged. A *Cell* carries no
`line`; its position in `heads`/`rows` is its address. A *Row* carries
`cells` and `line`. A *Mark* carries `header` (boolean, present only
when the address is a header tier), `row`, `col`, `fill`, `stroke` and
`class` when written, and `line`; header tiers `h1..hN` address as `row`
`1..N`. A *Mark* carries **no `style`** (`STYLE-KEY-SCOPE`).

*timing* (§4.3, [genres/experimental/timing.md](genres/experimental/timing.md)):

| key | type | present | meaning |
|---|---|---|---|
| `genre` | `"timing"` | always | discriminator — the nested genre governing this region |
| `id` | string | always | shared id namespace |
| `label` | string | when written | §12.3 |
| `fill` `stroke` | string | when written | §5 |
| `signals` | array of *Signal* | always, never empty | in document order |
| `gaps` | array of number | when at least one `gap` child is written | cycle indices of the visual breaks |
| `line` | number | always | 1-based source line |

A *Signal* carries `name`, `lane` (the raw lane string — one character
per cycle, over the closed alphabet `[01pnx=.]`), `data` (array, when
`data=` is written; spelled `labels` in both the source and the model
until 0.1, `SIGNAL-DATA-KEY-SPELLING`), and `fill`/`stroke` when written. A *Signal*
carries **no `style`** (`STYLE-KEY-SCOPE`), and **no** `line`: the
engine records none, and lane order is carried by array position.

*chart* (§4.4) is EXPERIMENTAL and outside the v0.1 conformance surface.
Its shape is `genre: "chart"`, `table` (the referenced table id), `type`,
and `line` (`level` was deleted, `CHART-LEVEL-KEY`). The genre value was `"plot"` and the
type field `kind` until 0.1 (`CHART-BLOCK-NAMING`).

### 12.3 Absent is omitted, and absence is meaning

**Normative rule.** An optional attribute that the source does not write
is **OMITTED** from the model. It MUST NOT be emitted as `null`, as an
empty string, or as a materialized stand-in value. A consumer therefore
tests for the *presence of the key*, and the presence of the key means
the author wrote it.

This is load-bearing, not tidiness. `READ-SIDE-DETERMINISM`/`OMITTED-LABEL-RECORDING` ruled that a materialized
default is legitimate only when it does not merge two distinguishable
documents into one model. `node a` and `node a "a"` are different
documents; recording the first as `label: "a"` forges authorial intent.
So: **the model records absence; the renderer substitutes the id for
display** (§2.1). The substitution is a RENDERING rule and MUST NOT
appear in the model.

The rule applies to every optional attribute in §12.2. Enumerated, the
omitted-when-absent fields are:

- `title` at document level, and the whole `externals` array
  when no `external` is declared. Every other top-level collection is present even
  when empty — the document shape is fixed. (`routing` and `paths` were
  the other two entries here until 0.1, when `EDGE-GEOMETRY-CONSTRUCTS` removed both keys
  from the model outright.)
- The optional **label** of `node`, `group`, `bundle`, `external`,
  `bitfield`, `table` and `timing`. (`class`'s `meaning` and a
  the `threshold` and `band` labels are MANDATORY on their directives and so are always
  present.)
- `fill`, `stroke`, `style` wherever §5 accepts them (0.1: `style`
  is no longer accepted on `field`, `cell` or `signal` — `STYLE-KEY-SCOPE`), and
  `class` on `node`, `group`, `edge`, `field` and the `cell` mark.
- `node.group` (absent when no `in=`), `group.gap`, edge `tail`/`mid`/
  `head`, `pin.x`/`pin.y` (together — `at=` is a point) and
  `pin.width`/`pin.height` individually.
- `node.role` — a flowchart role keyword's own claim, absent on a bare
  `node`; the absence is meaning (`UNSAFE-DEFAULT-ELIMINATION` §3), not a defaulted `"process"`.
- table `mark.header` — a boolean present **only**
  when true.
- `field.description`, `field.present` and `field.index`, table `width`,
  `marks` and
  `highlights`, timing `signal.data`, and timing `gaps`. **`field.present`
  is the sharpest case in the list**: it is a STRING whose empty value is a
  written value, so the test is absence and never truthiness — `present: ""`
  (conditional, condition not stated) and an omitted `present` (no presence
  claim at all) are two different facts, both recoverable (
  `PRESENCE-CONDITION-EXPRESSION`). **`field.index` is built to the same shape** (`BITFIELD-REPETITION-CONSTRUCT`): an
  EMPTY OBJECT `{}` is a written value meaning "it repeats, no index stated",
  an omitted `index` is no repetition claim at all, and the test is the
  presence of the key — never emptiness and never truthiness, both of which
  would merge the two.

Two consequences a second implementation must honour. A `null` anywhere
in a model is a defect, not a value: it marks a place where a numeric
attribute produced NaN, which is a line error instead.
And an omitted key is **not** a licence to substitute: §12.7 lists what
a reading agent may conclude, and "no label was written" is one of the
things it is entitled to know.

**Absence in the SOURCE — and the one place v0.1 has it.** Everything above
is about absence in the MODEL. There is exactly one construct in v0.1 where
absence in the *source text* carries meaning, and until 0.1 this
enumeration did not name it:

> **A table pipe row's cell is a COLSPAN when its raw segment is EMPTY —
> zero characters between the two `|`.** `| A || B |` is a two-column header
> whose first cell spans two columns. A segment holding only WHITESPACE is an
> ordinary empty cell: `| A |  | B |` is three independent single-column
> cells. The encoding is INJECTIVE — the two are distinguishable, and both
> are pinned as goldens — but the distinguishing evidence is the *absence of
> characters*, which is why it belongs in this section.

The consequences are stated where an author meets them,
[genres/table.md §"The colspan is spelled by an empty segment"](genres/table.md),
and they include a real hazard: **a formatter that pads cells rewrites the
figure.** `| A || B |` → `| A |  | B |` turns a two-tier header with a
colspan into independent single-column headers — no error, a different model,
a different figure. That is a `RENDERING-DETERMINISM` stability break through an ordinary tooling
pass, and it is filed as **§9 `COLSPAN-EMPTY-CELL-SPELLING`**, not fixed: changing the spelling is a
language change in a frozen genre.

**The empty string is a written value.** The rule above distinguishes
*absent* from *written*; an explicitly empty label is *written*, and it
is a third state (`EMPTY-LABEL-STATE`). `node a ""`, `group g ""`, `bundle b ""`,
`external x ""`, `title ""` and the typed blocks
`bitfield` / `table` / `timing` all record `label: ""` — never the omitted
key. A consumer that tests truthiness rather than key presence merges
two distinguishable documents and is non-conforming. (`plane L ""` was on <!-- fence-check: skip -->
this site list until 0.3, when `PAINT-ORDER-CONSTRUCT` withdrew the keyword; the
eleventh site, `field ""`, is the `bitfield` genre's own name slot and is
stated in [genres/bitfield.md](genres/bitfield.md), because what it draws
— an unnamed CELL — is not what any of the others draw.)

The reason is `READ-SIDE-DETERMINISM`'s own premise: writing `""` is a distinction the
author made, and the model must not destroy it. It is also the only way
to express a source figure whose shape carries no text; without it an
author must invent a label or drop the shape, and both lose source
fidelity (`TRANSCRIPTION-FIDELITY-TIERS`).

The id substitution is a RENDERING rule and it applies **only to
absence**. An empty label draws nothing at all — no text, and never the
id, which is an internal handle rather than authored display text. The
implicit `base` plane writes no label and therefore carries none.
(Frozen in case `215-label-empty-string`, with the absent half in
`214-label-absent-vs-id`; the split this replaced was
`conformance/DISCREPANCIES.md` **`EMPTY-LABEL-DIRECTIVE-COVERAGE`**, now resolved.)

### 12.4 Normalization rules

Everything a parser decides that is not literally in the source text.
The list is exhaustive for v0.1.

1. **Materialized defaults.** The parser resolves these before the model
   is visible, so they are ALWAYS present:

   | field | value | ruled by |
   |---|---|---|
   | `flow` | the genre default — `"down"` for `flowchart`, `"right"` for the other five — unless a `flow` line overrides it | §1, `GENRE-NAMESPACE` `PER-GENRE-DEFAULTS` |
   | `node.shape` | `"box"` | §2.1 |
   | `node.plane`, `edge.plane` | `"base"` | §2.4 |
   | `planes[0]` | the implicit base plane `{id: "base", z: 0}`, always first, never declared, and since 0.3 the only entry (§2.4, `PAINT-ORDER-CONSTRUCT`) | §2.4, `PLANE-Z-INDEX-DEFAULT` |
   | `band.extend` | `"up"` | §2.6 |
   | `band.fill` | the renderer's band colour | caveat below |
   | `bitfield.word` | `32` | §4.1 |

   Every one of them names something the author left to the language;
   none of them invents content (`READ-SIDE-DETERMINISM` §3). `numbering=` is deliberately
   **not** on this list: it has no default because a wrong bit ruler
   asserts a falsehood (`UNSAFE-DEFAULT-ELIMINATION` §3).

   `band.fill` is the one entry that breaks `READ-SIDE-DETERMINISM`'s rule and is recorded
   as such (it was misspelled `band.color` in this table until 0.1;
   the model key has always been `fill`): the engine resolves it before the model exists, so a band
   that wrote no `fill=` is indistinguishable from one that wrote the
   default value. Frozen in the goldens and noted in
   [conformance/README.md](../conformance/README.md); a v0.2 candidate,
   not a licence.

   `node.plane` and `edge.plane` are the last two materializations of a
   key the source can no longer write: `plane=` is withdrawn (`PAINT-ORDER-CONSTRUCT`,
   §2.4), so the value is always `"base"` and the parser supplies it. The
   asymmetry this rule used to record — materialized on `node` and `edge`,
   omitted when absent on `group`, `external`, `bundle`, `threshold`,
   `band` and `class` — is **discharged** by the withdrawal, because there
   is no longer an absent case to differ about. The two keys are kept in
   the model rather than dropped, since removing them would be a model
   change under `NORMATIVE-SEMANTIC-MODEL` and they carry a true fact: every element is painted
   in the one plane there is.

2. **Default `z` — DISCHARGED (`PAINT-ORDER-CONSTRUCT`).** The implicit `base`
   plane is `z = 0` and it is the only plane, so there is nothing left to
   order. Until this release a `plane` line without `z-index=` took its
   1-based position among the DECLARED planes in document order, an
   explicit `z-index=` overrode that for its own line without shifting its
   neighbours, and `z` values needed be neither unique nor contiguous with
   ties keeping document order. The rule number is kept rather than closed
   up so that every citation of rules 3–7 still resolves; what replaces it
   is the one-line invariant in rule 6, **document order is paint order**.
   (§2.4, `PLANE-Z-INDEX-DEFAULT`/`READ-SIDE-DETERMINISM`.)

3. **Edge direction is derived, and the written form is preserved.** The
   model of `edge b <- a` is `{a: "b", op: "<-", b: "a"}`. Normalizing
   it to `{a: "a", op: "->", b: "b"}` is FORBIDDEN, and so is the
   reverse rewrite. The **direction of the relationship** is read from
   the operator — `->` runs first→second, `<-` runs second→first, `--`
   is undirected, `<->` is bidirectional — so two implementations agree
   on direction without agreeing on a normal form. Normalizing would
   also break references: `bundle`
   records members in written order. (§2.3, `EDGE-WRITTEN-FORM`/`READ-SIDE-DETERMINISM`.) Restating an edge to
   refer to it was `path`'s mechanism too, until `EDGE-GEOMETRY-CONSTRUCTS` withdrew the
   directive — and that mechanism's inability to grow is
   what §9 `EDGE-IDENTITY-AND-GEOMETRY` records as the blocker.

4. **No inheritance, no resolution, one scope.** The model is flat.
   `class=` is recorded as a reference and its presentation values are
   NEVER copied onto the member; `in=` is recorded on the member as
   `node.group` and never as a child list on the group. No directive's
   attributes flow to any other line. The language has exactly one scope,
   rule 5. (`plane=` was the third example here, recorded on the element
   and never distributed from the `plane` line, until `PAINT-ORDER-CONSTRUCT` withdrew both
   halves.)

5. **Typed-block child scope is sticky.** A child directive attaches to
   the nearest preceding block of a kind that accepts it: `field` and
   `break` to a `bitfield`; `|` rows, `cell` and `width` to a `table`;
   `signal` and `gap` to a `timing` block. There is no `end` keyword — **any
   top-level directive closes the open block**, so a `field` after a
   `node` line is the error `"field" is a typed-block child — it needs a
   bitfield/table/timing block above it`. A child whose kind the open
   block does not accept is likewise a line error. Children appear as
   arrays inside their block in document order. (§4.)

6. **Document order, and the `line` field.** Every array is in document
   order. `pins` is keyed by id in the engine and is
   emitted **sorted by source line**, which is the same thing.
   **Document order is paint order** — a later line paints on top — and
   that holds without qualification, because there is
   one plane and no `z` to consult (§2.4, `PAINT-ORDER-CONSTRUCT`).

   Every element carries `line`, its **1-based source line number**, and
   that is what makes global document order recoverable across the
   per-kind arrays. Three element kinds carry no `line` because the
   engine records none — the implicit `plane`, timing `signal`, and a
   table *Cell* — and for those, array position is the only order
   information. The `plane` case is now trivial rather than a genuine
   asymmetry: there is exactly one, it is never declared, and no source
   line could carry its number. That
   asymmetry is a v0.2 candidate; it is recorded here because a second
   implementation must reproduce it to byte-compare.

   `line` counts physical source lines from 1, including comment and
   blank lines and the insignificant lines that may precede the header
   (§1). It is the same number the error model reports (§8), which is
   what makes the write→validate→fix loop work.

7. **Values are recorded as written.** Colours are NOT normalized: `#0d9`,
   `#0d9488`, `teal` and `transparent` are stored verbatim, because §5
   accepts all four spellings and a consumer that needs a canonical form
   can compute one. Percentages lose only their `%` sign
   (`threshold offset=15%` → `15`); `at=` and `cell`
   coordinates keep their sign and any fraction. Each of those two is a
   PAREN point in the source (`at=(x,y)`, `cell (r,c)`) and a pair of
   numbers in the model — the parens are the surface's way of saying "one
   composite value", and the model has no punctuation to carry.
   String escapes are RESOLVED at parse time **inside quoted-string
   tokens** (§1) — `\n` becomes a real line break in the stored string,
   `\"` a quote, `\\` a backslash — so the model holds the author's text,
   not its source spelling. **Pipe-row cells are not quoted strings**:
   their only escapes are `\|` and `\^^`, and the sole in-cell line break
   is the GFM-style HTML form `<br>` / `<br/>` / `<br />`, normalized to
   U+000A. A backslash-`n` pair in a cell is two literal
   characters.

8. **Nothing about the document is inferred.** There is no layout in the
   model, no id generation, no de-duplication, no reordering, and no
   completion of missing declarations: an id that names nothing is a
   line error (§8), never a synthesized element.

### 12.5 The canonical JSON binding

`NNN-name.model.json` as produced by
[conformance/normalize.js](../conformance/normalize.js) is the
**normative serialization** of the model. It is what a second
implementation is compared against, byte for byte.

**Two contracts, and they answer different questions.**
[spec/figdown-model.schema.json](figdown-model.schema.json) is the
**SHAPE** contract — a JSON Schema (2020-12) stating what keys exist on
each element, which are always present, and what each value's domain is.
It is the machine-readable form of §12.2's tables and it is CLOSED:
`additionalProperties: false` everywhere, so an unknown key is an error
and a new engine field fails the gate until the schema and §12.2 are
edited together (§8's closed-grammar rule, applied to the model). The
rest of §12.5 is the **BYTE** contract — key order, indentation,
escaping, the trailing newline — and no JSON Schema can state it. The two
do not substitute for each other: a model the schema accepts may still
fail byte comparison, and a model that fails the schema is wrong under
§12.2 whatever its bytes are. The schema does not state **referential
integrity** either — that `a`/`b`/`in`/`group`/`class` name something the
document declares, that ids are unique, that a genre admits the keywords
used — because those are PARSE-time rules (§8) whose violation produces
an error set and no model at all. `npm run gate:schema`
([tools/schema-check.js](../tools/schema-check.js), the 30th gate) checks
the schema against every `.model.json` golden in `conformance/cases/` and
`conformance/experimental/`, against every published figure in
`examples/` and `figures/` projected through the same `normalize.js`, and
against five negative controls that must be REJECTED for a recorded
reason — because a schema that accepts everything passes the first two
perfectly.

- **Encoding**: UTF-8, no BOM. Comparison is of **UTF-8 bytes**.
- **Serialization**: `JSON.stringify(model, null, 2)` — two-space
  indentation — followed by exactly one trailing newline (LF). A second
  implementation must match its output bytes, not invent an equivalent
  JSON text. **That call is a REFERENCE, no longer the statement**: the
  same bytes are specified without reference to any language in §12.5.1,
  and an implementer in a language that has no `JSON.stringify` reads
  that rather than emulating this one.
- **Key names**: exactly the names of §12.2. They are the model's
  vocabulary and are versioned like the grammar: renaming one is a
  migration entry (`template` → `genre`, 0.1, is the precedent).
- **Key order**: the emission order of §12.2, top level and per element.
  JSON objects are unordered in principle, but the goldens are compared
  as bytes, so key order is part of the binding. **`note` emits LAST among
  an element's content keys, immediately before `line`** — on *Node*,
  *Group* and *Edge* alike — and at the top level directly after `title`
  (`DRAWN-ANNOTATION-FORM`). Those are the positions §12.2 lists it in, and both
  follow one rule: a note is an **aside**, so it sits after everything the
  element declares about itself and before the source address that is not
  content at all; at the top level it sits beside the only other
  figure-wide authored text there is. Appending rather than interleaving
  also means no `figdown 0.1` or `figdown 0.2` golden moves a byte, since
  none of them can carry the key.
  **The five `sequence` collections follow the same rule at the top level**
  (`SEQUENCE-GENRE-VOCABULARY`): `lifelines` `messages` `states` `fragments`
  `operands` emit in that order, AFTER `regions`, and each is omitted when
  empty. Same reason again — no document below `figdown 0.4` can carry one,
  so no earlier golden moves — and it makes the top-level key order read as
  history: the v0.1 shape first, then what each later version added, in the
  order it was added.
- **Array order**: document order, per §12.4 rule 6.
- **Strings (the numeric-value rule = U)**: the model holds the author's
  text after source escapes are resolved (§12.4 rule 7) — real newlines,
  real quotes, real backslashes, and Unicode scalar values as written in
  the `.fd` (no NFC/NFD normalization). In the canonical JSON file:
  - non-ASCII code points appear as **raw UTF-8 characters** (e.g.
    `"title": "中文"`), not as `\uXXXX` escapes;
  - only the JSON-required escapes apply (`"`, `\`, and control
    characters such as a real U+000A becoming the two-character sequence
    `\n` in the JSON text);
  - requiring or preferring `\uXXXX` for non-ASCII is **not** conforming
    at the golden tier.
  Fixture `114-lex-utf8-strings` pins this.
- **Numbers**: plain JSON numbers as produced by `JSON.stringify` of an
  ECMAScript Number (the same binding as the reference
  `normalize.js`). Integers serialize without a decimal point;
  fractional values use ordinary JSON number syntax (so a source
  `1.10` is the number 1.1 and serializes as `1.1` — there is no
  “fractions as written” preservation of trailing zeros or of the
  source token spelling). Scientific notation is not accepted in the
  wire grammar for `pin` (the numeric-value rule value grammar); when a
  number is in the model it still serializes as a plain JSON number.
  A `null` where a number belongs is a defect (§12.3).
- **Absent keys**: omitted, never `null` (§12.3).
- **Nesting**: exactly as §12.2 — typed-block children nest inside their
  block, table cells inside `heads`/`rows`.
  The model is otherwise flat (§12.4 rule 4).

**The top-level shape, and the `sections` wrapper.** §12.2
describes the model of ONE section. A `.fd` file MAY hold more than one
(§1, `MULTI-FIGURE-DOCUMENTS`), so the canonical JSON has two top-level shapes and the section
COUNT — not the presence of a second `figdown` line, not the genre —
decides which:

- **One section**: the top level IS the §12.2 *Document* object. `header`
  is its first key. There is no wrapper. This is the shape of 62 of the
  65 valid normative fixtures.
- **Two or more sections**: the top level is an object with **exactly one
  key**, `sections`, whose value is an array of §12.2 *Document* objects,
  one per section. Nothing else appears at the top level — there is no
  document-wide `header`, `title` or count key, because there is no
  document-wide genre to put in one (each section declares its own).

| | one section | two or more |
|---|---|---|
| top-level keys | the §12.2 *Document* keys, in §12.2 order | `sections`, and only `sections` |
| where `header` lives | top level | `sections[i].header` |

- **`sections` array order** is **document order**: `sections[0]` is the
  section opened by the first `figdown` line in the file, `sections[1]` by
  the second, and so on. It is the same rule as every other array in the
  model (§12.4 rule 6), applied one level up.
- **Key order inside each element** is unchanged — the §12.2 emission
  order. The wrapper adds a level; it renames and reorders nothing.
- A section's `line` numbers are **file-wide**, not section-relative: they
  count from the first line of the `.fd`, so a `line` value is a valid
  address into the source no matter which section it came from.
- **A one-section file never takes the wrapper**, not even as an array of
  one. Two spellings for one document would defeat byte comparison, which
  is the whole mechanism of this tier.

Pinned by `013-header-duplicate`, `019-multi-section-stack` and
`020-multi-section-id-reuse`. The first two were the wrapper's ONLY
statement in the project until 0.1 — it lived in a comment in
`conformance/run.js`, in no normative document and not even in
`conformance/normalize.js`, so an implementer could emit the wrong
top-level shape with no way to have known.

Comparison is byte equality of the whole file. A second implementation
conforms at this tier when, for every NORMATIVE case in
`conformance/cases/`, it produces a byte-identical `.model.json` — see
[conformance/README.md](../conformance/README.md) for the recipe and for
`DISCREPANCIES.md`, which lists the places where the goldens freeze an
audited engine-vs-spec conflict. Which cases are normative is settled by
LOCATION: every fixture in `conformance/cases/` is normative,
and every fixture in `conformance/experimental/` is not — those are the ones
whose subject `CONSTRUCT-STATUS-TIERS` placed outside the conformance surface (an experimental
genre, or a demoted keyword), and `conformance/STATUS.txt` records the reason
for each. They still run and still pass; they simply carry no obligation.

#### 12.5.1 The serialization algorithm, stated without an implementation

Everything above binds the canonical bytes to a JavaScript call. This
subsection states the same bytes as an algorithm, so that an implementer
in a language whose JSON writer sorts keys, escapes non-ASCII, or omits
the trailing newline knows exactly what to override. **It adds no rule and
changes no byte**; where it and `JSON.stringify(model, null, 2)` could
disagree, the goldens decide and this text is the defect.

Write the *Document* object of §12.2 (or the `sections` wrapper) as UTF-8,
no BOM, LF line endings, with:

1. **Key order is the EMISSION order of §12.2 — never sorted.** Top level
   and per element. This is a deliberate choice against lexicographic
   order; §12.5.2 records why.
2. **Array order is document order** (§12.4 rule 6).
3. **Absent keys are omitted, never `null`** (§12.3). A `null` anywhere in
   a canonical model is a defect, never a value.
4. **Indentation.** A non-empty object is `{`, LF, then one line per
   member indented by **two spaces per nesting level**, members separated
   by `,` at the end of the line, then LF and the closing `}` indented by
   the level of the object's own line. Arrays are the same with `[` `]`.
   A member line is `"key": value` — the key in double quotes, a colon,
   **exactly one space**, the value. An **empty** object is the two bytes
   `{}` and an empty array the two bytes `[]`, with no interior
   whitespace and no line break. No trailing whitespace on any line.
5. **Strings.** Escape `"` as `\"` and `\` as `\\`. Escape U+0008, U+0009,
   U+000A, U+000C and U+000D as `\b`, `\t`, `\n`, `\f`, `\r`. Escape every
   other code point below U+0020 as `\u00XX` with **lowercase** hex
   digits. Emit **every other code point as raw UTF-8** — including
   U+007F and everything above U+00FF (`"title": "中文"`, never
   `中文`). Do **not** escape `/`. Apply no Unicode normalization:
   the model holds the author's scalar values as the `.fd` wrote them
   (§12.4 rule 7). Fixture `114-lex-utf8-strings` pins this.
6. **Numbers** are serialized by the ECMA-262 `Number::toString` production
   — the shortest decimal that round-trips to the same double. An integral
   value carries no decimal point and no exponent (`20`, not `20.0` or
   `2e1`); a fractional value uses ordinary JSON number syntax (source
   `1.10` is the number 1.1 and serializes as `1.1`, so trailing zeros and
   the source token's spelling are not preserved); negative zero
   serializes as `0`. The exponent forms of that production
   (`1e+21`, `1e-7`) are unreachable from this language's value grammars
   but are part of the rule, not an exception to it. `NaN` and the
   infinities cannot occur: a value that is not a finite number is an
   engine defect (§12.3).
7. **Terminate the file with exactly one LF** after the closing brace.

Two edge cases are named so an implementer does not have to guess whether
the reference call has a rule they are missing. **Lone surrogates**: the
reference call would escape one as `\uXXXX`, and the case is unreachable
here because a `.fd` source is UTF-8 and cannot encode one. **Duplicate
keys**: impossible by construction — the model is built from §12.2's fixed
key list, so no object is ever offered the same key twice.

#### 12.5.2 RFC 8785 (JCS) — evaluated, and DECLINED for this binding

[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html), the JSON
Canonicalization Scheme (Independent Submission, Informational, June
2020), is the obvious candidate for "canonical JSON" and it was evaluated
against §12.5 clause by clause. **The verdict is DECLINE**, and the
interesting part is that the disagreement is narrow.

| | JCS | §12.5 | |
|---|---|---|---|
| numbers | ECMA-262 `Number::toString` (§3.2.2.3) | ECMA-262 `Number::toString` (§12.5.1 rule 6) | **agree** |
| strings | minimal escaping, raw UTF-8 above U+001F (§3.2.2.2) | the same (§12.5.1 rule 5) | **agree** |
| encoding | UTF-8 (§3.2.4) | UTF-8, no BOM | **agree** |
| key order | sorted by UTF-16 code unit (§3.2.3) | §12.2 emission order | **conflict** |
| whitespace | none between tokens (§3.2.1) | two-space indentation, one member per line | **conflict** |
| file end | no trailing newline | exactly one LF | **conflict** |

**Grounds for declining.** JCS's two substantive rules are the two this
project chose against on purpose, and both choices are load-bearing:

- **Key order.** A sorted model reads alphabetically, which is an order
  nothing in the language means. §12.2's order is the order a *reader*
  needs — `id`, then what the element declares about itself, then `note`
  as the aside, then `line`, which is the source address and not content
  at all — and at the top level it reads as history: the v0.1 shape, then
  what each later version appended (`DRAWN-ANNOTATION-FORM`'s `note`, `SEQUENCE-GENRE-VOCABULARY`'s five
  `sequence` collections). Sorting would also DESTROY that appending
  property: under JCS, adding `note` reorders keys inside every element
  that can carry one, so goldens written before the key existed would move
  bytes, and §13's whole "no earlier golden moves" discipline would have
  nothing to stand on.
- **Whitespace.** A golden is REVIEWED. Every model in
  `conformance/cases/` is read by a human in a diff before it is frozen,
  and the model is the conformance surface precisely because a person can
  check it. One-line JCS output would make a one-key change an unreadable
  whole-file diff, which is a real cost paid for a benefit — invariance
  under a JSON parse/re-emit round trip — this tier does not need, because
  the golden is compared as **bytes** and never round-tripped.

**What DECLINE does not mean.** It is declined as *this* binding, not as a
tool. §12.5 is a human-reviewed comparison tier; a **hash** is not read by
anyone, and the objections above are objections about reading. If the
semantic projection and `semantic_sha256` of §9 `PUBLICATION-MANIFEST-PROFILE` land, JCS is the
right shape for that input and it should be evaluated on its own terms
there — a hash over a canonical form that a third party can reproduce
without this repository's key-order table is worth more than one over a
form only this spec defines. That question is open and is not settled
here; what is settled is that the *golden* tier keeps the readable order.

**Worked example.** This document — deliberately written on the FROZEN
surface only, so that the model below needs nothing from
[experimental.md](experimental.md) to be read (0.1; it declared a
EXPERIMENTAL `plane` until then, and no document can — `PAINT-ORDER-CONSTRUCT`):

```figdown
# provenance: hand-authored
figdown 0.1 block
title "Lookup path"
class hot "Hot path" stroke=#dc2626
node parse "Parser"
node lookup
external wire "to wire"
edge lookup <-[hit]- parse class=hot
edge lookup -> wire
layout
pin parse at=(20,20)
```

has exactly this model:

```json
{
  "header": {
    "version": "0.1",
    "genre": "block"
  },
  "title": "Lookup path",
  "flow": "right",
  "classes": [
    {
      "id": "hot",
      "meaning": "Hot path",
      "stroke": "#dc2626",
      "line": 4
    }
  ],
  "planes": [
    {
      "id": "base",
      "z": 0
    }
  ],
  "nodes": [
    {
      "id": "parse",
      "label": "Parser",
      "shape": "box",
      "plane": "base",
      "line": 5
    },
    {
      "id": "lookup",
      "shape": "box",
      "plane": "base",
      "line": 6
    }
  ],
  "groups": [],
  "externals": [
    {
      "id": "wire",
      "label": "to wire",
      "line": 7
    }
  ],
  "edges": [
    {
      "a": "lookup",
      "op": "<-",
      "b": "parse",
      "mid": "hit",
      "plane": "base",
      "class": [
        "hot"
      ],
      "line": 8
    },
    {
      "a": "lookup",
      "op": "->",
      "b": "wire",
      "plane": "base",
      "line": 9
    }
  ],
  "ranks": [],
  "pins": [
    {
      "id": "parse",
      "x": 20,
      "y": 20,
      "line": 11
    }
  ],
  "thresholds": [],
  "bands": [],
  "bundles": [],
  "regions": []
}
```

Read it against the rules. The comment above the header is insignificant
(§1) but still counts a source line, so `class` is `line: 4`. `flow` is
materialized to `"right"` although no `flow` line exists. `planes` is
present and holds exactly the implicit `base` at `z: 0` — that is what EVERY document looks like, not merely one on the
frozen surface, and every element still reports `plane: "base"`. `node lookup` has **no**
`label` key — the renderer will draw
"lookup", the model will not claim the author wrote it. The first edge
keeps `op: "<-"` with `a: "lookup"`, `b: "parse"`: the relationship runs
parse→lookup, derived from the operator, and the endpoint order is the
author's. `class` is a reference and is an ARRAY, `["hot"]`, because
`class=` is multi-valued — `#dc2626` appears once, on the
class, and is never copied onto the edge. `groups`, `ranks`,
`thresholds`, `bands`, `bundles` and `regions` are present and empty. The
`layout` line contributes
nothing of its own; the `pin` after it lands in `pins`.

*Two defects in the previous edition of this example were found and fixed
when it was rewritten, and both were the same kind: the JSON
had been hand-maintained and had drifted from the engine.* It showed
`"fill": "#dc2626"` on a class whose source line writes `stroke=`, and it
showed `"class": "hot"` as a bare string where the engine emits the array
`["hot"]`. The example is now generated from the engine through
`conformance/normalize.js`, which is the same projection the goldens use.

### 12.6 What the model deliberately does not carry

The model is what survives stripping the layout zone (`GUI-WRITEBACK-STRUCTURE`'s strip test)
and removing every presentation-only attribute (§5's
presentation-ignorable invariant, `PRESENTATION-AS-MEANING-CARRIER`). Everything a renderer computes
is outside it:

- **Geometry** — computed positions and sizes, canvas dimensions,
  border anchor points, edge polylines, elbow corners, label placement,
  text metrics, font sizes. `pin` is in the model as
  the author's *declared constraints*, not as results; nothing else
  geometric is.
- **Resolved presentation** — the colours, strokes and dash styles a
  `class` supplies to its members. The model records `class: ["hot"]` and
  the class's own values, once. A consumer that wants the effective
  colour computes it; the model never bakes it in, because doing so
  would make an inherited value indistinguishable from a written one
  (§12.4 rule 4).
- **Layout results** — rank assignment, obstacle detours, the derived
  `bundle` ring, the derived legend strip, external-endpoint anchor
  placement.
  Each of these is a drawing convention the renderer owns (`DOMAIN-CONVENTION-DIRECTIVES`); the
  model holds only the semantic declaration that triggers it.
- **Derived genre readings** — the bitfield's per-field bit ranges and
  bit numbers, the table's expanded logical grid, the timing figure's per-cycle
  values. Each is a deterministic function of keys the model already
  carries, and §12.7 licenses the reader to compute it. Recording the
  result instead would put a computed value beside the written ones with
  nothing to tell them apart — the same objection that keeps resolved
  `class` presentation out (§12.4 rule 4) — and for `bitfield` there is
  often no single value to record at all: a field carrying `present=`
  branches the answer (`BITFIELD-CONDITIONAL-OFFSETS`), a `*` field leaves it indeterminate, and a
  repeated element whose range is not fully literal leaves every later
  offset indeterminate (§12.7). `DECLARATION-ORDER-SEMANTICS`, 0.1; the `index=` half of
  that last clause is 0.1, `BITFIELD-REPETITION-CONSTRUCT` — a fully-literal `index=` range
  makes the later offsets computable again, and the model still records
  the range rather than the computed offsets.
- **Rendered artifact facts** — the SVG, its embedded source copy and
  SHA-256, and any `data-render-options` (§7). The model is a function
  of the source; the artifact is a function of (source, options).
- **Anything not written down.** No inferred grouping, no inferred
  ordering, no inferred identity between elements (`IDENTITY-ASSERTION`).

The test to apply is `GUI-WRITEBACK-STRUCTURE`'s: delete every `pin` line
and the `layout` opener, then delete every `fill=`,
`stroke=`, `style=` and `gap=`. What is left MUST parse,
MUST still render under auto layout, and MUST express the identical
structure and relationships. What that stripped document says is the
model's subject matter; `tools/strip-check.js` runs the first half of the
test mechanically.

### 12.7 The reading-agent contract

An agent reading a model MAY conclude:

- **Participants and relationships.** `nodes` are the figure's
  participants; `edges` are relationships between them, with direction
  derived from `op` (§12.4 rule 3). An `external` is NOT a participant —
  it states only that the connection crosses the figure's boundary
  (§2.8).
- **Containment** from `node.group`; **rank co-membership** from `ranks`;
  **bundle membership** from `bundles[].members`.
- **Category** from a `class` reference plus that class's `meaning` — the
  declared, machine-readable mapping colour alone never provides (§2.7,
  `CATEGORICAL-MEANING-MAPPING`). **A class whose `meaning` is `""` asserts NO category** (
  `CLASS-EMPTY-MEANING`): the author declared the class as attribute grouping and claimed
  nothing, so there is nothing to conclude and an agent MUST NOT invent one
  from the id or from the shared presentation. Such a class draws no legend
  entry either, so the two readers agree — which is the point.
- **Genre-specific readings** from the genre documents, which are
  normative and self-contained (`GENRE-DOCUMENT-CONTRACT`): bitfield offsets as the cumulative
  sum of declared widths with `present=` branching (`BITFIELD-CONDITIONAL-OFFSETS`), the table's
  logical grid including merges, timing cycles as lane character positions.
- **Bitfield bit numbers (`DECLARATION-ORDER-SEMANTICS`).** Until then this list said
  "offsets" and nothing about `numbering=`, and the rule below — "MAY rely
  on §12.7 and MUST NOT infer more" — therefore FORBADE a conforming
  reader to derive a bit number at all. That made the one REQUIRED option
  in the language (`numbering=`, `UNSAFE-DEFAULT-ELIMINATION`) invisible to the reader it exists
  for, while the human reading the same figure's ruler got an answer. What
  is licensed, and no more:
  - a field's **drawing span** follows from declaration order and the
    declared widths: fields fill a word left to right in the order
    written (**`DECLARATION-ORDER-SEMANTICS`**, §1), wrapping to the next word when it is full, and a
    `break` advances the cursor to the start of the next word. This gives
    each field a row `⌊p / word⌋` and a column `p mod word`;
  - a field's **bit numbers** follow by relabelling those columns with
    `numbering=`: under `msb0` the bit number of column `c` is `c`, so
    bit numbers ascend left to right; under `lsb0` it is `word − 1 − c`,
    so they DESCEND left to right and the highest bit number is at the
    left. The geometry is the same under both values; only the numbers
    change;
  - the derivation is **deterministic** given `word`, `numbering` and the
    widths, all of which the model carries (§12.2). It is therefore
    DERIVED, not inferred, and rule 8 of §12.4 — which bars inference —
    does not reach it;
  - the model does **not** materialize the result, by decision (`DECLARATION-ORDER-SEMANTICS`):
    `present=` makes the answer branched (`BITFIELD-CONDITIONAL-OFFSETS`), `*` makes it
    indeterminate, and a repeated element whose range is not fully
    literal makes every later offset
    indeterminate, so there is no single value to record for the cases
    that most need one, and §12.4 rule 1's materialized-defaults list
    stays what it says it is — things the author left to the language,
    never content computed from what the author wrote. A reader computes
    the ranges; [genres/bitfield.md](genres/bitfield.md)'s semantic model
    is the normative arithmetic and
    `conformance/cases/418-bitfield-bit-number-derivation` is the worked
    example;
  - what MUST NOT be concluded is unchanged: the drawn width of a `*`
    field is not its length, offsets across a field carrying `present=`
    MUST be branched or stated as assumptions, and nothing may be inferred
    from drawing geometry (`MEANING-RECOVERY-SOURCE`).
- **Authored documentation prose (`description=`, 0.1, `DESCRIPTION-KEY-SPELLING`).** A
  `field`'s `description` is authored documentation prose — **quotable and
  displayable, never parsable**. Until 0.1 the key was spelled
  `note` and §12.7 did not list it at all, so a conforming reader was
  forbidden to conclude anything from it: authored content, in the content
  zone, silently discarded. What is licensed is exactly this — quote it,
  display it, attribute it to its field — and no more. It carries no
  structure, no relation and no condition; an agent MUST NOT parse it,
  and MUST NOT read a presence condition out of it (that is what
  `present=` is for, and even there the value is unparsable).
- **The drawn annotation (`note=`, 0.3, `DRAWN-ANNOTATION-FORM`).** A `note` is
  authored documentation prose — **quotable and displayable, never
  parsable**, the `description=` rule above applied to a channel that
  paints ink. It is an **aside**: its content is commentary about the
  figure, not a fact about the figure's structure, and the model records
  the string and nothing else. What is licensed is exactly this: a reader
  **MAY** attribute a `note` to the element that carries it — `node.note`
  is about that node, `edge.note` about that connector, `group.note` about
  that group, and **`doc.note`** (from a `note=` on the `title` line)
  about the figure as a whole — and a reader **MUST NOT** infer a
  participant, an edge, a containment, a category or any other structure
  from it. A note naming a node that is not declared does not declare it;
  a note describing a relation does not create an edge; a note asserting
  a kind is not a `class` meaning, which is the one machine-readable
  category channel (§2.7, `CATEGORICAL-MEANING-MAPPING`). The division that makes this rule coherent
  is stated in §10: `description=` addresses the machine and draws
  nothing, `note=` addresses the human and always draws, and **neither is
  a fallback for the other** — so an agent that finds a `note` where it
  expected a `description` has found a different channel, not a
  substitute, and may not read the second obligation out of the first.

- **Absence.** No `label` key means the author wrote no label, and
  `label: ""` means the author wrote an empty one deliberately — two
  different facts, both recoverable (§12.3).
- **Thresholds and bands (EXPERIMENTAL, §2.6).** Until 0.1 this
  list did not mention them at all, and the rule below — "MAY rely on
  §12.7 and MUST NOT infer more" — therefore FORBADE a conforming reader
  to conclude anything from `threshold "fHIGH = PAUSE turn-on threshold"`:
  authored content, in the content zone, silently discarded with no
  warning. What is licensed, and no more:
  - a `threshold` asserts that the value named by its **label** is a
    reference value on its `in=` target;
  - **the relative ordering of two thresholds on the same target, by
    `offset`, IS knowledge**, not layout. This is an explicit exception to
    the "Arrangement is not precedence" bullet below, which would
    otherwise discard it: the corpus states the fact verbatim
    ("Thresholds ordered from highest to lowest on the … axis"), so
    without the licence the document loses it;
  - `offset=` is a **fraction of the target's rendered extent, not a value
    of any quantity**. The target declares no scale, so no conversion
    exists, and an agent MUST NOT report it as a data value. If a
    quantity is wanted, it is in the label or it is not in the document
    — and if a distinction exists only in the fraction, the document has
    lost it, and the agent SHOULD say so;
  - a `band` asserts that the region named by its **label** spans that
    fraction of the target. Before 0.1 a band had no label and
    therefore asserted nothing at all (§5, `BAND-LABEL-STATUS`);
  - both constructs are EXPERIMENTAL and outside the compatibility
    promise (§10, `CONSTRUCT-STATUS-TIERS`), so a reader that needs a portable conclusion MUST
    treat their presence as it treats any EXPERIMENTAL construct.

**Conditional presence (`bitfield`, 0.1, `PRESENCE-CONDITION-EXPRESSION`).** A `field` carrying
`present=` MAY be absent from the encoded data; a reader computing offsets or
bit numbers across it MUST branch (`BITFIELD-CONDITIONAL-OFFSETS`). The VALUE is authored prose about
that field's presence condition and nothing more. An agent MAY quote or
display it verbatim; it **MUST NOT parse it**, **MUST NOT evaluate it**,
**MUST NOT resolve any name inside it** against a field name, a `class` id or
any other element of the document, and **MUST NOT derive a presence decision
from it**. `present=""` asserts conditional presence with **no condition
stated** — the author claimed nothing, and an agent MUST NOT invent one from
the field's name, its `class`, its `description=` or its neighbours. An
**absent** `present=` key means the author wrote no presence claim; it is
**NOT** an assertion that the field is always present.

**Repetition (`bitfield`, 0.1, `BITFIELD-REPETITION-CONSTRUCT`).** An agent **MUST NOT** assume
that a declared field list enumerates every occurrence of a repeated element,
and where a figure indicates repetition it **MUST** treat all later offsets as
indeterminate. §12.7 already warns a reader off a `*` field's drawn width and
forces branching across a conditional field — the two cases where the model
cannot give one answer. A **repeated** element is the third, and it is the
worst of them: a reader computing offsets across
`field "Segment List[0]" 128` … `field "Segment List[n]" 128` derives a
**confidently wrong** number with nothing attached to warn it. Every other gap
in this section degrades to "unknown"; this one degrades to *wrong*, which is
why it is stated as a MUST NOT rather than left to the genre document.
Indications of repetition include an index or range in a field label
(`[0]`, `[n]`, `1..n`), a declared count field naming the repeated section
(DNS's QDCOUNT/ANCOUNT/NSCOUNT/ARCOUNT), and a terminator condition stated in
prose (MPLS's S bit).

**`index=` is the exception, and it is the only way out of that MUST NOT
(`BITFIELD-REPETITION-CONSTRUCT`).** Where `index=` is written, the base **is
machine-readable and may be stated**:

- **both ends literal** ⇒ the run is DETERMINATE. Its element count is
  `|last − first| + 1`, the field contributes width × count to the declared
  bit sequence, and **every later field's declared offset becomes determinate
  again** — the arithmetic the MUST NOT above otherwise destroys;
- **one end prose**, or `index=""` ⇒ later offsets stay indeterminate exactly
  as before. What changed is not the conclusion but how it is REACHED: from
  **syntax**, rather than from a reader inspecting label text;
- the value is **never parsed, evaluated or resolved**, verbatim from the
  `present=` contract above. A prose `<last>` may be quoted and displayed and
  nothing more; an agent MUST NOT resolve it against a field name, a `class`
  id or anything else in the document (§9 `ANNOTATION-LOCATOR-SPLIT`);
- **determinacy is decided by the model's TYPES, never by prose** — `last` a
  number ⇒ determinate, `last` a string ⇒ not (§12.2). An agent that needs to
  regex the author's text to answer this question is doing something the
  model was shaped to make unnecessary;
- the drawing shows at most TWO occurrences — the first element, the elision
  mark, and the last (`REPEATED-RUN-DRAWING`), or ONE alone when `index=""` states
  no index — whatever the count. Two drawn boxes are never two elements' worth
  of bits: a reader derives the count from `index=` and never from the number
  of drawn rows (`MEANING-RECOVERY-SOURCE`);
- **`step` is RESERVED inside the range and is a line error** (
  `RULE-POSITION-ENUMERATION`). The trigger is a bare lowercase `step` token in the prose `<last>`:
  `index="0..7 step 2"`, `index="0..N step 2"` and `index="0..end step"` are
  line errors; `index="0..7 Step 2"`, `index="0..7 in steps of 2"` and
  `index="0..stepping"` are not, because only the exact spelling a stepped
  range would use can ever be ambiguous with it. The reservation exists so
  that shipping the stepped range in v0.2 (§9 `INDEX-RANGE-STEP`) cannot change what a
  document already written MEANS.

**`BITFIELD-REPETITION-CONSTRUCT`'s MUST NOT stands, and it UNDER-FIRES — recorded, because it is this
section's only prose-parsing reading rule.** The rule tells a reader to look
for indications of repetition in field LABELS, and one of its own three cited
files does not put the indication there. In `examples/mpls.fd` the only
statement that the label stack repeats is in a `description=` — and the
`description=` paragraph above forbids parsing that. So the MUST NOT is
written to fire on a signal the same section refuses a reader permission to
read, and on that file it therefore does not fire at all. `index=` is the
answer to this as much as to the arithmetic: a key that a reader may consult
does not depend on where an author happened to put a sentence.
An agent MUST NOT infer:

- **Arrangement is not precedence.** Array order and `line` record the
  author's statement order, which is a focus and reading-order signal,
  not a ranking, a priority or a sequence. Where an arrangement really is
  knowledge the language has no construct yet (`MEANINGFUL-ARRANGEMENT`) — the layout
  namespace is not that construct. A reading agent's DEFAULT is to ignore
  every member of the layout namespace — enumerated in §10 (a′) — wherever
  in the document it appears (§3, `GENRE-NAMESPACE`); membership decides, not
  position, so a `pin` written before the `layout` opener is ignored
  exactly like one written after it. `pins` is
  in the model as the author's declared geometry constraints, and
  carries no meaning. The one exception is the missing-construct workaround
  of §3: when a document's layout looks load-bearing, read it and say
  that you did.
- **`flow` is a reading axis, not a sequence.** It states the direction
  the figure is meant to be read; it does not order the nodes and it
  asserts nothing about time or causality. There is no construct in v0.1
  asserting relative order between edges (see the sequence-genre note in
  §9).
- **A keyword's meaning is relative to its genre.** `header.genre` names
  the namespace of the document's top-level lines, its defaults, and its
  validation profile (§1, `GENRE-NAMESPACE` `GENRE-NAMESPACE`). An agent MUST NOT carry a reading from
  one genre into another: a genre MAY own keywords and MAY spell one the
  same as another genre's with a different meaning and different defaults
  (`GENRE-VOCABULARY-OBLIGATION`), so a directive is only defined relative to the genre whose
  namespace it sits in. A nested region (`bitfield`, `table`, `timing`) is
  read under ITS genre, never the host's (`GENRE-COMPOSITION`, §4). (`GENRE-KEYWORD-ALLOWLIST`)
  the three pure genres reject scene top-level keywords, while the three
  scene genres still share one scene vocabulary — so a keyword's
  *legality* already depends on the section genre even when its *meaning*
  does not yet diverge (`GENRE-VOCABULARY-OBLIGATION` still unused). Multi-section files give each
  section its own genre allowlist. **Two sets are the exception an agent may
  rely on permanently.** The core (`UNIVERSAL-CORE-KEYWORDS`) — `figdown`, `title`, `layout` —
  means the same thing under every genre, and so does **every keyword of
  the layout namespace** (`LAYOUT-ZONE-NAMESPACE`): that is `pin`,
  and nothing else.
  Together those are what let an agent ignore the layout namespace (§3)
  without first knowing the genre: `LAYOUT-ZONE-NAMESPACE` guarantees that the one enumeration
  in §10 (a′) is correct under every genre and that nothing in it can be
  a genre's own semantics, and `layout` marks where authors conventionally
  collect the members.
  **Status does not qualify this**, and the pair that made the point is
  the proof: `path` and `routing` were EXPERIMENTAL (`CONSTRUCT-STATUS-TIERS`)
  and genre-independent the whole time, and the status
  clause cashed out — `EDGE-GEOMETRY-CONSTRUCTS` **withdrew** both from the language (§10).
  Stability and belonging are different questions, and an agent that needs
  a portable figure must consult §10's status column rather than assume a
  construct's presence is a promise. Until 0.1
  this clause said an agent "must not assume their meaning is
  genre-independent"; that reading is **withdrawn** (`LAYOUT-ZONE-NAMESPACE`).
- **Status is part of what a reader must carry (`CONSTRUCT-STATUS-TIERS`, §10).** Every
  keyword and option key is either NORMATIVE (NORMATIVE — in the conformance
  surface and the compatibility promise) or EXPERIMENTAL (EXPERIMENTAL — the
  engine accepts it, but it may change or be withdrawn in a later `0.x`
  without a migration entry). The parser emits no warning for an
  experimental construct, so an agent that needs a portable figure MUST
  consult §10's status column rather than infer status from the fact that
  a line parsed.
- **Presentation is not meaning on its own.** `fill`, `stroke`,
  `style`, `gap` and `z` MAY render meaning but MUST NOT be its only
  carrier (§5, `PRESENTATION-AS-MEANING-CARRIER`). An agent is entitled to discard them; if a
  distinction exists only there, the document has lost it, and the agent
  SHOULD say so rather than reconstruct it.
- **Identity.** Two elements sharing a `class` share a CATEGORY, not an
  identity; the language has no equivalence or alias relation (`IDENTITY-ASSERTION`).
- **Element ids are HANDLES (`CONNECTOR-IDENTITY-KEY`).** An id is what other constructs in the
  same section use to name this element, and what a diff pairs on. An agent
  MUST NOT read meaning out of an id's SPELLING — not a category, not an
  ordinal, not a relationship to a similarly-spelled id (§12.3; `CLASS-EMPTY-MEANING`'s rule
  for `class`, applied to every id). `edge lag_member_1` licenses the
  conclusion that some construct can refer to this connector, and licenses
  **none** of: that it is a LAG member, that it is member number 1, or that it
  is related to `lag_member_2` — the membership claim is made by `bundle` or it
  is not made. Two elements with similar ids are two elements; the language has
  no equivalence relation (`IDENTITY-ASSERTION`). And an element **without** an id is not
  thereby less real, less connected or less asserted: anonymity is the
  language's default and carries no claim at all. That last sentence is the one
  this bullet exists for — the moment some connectors are named, a reader is
  tempted to treat the named ones as the important ones.

#### 12.7.1 Four classes of conclusion, and where absence is not negation

The two lists above license some conclusions and forbid others. This section
names the **four classes** every conclusion falls into. It adds no key, no
keyword and no licence that was not already granted somewhere; it exists so
that a reader can say, of each sentence it writes about a figure, **which
class that sentence belongs to** — and so that the two classes that must be
declined are named rather than left to judgement.

**1. EXPLICIT — the author wrote it.** This node exists; this edge relates
these two; the label is this string; this `class` means this sentence; this
field is 16 bits wide; this cell holds this text. An EXPLICIT conclusion is
quotable back to a line of the source. `label: ""` is one of them: it says the
author deliberately wrote an empty label, which is a different fact from
having written none (§12.3).

**2. DERIVED — a stated reading rule licenses it.** Not written, but computed
from what is written, deterministically, by a rule that is itself written
down, and with no appeal to the drawing (`MEANING-RECOVERY-SOURCE`). **A licence always has an
address**, and a reader that cannot name the address is inferring rather than
deriving. The addresses, complete as of this version: §12.7's own list above
(edge direction from `op`, containment from `node.group`, category from
`class` plus its `meaning`, bit numbers from `word` / `numbering` / the
declared widths, relative threshold order from `offset`); the derived legend
(§2.7); and each genre document's normative **Semantic model** section —
[genres/bitfield.md](genres/bitfield.md) (declared offset, drawing position,
bit number), [genres/table.md](genres/table.md) (the logical grid, merges
included),
[genres/experimental/timing.md](genres/experimental/timing.md) (cycle *t* is
the *t*-th lane character),
[genres/experimental/statechart.md](genres/experimental/statechart.md)
(reading rules 1–5),
[genres/experimental/sequence.md](genres/experimental/sequence.md) (reading
rules 1–7). §12.6 says why the model records the inputs and never the computed
result.

**3. NOT-ASSERTED — the figure makes no claim, and makes no denial either.**

> **THE ABSENCE RULE.** **A relationship a figure does not draw is NOT a claim
> that the relationship does not exist.** Absence is omission, and an agent
> **MUST NOT** convert it into negation — not from a missing edge, not from a
> missing transition, not from an absent row, and not from an option key
> nobody wrote (§12.3 states the key-level twin: an absent `present=` is *no
> presence claim*, and never a claim of unconditional presence). **The one
> exception is a genre reading rule declaring a stated extent TOTAL, and a
> reader must be able to cite that rule by address.** §12.7.2 enumerates every
> such rule the language has; outside them the reading is open-world.

> **No genre asserts closed-world completeness about its SUBJECT.** Where a
> genre calls a set *closed as drawn* — `statechart` reading rule 5 for
> states, `sequence` reading rule 1 for participants — the sentence is about
> the FIGURE (these are the modes, or the participants, that this figure
> asserts; an undeclared one *is not asserted to exist*) and never about the
> subject (the machine has no other mode). Rule 5 carries the mirror clause in
> the same breath: *"Absence is still not prohibition — a transition not drawn
> is not stated to be forbidden."* **`STATECHART-TRANSITION-TOTALITY` settled the harder direction: a
> `statechart` cannot assert that its transition function is TOTAL**, and the
> ground is not a missing FigDown keyword but UML 2.5.1 §14.2.3.9.1, whose
> run-to-completion paradigm *discards* an event with no enabled transition
> and completes the step trivially — so the undrawn case and the
> drawn-and-deliberately-ignoring case are indistinguishable in any model,
> UML's own included. Generalised, and this is the sentence the rule exists
> for: **a drawn graph cannot assert the totality of its relation.** What an
> author writes instead is prose, attributed and never inferred — a `note=` on
> the element (`DRAWN-ANNOTATION-FORM`), a `class` meaning, or a companion `table` whose filled
> grid IS the enumeration the graph cannot make. The worked example is
> [../examples/statechart/bfd-session.fd](../examples/statechart/bfd-session.fd),
> which carries both halves and states in its own comments why the second one
> is there.

**4. UNKNOWN — the document does not settle it, and its silence is not
evidence either way.** NOT-ASSERTED is a fact about the FIGURE; UNKNOWN is the
reader's position with respect to the SUBJECT. The source behind the figure
may or may not carry the fact, and this document cannot tell you which. Two
shapes recur, and neither is a defect to be repaired by guessing:

- **opaque by design** — a channel that is quotable and never parsable, so the
  value is present and unresolvable: `description=` (`DESCRIPTION-KEY-SPELLING`), `note=` (`DRAWN-ANNOTATION-FORM`),
  `present=` and the prose end of `index=` (`PRESENCE-CONDITION-EXPRESSION`, `BITFIELD-REPETITION-CONSTRUCT`), a `statechart`
  `[mid]` inscription, a `sequence` `state` name;
- **indeterminate by construction** — a `*` field's length, every declared
  offset after a `*` or an undeclared repetition (`BITFIELD-REPETITION-CONSTRUCT`), and any offset across
  a conditional field that has not been branched (`BITFIELD-CONDITIONAL-OFFSETS`).

**The reader's obligation, and it is the same for both.** For a NOT-ASSERTED
and for an UNKNOWN question the correct answer is **NOT STATED** — with, where
it helps, one clause saying which of the two it is and why. A committed answer
to either is a hallucination and not a helpful guess. Every gap in this section
degrades to NOT STATED except one: `BITFIELD-REPETITION-CONSTRUCT`'s repetition arithmetic degrades to a
*confidently wrong number*, which is why it is written as a MUST NOT. **This is
measured, not merely asked for:** `tools/comprehension-check.js` scores two
refusal question classes — `unanswerable` (an in-scope fact of the subject the
figure honestly does not carry) and `forbidden-inference` (a conclusion a
reader is tempted to draw from a MISSING element) — in a denominator of their
own, where any committed answer counts as a hallucination
(decisions/registry.md).

This is the same discipline §15.5 applies to itself, where the security
chapter names the claims that resolve to *no gate* instead of leaving the
absence to be discovered: a contract that states what it does not cover is the
only kind a second implementation can meet.

#### 12.7.2 Where each genre sits

Open-world is the default and needs no rule. What needs a rule — and what the
absence rule of §12.7.1 requires a reader to cite by address — is a **stated
extent that is TOTAL**, where absence *inside* the extent is itself a fact.
This table is the complete enumeration.

| Genre | Extent that is TOTAL (absence inside it is a stated fact) | Open-world (absence is NOT-ASSERTED) | Order |
|---|---|---|---|
| `block`, `topology`, `flowchart` | none | the whole figure: nodes, edges, containment. A scene genre can draw that something happened; it cannot state that something did not | declaration order is not precedence (§12.7); `flow` is a reading axis |
| `statechart` (EXPERIMENTAL) | none. The state set is *closed as drawn* — a claim about what the figure asserts, not about the machine | transitions above all: absence is not prohibition, and the transition function is NOT assertable as total (`STATECHART-TRANSITION-TOTALITY`). No current, initial or final state | peer transitions out of a state have no sequence (reading rule 8) |
| `sequence` (EXPERIMENTAL) | **the ORDER of the declared occurrences**, by declared divergence (`SEQUENCE-ORDER-MODEL`) | participants (*closed as drawn*), elapsed time, concurrency without `par`, activation, loss, retransmission | **EXPLICIT, not derived** — see below |
| `bitfield` | **the interior of the declared bit sequence**: no implicit padding anywhere, so there are no undeclared bits between two declared fields — see below | the tail: nothing requires the widths to fill a word, and `*`, `present=` and undeclared repetition each end the determinate region | declaration order IS bit order and drawing order (`DECLARATION-ORDER-SEMANTICS`); the offsets are DERIVED from it |
| `table` | **the logical grid**: every address inside it exists, and an empty cell is a stated empty cell | whether the axes enumerate the subject's whole domain — that claim lives in the header prose, not in the model. A row nobody wrote is simply not in the grid | row and column order is the grid's addressing, `h1..hN` top-down and data rows 1-based |
| `timing` (EXPERIMENTAL) | **the lane, over its own length**: cycle *t* is the *t*-th character, `.` is a stated continuation and not a gap, and `gap` never removes or renumbers a cycle | everything after a lane's last character; lanes need not be equally long | the character positions ARE the axis; cycles are contiguous and aligned across the block's signals |

`chart` is not a genre (§4.4): it reads an existing `table`'s grid, so the
`table` row governs it.

**`bitfield`'s totality claim is contiguity, and it is narrower than it
looks.** The rule is *"there is no implicit padding anywhere — a real
padding/reserved region MUST be declared as an explicit field"*
([genres/bitfield.md](genres/bitfield.md) §Semantic model), and that is a
genuine closed-world claim: between two declared fields there are no
undeclared bits, so a reader may compute the second field's offset by adding
the first field's width and nothing else. It does **not** extend to the word:
no rule requires the declared widths to sum to a multiple of `word`, no error
fires when they do not, and a figure whose last word is partly declared is
legal and common. Such a figure asserts the bits it declares and says nothing
about the rest of the word.

**`sequence`'s order is EXPLICIT, and that is why it costs what it costs.**
Under every other genre, statement order is arrangement and a reader is
forbidden to read precedence out of it (§12.7). Under `sequence` the genre's
own reading rule makes the declaration order of `message` and `state` lines
the time order of **every** occurrence in the figure — so an order conclusion
there is a declared fact, not a derivation, and `SEQUENCE-ORDER-MODEL` records that this is
**stronger than both source standards**, which order events along one lifeline
only. The cost is on the record and is not softened here: where two messages
cross — both ends transmitting at once, neither send preceding the other in
the protocol — the figure must pick an order, and the picked one is a fact the
source does not state (**`MESSAGE-OCCURRENCE-GRANULARITY`**). A figure that means to leave two messages
unordered says so with `fragment … type=par`, which is part of the order model
rather than a convenience. An author who cannot say it truthfully should say
so in prose and attribute it, on the same terms as every other fact a genre
cannot carry.

## 13. Stability and versioning (normative)

**If you are adopting FigDown, read this section first.** It states what
the project promises about a document you write today, what it does not
promise, and what it commits to instead. The requirement keywords in this
section are the document's, declared once in **§0.1** under BCP 14
(RFC 2119 + RFC 8174) with the capitalization caveat. *(Until `NORMATIVE-KEYWORD-DECLARATION` this
paragraph carried the declaration itself, naming four of the eleven
spellings and one of the two RFCs; four in five of this document's
all-caps occurrences were outside it.)*

### 13.0 The version scheme (normative)

The project carries **two** version numbers, and every sentence in this
section names which one it means. They can drift, so the scheme binds
them:

| number | what it versions | where it is written |
|---|---|---|
| **`figdown X.Y`** | the **language** — the document format | the `figdown` header line of every `.fd` document (§1) |
| **`vX.Y.Z`** | the **release** — this repository and its engine | the git tag, `package.json`, and the `data-engine-version` attribute of every artifact (§7) |

**The binding.** `figdown X.Y` is **the first two parts of the release
version**. Release `v0.3.2` implements language version `figdown 0.3`.
The language number has no third part and never will: a `Z` bump is by
definition a change the language did not make.

**What each part means.** The scheme is semver-shaped, and `X` is
defined more strictly than semver requires:

| part | meaning |
|---|---|
| **`Z`** | **The language does not move — that is the test, not the size of the change (`VERSION-PART-SEMANTICS`).** No `.fd` document's meaning changes and none needs a rewrite. A `Z` release MAY add a tool, a document, a non-core profile, a schema, a render option, or a gate — anything that is not itself a language construct — because none of those move `figdown X.Y`. `v0.1.1` MAY fix a rendering defect with **no `.fd` file altered** and the language unchanged, on the same test. |
| **`Y`** | **New features are added. Nothing is ever removed.** Every document that a `Y` release accepted, the next `Y` release still accepts. |
| **`X`** | **The only point at which support may be removed**, and removing it **forces a migration**. |

`X` carries the whole of the removal budget. That is the strict part,
and §13.9 states what it costs.

#### 13.0.1 The compatibility rule

> For a document declaring `figdown x.y` and an engine implementing
> language version `figdown X.Y`:
>
> - **`X` = `x` and `Y` ≥ `y`** ⇒ the engine **MUST** accept the
>   document and render it correctly. Nothing the document used has been
>   removed, because `Y` never removes.
> - **`Y` < `y`** ⇒ the engine **MUST** reject it with a **named
>   diagnostic** (§13.7). It **MUST NOT** guess.
> - **`X` ≠ `x`** ⇒ out of scope of this rule; a major bump is exactly
>   the point at which support may have been removed, and the migration
>   is the answer (§13.9).
> - **`Z` is irrelevant to compatibility.** It is not part of the
>   language number and no compatibility question turns on it.

Never guessing is the same principle §0 states for unknown lines (`CLOSED-GRAMMAR`),
applied to the header: a guess yields **a figure that looks right and
means something else**, and that is the one failure a reader cannot
detect.

#### 13.0.2 Two limits, stated explicitly

Both of these are commonly assumed away, and conflating either one with
the rule above makes the archive (§13.5) look redundant when it is not.

**Limit 1 — this takes force at `v1.0.0`.** During 0.x there is **no such
guarantee**. 0.x is a preview, migrations are best effort (§13.3), and
**`figdown 0.1` → `figdown 0.2` is NOT covered** by §13.0.1. The rule is
written now so that it is rehearsed now (§13.4); it binds from `v1.0.0`.

**Limit 2 — compatible is not byte-identical.** `RENDERING-DETERMINISM` guarantees identical
SVG output for **the same source and the same renderer version** — that
and no more. A `Y` bump MAY legitimately change rendering (better label
placement, say) while removing nothing at all: the document's **meaning**
is unchanged, the **picture** may not be. To recover the exact figure you
need the exact engine, **which is precisely why the per-release archive
exists** (§13.5).

#### 13.0.3 Three promises, three different conditions

Today one word — "version" — carries all three of these, which is why
they get confused. They are distinct, and each is worth something on its
own:

| promise | what it says | condition |
|---|---|---|
| **Compatible** | the document's **meaning** is preserved; the engine accepts it and renders it correctly | same `X`, engine `Y` ≥ document `y`; **from `v1.0.0` only** |
| **Reproducible** | the **bytes** of the SVG are identical | same source **and** same renderer version `vX.Y.Z` (`RENDERING-DETERMINISM`); holds today |
| **Available** | the **archived engine still runs**, so the exact figure can always be recovered | unconditional, from the first release onward (§13.5) |

A document can be **compatible** without being **reproducible** (limit
2). It can be **reproducible** without being **compatible** under a
newer engine (that is what the archive is for). **Available** is the one
unconditional commitment 0.x can make, and §13.5 is why it can be made
unconditionally.

#### 13.0.4 What the source version string does between releases

The table above says `vX.Y.Z` is written in three places. Between two
releases the repository is at none of them, and this states what each
one carries meanwhile.

| where | between releases | at a release |
|---|---|---|
| the **git tag** | none exists for the working state | `vX.Y.Z` is created |
| **`package.json`** | still names the **last** release; it is not the state of the tree | bumped to the new `vX.Y.Z` **by the release act** |
| the engine's version constant, stamped into `data-engine-version` | a **dev increment** `X.Y-dev.N` | rewritten to `X.Y.Z` at publish |

**The dev counter does not reset and does not adopt the release
number (`SOURCE-VERSION-COUNTER`).** `N` counts source states of the engine and only ever
increases: `v0.1.0` was published from 0.1, and the next source
state is 0.1, then 0.1. A reset would make `N`
ambiguous across releases, and adopting `X.Y.Z` early would put a
release number on an engine no tag names. Only the release act writes a
release number, and it writes it in `package.json`.

**The mapping to `vX.Y.Z` happens once, at publish.** The published
engine's version constant — and therefore the `data-engine-version` of
every artifact built from it (§7) — reads `X.Y.Z`, matching the tag and
`package.json`. Both readings of that attribute are therefore true and
distinguishable:

- **`X.Y.Z`** — a released engine. A tag names it and an archived page
  runs it (§13.5).
- **`X.Y-dev.N`** — an unreleased source engine. **No tag and no
  archived page exists for it, and none is owed**; `RENDERING-DETERMINISM`'s reproducibility
  still holds for it, but only against that exact source state.

A reader who meets `X.Y-dev.N` in an artifact is looking at output from
between two releases. It is not a version of the **language** — the
language number is still `figdown X.Y` (§13.0) — and nothing about
`figdown X.Y` compatibility turns on it.

#### 13.0.5 Which lever a change pulls — the three-file rule (`VERSION-EVENT-TRIGGER`)

§13.0 says the project carries two version numbers. This says **which
one a given change moves**, because a change arrives at one of three
files and the file decides the lever. Written as a rule so an
implementer does not have to re-derive it, and so that no extension can
buy an exemption from either lever by choosing where to live.

| the change alters | the lever | what it costs |
|---|---|---|
| **the rendered artifact's BYTES** for an unchanged source | the **engine/release** version `vX.Y.Z` | `FIGDOWN_VERSION` moves, `data-engine-version` records it, and every shipped artifact is rebuilt. `RENDERING-DETERMINISM` is a promise per renderer version (§13.0.3), so this is what makes the old bytes still explicable |
| **what a reading agent may CONCLUDE** from the source | the **language** version `figdown X.Y` | §13.7's acceptance rule, a [`migrations.md`](migrations.md) entry, and the whole of §13's migration machinery. No exception, for anything |
| **neither** | **no version event at all** | it belongs in a file that is neither the source nor the artifact — the sidecar ([`figdown-manifest.md`](figdown-manifest.md)), which carries its own version key, never enters the SVG, and whose absence asserts nothing (§12.3's rule, one layer out) |

**Three consequences worth stating, because each has already been got
wrong once.**

- **The two levers are separable, and one may move without the other.**
  A `Y` bump MAY change rendering while removing nothing (§13.0.2 limit
  2), and a render option MAY change bytes only when it is asked for,
  which is why `with-a11y` shipped without a `FIGDOWN_VERSION` bump
  (`VERSION-EVENT-TRIGGER`). Byte change and meaning change are different events and are
  priced separately.
- **A change that alters no bytes and no meaning is not made into a
  version event by being conspicuous.** A diagnostic's wording is the
  worked example: it moves a golden and owes a `migrations.md` entry
  under [`.github/CONTRIBUTING.md`](../.github/CONTRIBUTING.md) §3, and it moves neither number,
  because no document's rendering and no document's meaning changed
  (`RESERVED-PREFIX-ENFORCEMENT` is the first application).
- **The corollary, which is the sentence to remember:** *an extension
  that needs a version bump is not an extension; it is a feature that
  has not been through the gate.* Row 3 is the only place where
  extension is genuinely free, and that is the definition of the space
  rather than a coincidence — it is why the manifest can carry
  `x-<owner>-<key>` keys (§10; `figdown-manifest.md` §2 rule 2) without
  touching either number.

### 13.1 0.x is a preview, and it is NOT stable

FigDown 0.x — both the language `figdown 0.y` and the releases `v0.y.z`
that implement it — is a **preview**. The language MAY change between
`figdown 0.y` versions in ways that require a document to be rewritten.
**No 0.x version carries a stability promise**, and none should be read
as carrying one.

This is said plainly because the alternative failure is expensive and
silent: an adopter infers stability from the project's visible care —
the migration log, the conformance suite, the word "frozen" — and finds
out the inference was wrong after standardising on it at scale. The care
is real. The promise a reader would guess from it is not the promise
being made. §13.3 states the promise that is.

### 13.2 "Frozen" is not "stable", and the difference is the point

The word **frozen** appears throughout this repository — a frozen
construct, the frozen surface, a frozen-surface change. It does **not**
mean "will not change".

> **Frozen** names the **scope of the change-management promise**, not
> the absence of change. A frozen construct MAY still change. What
> "frozen" guarantees is *how* it may change: a change to a frozen
> construct MUST ship, in the same release,
>
> 1. an entry in [migrations.md](migrations.md) carrying a **mechanical
>    rewrite rule** (any non-mechanical step flagged as such),
> 2. a **named diagnostic**, so a document written against the old
>    spelling fails loudly and by name instead of silently changing
>    meaning, and
> 3. the corresponding rewrite in
>    [`tools/migrate-figdown.js`](../tools/migrate-figdown.js).
>
> **Stable** would mean the construct does not change. FigDown `figdown
> 0.y` claims that of nothing.

A construct that is not frozen — anything marked EXPERIMENTAL (§10) —
MAY change or be withdrawn with none of (1), (2) or (3). "Frozen" is
therefore a real and useful guarantee, and it is a guarantee about
*process*, not about *permanence*. Read as "stable" it is a promise the
project never made, and the whole of §13 exists so that no one has to
guess which one it is.

What "may still change" **costs** once the §13.0.1 guarantees begin is
stated in §13.9, and it is not small: after `v1.0.0` a frozen construct
cannot be renamed inside `1.y.z` at all.

### 13.3 The three tiers

| | **0.x** (language `figdown 0.y`, releases `v0.y.z`) | **`v1.0.0` and later** |
|---|---|---|
| **Mechanical migration between language versions** | **SHOULD** — best effort | **MUST** |
| **A document staying on an older declared language version** | **MAY** — permitted, not promised | **MUST** be honoured; a rewrite is never forced |
| **The archived engine for a release version remaining runnable** | **MUST** | **MUST** |

Row by row:

- **Mechanical migration.** In `figdown 0.y` the project SHOULD ship a
  mechanical rewrite for every change to a frozen construct, and in
  practice does (§13.2, and every entry in the migration log). It is
  *best effort*: a `figdown 0.y` change MAY land whose migration is
  partly manual, or whose rewrite the tool cannot perform without
  knowing something only a human knows. From `v1.0.0` this becomes MUST
  — a language change that cannot be migrated mechanically cannot ship
  in a `v1.y.z` release.
- **Staying on an older language version.** In 0.x an implementation MAY
  keep accepting an older declared language version, and the project MAY
  stop doing so in a later `v0.y.z` engine. Permitted, not promised.
  From `v1.0.0` this becomes MUST: a document declaring a released
  language version keeps being readable under that version's semantics,
  and **a rewrite is never forced**. §13.0.1 is the precise form of that
  obligation.
- **The archive.** Both tiers: **MUST**. This is the one promise that
  does not weaken in 0.x, and §13.5 is why. Note what it is a promise
  about: the **release** `vX.Y.Z`, not the language `figdown X.Y` —
  because recovering the exact picture needs the exact engine (§13.0.2,
  limit 2).

### 13.4 0.x is a rehearsal — what that means

The migrations are not promised, and they are written all the same. The
reason is deliberate and belongs in the record: **accumulating them is
the rehearsal for `v1.0.0`'s machinery.** A project that begins writing
mechanical rewrites on the day it first MUST have one will discover on
that day whether its tooling, its diagnostics and its habits can produce
one — and it will discover it under a promise it cannot take back.
Writing them through 0.x is how the obligation is tested before it is
binding.

Stated as the working rule:

> **During 0.x the project follows the `v1.0.0` rules without being
> bound by them.** It issues the migration entries, writes the named
> diagnostics and keeps the archive as though §13.0.1 were already in
> force. It is not. **A process failure discovered in 0.x is therefore
> the point of 0.x, not an embarrassment** — it is the rehearsal
> returning its result, and the result is only useful if it is written
> down rather than tidied away.

Two such failures are on the record already, and they are cited here as
evidence that the rehearsal works, not as apologies:

1. **A migration that could not be mechanical.** §13.2's
   contract promises a **mechanical rewrite rule** for a change to a
   frozen construct. The withdrawal of `path` and `routing` had none —
   a *withdrawal* names no replacement spelling, so the correct action
   is **deletion**, and deletion changes the rendered output. The
   contract's wording never anticipated withdrawal at all. Found in 0.x
   this is a sentence to fix; found after `v1.0.0` it is a broken
   promise. (Those two constructs were EXPERIMENTAL, so nothing was
   owed for them — which is exactly why the gap in the *wording* was
   visible without a promise being broken. See §13.9's pressure valve.)
2. **No gate compared an artifact against its source.** Stale artifacts
   shipped twice — 32 files, then 5 — while
   every check reported success, because each artifact was internally
   consistent with itself. The fix was roughly ten lines
   (`tools/artifact-check.js`). A promise of reproducibility (§13.0.3)
   made on top of that gate set would have been a promise nothing was
   checking.

That reason carries a requirement with it, which would not follow from
"be correct for the current release":

> **[`tools/migrate-figdown.js`](../tools/migrate-figdown.js) MUST be
> cumulative and idempotent across language versions**, not merely
> correct for the latest hop. Every rewrite the project has ever shipped
> stays in the tool; a document from any earlier `figdown x.y` MUST
> reach the current one in a single run; and running the tool on an
> already-current document MUST change nothing.

Cumulative, because the rehearsal is worthless if the accumulated
rewrites are discarded each release — `v1.0.0`'s machinery is exactly
the whole chain, and a chain that has never been run end to end has not
been rehearsed. Idempotent, because a migration a user cannot re-run
safely is a migration they will not run at all.

**And the tool MUST itself be tested by fixtures, not by reading it.**
The migration tool is what makes §13.0.1 affordable: an `X` bump costs a
downstream corpus a command rather than a decade. That is only true if
the tool is right, and *staying* right is the failure mode that actually
occurred — it carried a rewrite rule for a full release after the ruling
behind it had been reversed, and a person reading the source found it,
because nothing ran it. [`tools/migrate-check.js`](../tools/migrate-check.js)
is the gate: one fixture pair per rewrite rule, a golden report per
report-only rule, idempotence asserted for every fixture, every migrated
result parsed through the engine, and a **negative** fixture that fails
if a retired rewrite direction ever reappears. FigDown can hold that
standard where a large general-purpose language could not, because the
language is small, closed and line-oriented —
[axiom 6](../README.md#design-axioms) paying off.

### 13.5 The archive — the promise that does not weaken

Per **release version** `vX.Y.Z`, the project commits to two artifacts:

1. **One git tag** naming the release.
2. **One immutable, self-contained engine page** for that release — a
   single HTML file with no external dependency, which renders `figdown
   X.Y` documents exactly as release `vX.Y.Z` defined them.

Neither is ever rewritten. A user who wants to stay on a release always
can, by using that release's engine, and **the promise does not depend
on any future engine understanding old documents**. That independence is
what makes tier 1's "best effort" honest rather than empty: even in the
worst case — a `figdown 0.y` change whose migration is incomplete, or a
future engine that drops an old declared language version — a document
written today still renders, from an artifact that already exists and
cannot be taken away.

It is also the answer to §13.0.2's limit 2. Compatibility preserves
**meaning**; only the archived engine preserves the **picture**. A `Y`
release that improves label placement removes nothing and breaks no
promise, and the reader who needs the byte-identical figure from two
years ago gets it from the archived `vX.Y.Z` page rather than from a
compatibility claim that was never made.

It is also why the archive is a MUST in 0.x while migration is only a
SHOULD: **it costs nothing ongoing.** A tag and a frozen file are
written once at release and never maintained again. A promise that is
cheap to keep forever can be made unconditionally; one that is not
should not be.

**The archive scheme.**

- The **tag** is the release's immutable source state, named `vX.Y.Z`.
- The **engine page** is the release's runnable renderer, published
  beside the tag and never edited afterwards.
- **[migrations.md](migrations.md) is the narrative index** over the
  archive: it is the ordered account of what changed between language
  versions and how to rewrite for it, and each release's entry links
  **its tag** and **its runnable page**. A reader who needs to run an
  old document goes to the migration log, finds the release, and
  follows the link.

During the `figdown 0.1` draft period the log records dated dev
increments (0.1) which are **not** releases: they will be
squashed into `figdown 0.1` at freeze (MIGRATIONS, header). No tag or
archived page exists for a dev increment, and none is owed — the archive
obligation attaches to **release versions**, and `v0.1.0` is the first.

**What else the archive act freezes.** From the first language version
released after `figdown 0.4`, the release act MUST also freeze, beside
that version's engine page and under the same manifest:

- **its conformance suite** — the fixtures and the runner that executes
  them, at `archive/<X.Y>/conformance/`; and
- **its normative text** — this document and `genres/**`, both language
  editions, at `archive/<X.Y>/spec/`.

Both are taken from the **release tag's tree**, never from the working
tree, and both are archived content in the sense of this section: never
rewritten, and never added to. The reason is the one §13.0.2 gives for
the engine page, applied to the other two things a released version is
made of. A working suite tracks the language under development, not the
one released, so it answers a later question than the reader asked; and
the words that fixed a document's meaning are edited continuously after
the release that fixed it. Without the freeze, a released version has no
definition an independent implementation can be measured against and no
stable text behind the meaning its documents declare — while §13.0.1
goes on promising that a document declaring that version is honoured
under **its** semantics.

This obligation is **forward-only**, and the exclusion is normative
rather than a backlog item: it is NOT applied to `figdown 0.1` through
`figdown 0.4`. Those archives are released, and this section does not
permit adding to a released archive — a backfill would be a rewrite of
one. Nor would it be a record: bytes assembled later were not what the
release shipped, and an archive that is sometimes a reconstruction is
not evidence anywhere. For those four versions the suite and the text
are reached through the release tag, which this section already
guarantees is there and unrewritten.

### 13.6 One migration at `figdown 1.0`, none after

**`figdown 0.1` → `figdown 1.0` MAY require a migration**, and that is
**the last time one may be required.** From `v1.0.0` onward, a document
declaring a released language version keeps working as declared: within
`figdown 1.y`, old documents are honoured under their declared semantics
(§13.0.1), and no `v1.y.z` release forces a rewrite.

Downstream can plan on exactly that shape: **expect one migration at
`figdown 1.0`, and none after it** — until a `figdown 2.0`, which is by
§13.0 the only place a removal may occur and which arrives with its own
migration.

If the `figdown 1.0` migration is needed, it arrives under tier 2's MUST
— mechanical, with a named diagnostic, and with the `v0.1.z` archive
still runnable for anyone who chooses not to move.

### 13.7 What a conforming engine MUST do about versions

- **An engine MUST NOT silently reinterpret a document under a language
  version other than the one it declares.** For a document whose
  `figdown` header declares language version *V*, a conforming engine
  either renders it under *V*'s semantics or **rejects it with a named
  diagnostic**. It MUST NOT fall back to another version's semantics,
  guess, or apply a "closest supported" reading. This is §13.0.1's
  `Y` < `y` branch, and it holds for every mismatch, not only that one.

  The reason is the asymmetry: guessing produces **a figure that looks
  right and means something else**. Refusing produces a diagnostic the
  author acts on in one step. A figure whose meaning silently drifted is
  the one failure the reader cannot detect, which makes it worse than no
  figure at all — the same principle §0 states for unknown lines (`CLOSED-GRAMMAR`),
  applied to the header.

- **An engine MUST state which language versions it accepts.** The set
  of accepted declared versions is part of the engine's documented
  interface, not an implementation detail to be discovered by trying a
  document and reading the diagnostic. An adopter choosing an engine
  MUST be able to find out, without running it, whether it will read
  their corpus. Stating the release version `vX.Y.Z` alone does not
  satisfy this; the accepted `figdown X.Y` set is what an author's
  document is checked against.

- **The reference engine's accepted set, stated (`STATECHART-GENRE-SCOPE`, 0.2;
  extended 0.3, `DRAWN-ANNOTATION-FORM`; extended 0.4, `SEQUENCE-GENRE-VOCABULARY`; extended
  0.5, `CONNECTOR-IDENTITY-KEY`).**
  `editor/figdown.html` and everything generated from it — `dist/`,
  `skill/figdown/figdown.html`, `tools/build-svg.js` — accept **`figdown
  0.1`, `figdown 0.2`, `figdown 0.3`, `figdown 0.4` and `figdown 0.5`**, and
  nothing else.
  Any other version token is
  `unsupported version "<token>" (expected 0.1 or 0.2 or 0.3 or 0.4 or 0.5)` — the list
  is the constant joined, so the message grows with the set. The set lives in
  one constant, `LANG_VERSIONS` — `['0.1','0.2','0.3','0.4','0.5']` — so this sentence
  and the code cannot
  drift. **This bullet is what §13.7.2a's second ground is about: it names a
  set of LANGUAGES, and it can only do that while every added spelling is
  gated on the version that introduced it.**
  (It said "0.1 and 0.2" from 0.2 to 0.3, "0.1, 0.2
  and 0.3" from 0.3 to 0.4, and "0.1 through 0.4" from
  0.4 to 0.5. The constant
  is what kept that from becoming a silent lie: the sentence is checked
  against it whenever the set moves, which is the whole reason the set was
  put in one place.) The **archived** `v0.1.0` engine (`archive/0.1/figdown.html`)
  accepts `figdown 0.1` alone, which is §13.0.1's `Y` < `y` branch working
  as specified: it rejects a `0.2` document by name rather than guessing.

#### 13.7.1 The first `Y`, and what it did and did not owe

`v0.2.0` is the project's first `Y` release, and the policy above had
never been exercised. What it did:

- **It added and removed nothing.** Every construct, genre and diagnostic
  of `figdown 0.1` is unchanged, and every `figdown 0.1` document produces
  the same model it produced under `v0.1.8`. That is `Y`'s definition
  (§13.0), and it is checked rather than asserted: the conformance suite's
  `figdown 0.1` goldens all pass unmodified.
- **It honoured §13.0.1 voluntarily.** §13.0.2 limit 1 says `figdown 0.1`
  → `figdown 0.2` is **not** covered during 0.x. It was honoured anyway,
  which is §13.4's rehearsal rule — follow the `v1.0.0` rules without
  being bound by them — and the result is that the first `Y` cost
  downstream nothing at all.
- **It owed no migration rewrite.** A migration entry is written (tier 1),
  but `tools/migrate-figdown.js` gains **no rule**: nothing was renamed,
  retired or moved, so there is no document the tool could correctly
  change. A cumulative migration tool with an empty hop for a `Y` that
  removed nothing is the correct outcome, not a gap.
- **It did NOT owe version dispatch in the parser or a per-version
  conformance partition.** §13.8 defers both until they are owed, and
  they become owed at `v1.0.0`. A single golden set still covers both
  language versions, because `Y` compatibility means the `0.1` goldens
  *are* `0.2` goldens.
- **It DID owe a new `read/<X.Y>/`.** The reading contract is indexed by
  language version, `read/0.1/` is frozen in `archive/MANIFEST.tsv`, and
  the new genre's reading rule had to go somewhere. `read/0.2/` is that
  somewhere, and it is the live contract from `v0.2.0` on.
- **It owes an `archive/0.2/figdown.html` at publish, not before.** §13.5
  archives the engine *as users ran it* — the published engine stamped
  `0.2.0`, which does not exist until the release act. `archive/0.2/` and
  the `0.2` rows of `archive/MANIFEST.tsv` (which cover `read/0.2/` too)
  are written then, by `node tools/archive-check.js --write 0.2`.

#### 13.7.2 The second `Y`, and why one of its two changes is gated

`v0.3.0` is the project's second `Y`. It is smaller than the first and it
exercises a distinction the first could not: **a `Y` may contain changes
that need a version gate and changes that do not, and the difference is
not "new" versus "old".**

- **What it added: one option key and one resolver widening.** The key is
  `note=`, the drawn annotation, accepted on ten directives including
  `title` (§2.9, §10, `DRAWN-ANNOTATION-FORM`). The widening is `in=` on `threshold`/`band`
  resolving a **region** id — a `bitfield`, `table` or `timing` block — in
  addition to a node or a group (`MARKER-TARGET-KINDS`). No genre, no keyword, no new
  value shape.
- **It removed nothing**, which is `Y`'s definition (§13.0). Every
  `figdown 0.1` and `figdown 0.2` document produces the same model under
  `v0.3.0` that it produced under `v0.2.0`, and the `figdown 0.1` and
  `figdown 0.2` goldens pass unmodified. One retirement is *reversed*
  rather than performed: `note=` was retired and is live
  again under SYNTAX-STYLE RULE 4.9 (§10). A revival removes nothing —
  it removes a diagnostic.
- **Why the LANGUAGE number had to move at all**, and not only the
  release: §13.0 defines `Z` by whether **the language moved**, not by
  the size of the change, and a new option key **is** a language
  construct — grammar `figdown X.Y` accepts. §13.0 also binds `figdown X.Y` to the first two parts
  of the release version, so shipping `note=` under a `v0.2.z` would make
  `figdown 0.2` name **two different languages** — the one `v0.2.0`
  published and the one with `note=`. The engine constant is
  0.3: `N` counts source states and never resets (§13.0.4), so 81
  is followed by 82.
- **`note=` is GATED on the declared version; `in=`'s widening is NOT.
  This is the section's real content.** The discriminator is not novelty:
  - **`in=` widening — ungated.** It adds **no spelling**: no keyword, no
    option key, no enum value, no character, and §10's registry is
    byte-unchanged. The only documents it affects are ones that **did not
    parse at all** — `threshold "Max" in=q offset=50%` over a `table q`
    answered `unknown target "q" for threshold`, the same error a
    nonexistent id gets, because the resolver's accepted set was
    hard-coded to nodes and groups. **A widening that makes a
    previously-erroring document parse is not a compatibility break and
    is therefore not gated**: no valid document changes meaning, none
    becomes invalid, and §13.0.1's hazard cannot arise, because `in=q`
    naming the document's own region has exactly one possible meaning and
    the alternative was an error message rather than a different figure.
    There is nothing to drift from.
  - **`note=` — gated, and its DIAGNOSTIC is specific to this key.** It has
    a **prior meaning on the record**: it was the retired spelling of
    `description=`, and its retirement diagnostic actively told authors to
    write `description=` for a never-drawn tooltip. An engine accepting
    `note=` under a `figdown 0.2` header would repaint that author's
    tooltip as ink on the page — §13.0.1's named hazard, *a figure that
    looks right and means something else*. ~~A key that had never been
    spelled before would carry no such risk and would need no gate.~~
    **WITHDRAWN (`CONNECTOR-IDENTITY-KEY`), struck through rather than deleted
    because a document written against any version of this section still
    needs to find it.** The sentence made the gate contingent on a
    spelling's history, and the record it was written against says the
    opposite: **four gating precedents and zero ungated vocabulary
    additions** — `flowline`/`transition` (`GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`), `statechart` (`STATECHART-GENRE-SCOPE`)
    and `sequence` (`SEQUENCE-GENRE-VOCABULARY`), `note=` itself (`DRAWN-ANNOTATION-FORM`) — while the one
    ungated change of the four `Y`s, `in=`'s widening above, **added no
    spelling at all**. The withdrawn sentence described a case the project
    had never had, and it was never exercised. The rule it is replaced by
    is stated in §13.7.2a. `note=`'s prior meaning is why its own message
    argues from repainting; it was never why the key was gated.
    The diagnostic follows the `KEYWORD-RENAME-SCOPE` device: it names the version and offers
    the one-step fix.

- **It DID owe a new `read/<X.Y>/`, at the same point `read/0.2/` was
  owed.** §13.7.1's rule is not about genres, it is about the reading
  contract being indexed by language version: `read/0.1/` and `read/0.2/`
  are frozen, `figdown 0.3` gives a reader a key it must know how to
  handle, and the note reading rule (§12.7) has to go somewhere.
  `read/0.3/` is that somewhere, written from `read/0.2/` plus that rule,
  and it is the live contract from `v0.3.0` on.
- **It owes an `archive/0.3/figdown.html` at publish, not before**, on
  §13.5's rule unchanged: the archive holds the engine as users ran it,
  and the engine stamped `0.3.0` does not exist until the release act.
  `archive/0.1/figdown.html` is untouched by this release and MUST stay
  so, including the retirement message it carries — that engine implements
  `figdown 0.1`, where the message was true.

#### 13.7.2a The gate engages on ADDING A SPELLING (`CONNECTOR-IDENTITY-KEY`)

**A change that adds a spelling to the accepted surface — a keyword, a
genre token, an option key, an enum value, an escape — is GATED on the
declared version. A change that adds no spelling is not.** That is the
whole rule, it is what the record has always done, and it is stated here
so the next key is not re-argued from scratch.

`id=` on the four scene connectors (§10, `CONNECTOR-IDENTITY-KEY`) is the first key gated
under it, and it is the case the withdrawn sentence was about: a key that
had never been spelled before, with no prior meaning of any kind, carrying
under `figdown 0.4` exactly one candidate reading — an error. Three
grounds decide it, and each is a fact about **this** project rather than a
general claim about language design:

1. **`read/<X.Y>/` is the reader's contract and it is indexed by the
   declared version** (§13.7.1). A reader holding a document to `figdown
   0.4` reads `read/0.4/`, which describes a language with no `id=`.
   Accepting the key anyway makes the frozen tree wrong about a document
   that declares that version — and a frozen tree is never edited
   (§13.7.3), so the error would be permanent and the erratum path does
   not cover an engine that ignores its own index.
2. **The accepted-versions statement stops determining the surface.** The
   bullet above requires an engine to state which language versions it
   accepts. Ungated keys make that statement name a set of *headers*
   rather than a set of *languages*, and a declaration the engine may
   silently override is a comment.
3. **The archived engine would refuse the document** (§13.5). The archive
   holds each engine as users ran it. An ungated `figdown 0.4` document
   using `id=` parses in the current tree and fails under
   `archive/0.4/figdown.html` — the AVAILABLE promise failing quietly,
   which is the one failure this project keeps an archive to prevent.

**What is still ungated, unchanged.** A resolver widening (`MARKER-TARGET-KINDS`), a
diagnostic added or reworded, a drawing convention (`DOMAIN-CONVENTION-DIRECTIVES`), and any change
that makes a previously-erroring document parse without adding a spelling.
The discriminator is the spelling, not the novelty and not the risk of
misreading.
#### 13.7.3 A discovered error in a frozen reading contract — the erratum path

A `read/<X.Y>/` tree is frozen the moment its release ships (§13.5, and the
`archive/MANIFEST.tsv` rows that hash it): it is the reading contract for
language version `figdown X.Y`, and it preserves what that version *said*.
Freezing has a consequence the project met for the first time at `GENRE-NAMESPACE`: a
frozen tree can be found to say something **wrong** — not something that later
changed, but a description that was already wrong on the day it shipped. `GENRE-NAMESPACE`'s
case is the type specimen: `read/0.1/`, `read/0.2/` and `read/0.3/reading.md`
each tell a reader to *"Ignore the layout zone. Everything from the `layout`
keyword down is …"*, a **position** test, when the rule is **membership** — a
`pin` may sit before the `layout` line and is ignored just the same. The engine
never behaved otherwise, so nothing *changed*; the words were wrong from the
start. This subsection states, once, what happens then.

- **The frozen bytes are never edited.** Not to fix a wrong description, not
  ever. The freeze (§13.5) preserves the released wording precisely *because*
  it may not be second-guessed later; an in-place "correction" destroys the
  record of what the version said and breaks every hash and every downstream
  consumer pinned to those bytes. A frozen tree is right to preserve a wrong
  sentence.

- **The error is registered in [ERRATA.md](ERRATA.md), outside the frozen
  trees.** One entry names the frozen file(s), quotes the wrong wording
  verbatim, gives the correct wording, cites the **superseding ruling**, and
  lists the affected language versions. `gate:errata` proves each entry against
  the real files and the real ledger, so the registry cannot drift from what it
  documents. This is *not* a MIGRATIONS entry and *not* a version change: an
  erratum corrects a **description of behaviour**, and the behaviour did not
  move, so the engine does not increment and no `figdown` header changes.

- **The correction lands in the NEXT read tree written**, which — because
  `read/` is indexed by language version, not release version (§13.0, §13.7.1)
  — is `read/<the next language version X.Y>/`, created when that version ships,
  written from the current tree *with the corrected wording substituted for the
  wrong wording*. A `Z` patch release reuses the frozen `read/<X.Y>/` unchanged
  and carries no correction.

- **The honest gap, named and not papered over.** A correction that applies to
  an **already-frozen language version** never reaches that version's `read/`
  tree at all — those bytes stay wrong forever, by design. Between the erratum
  being registered and a *new* language version's read tree being written,
  every consumer of the frozen tree reaches the correction **only through
  ERRATA.md** and whatever reader-facing pointer routes them to it (today:
  `skill/figdown/SKILL.md`, the entry point that hands an agent the vendored
  frozen contract). `GENRE-NAMESPACE` applies to `figdown` 0.1, 0.2 and 0.3, all frozen, so
  its correction reaches a `read/` consumer through the erratum register until a
  `figdown 0.4` writes a `read/0.4/` that states the rule correctly. There is no
  mechanism that closes this gap without either editing frozen bytes (refused
  above) or shipping a new language version; the register is the bridge, and the
  gap is the price of the freeze being real.
  **That is now history rather than a live gap: `figdown 0.4` shipped
  `read/0.4/` and it states the rule by membership. §13.7.4
  records the discharge.**

#### 13.7.4 The third `Y`, and what a genre token costs when it arrives empty

`figdown 0.4` is the project's third `Y`, and it is the *first* one written to
a shape the project had already rehearsed: `STATECHART-GENRE-SCOPE`'s. It is deliberately the same
increment, one genre later.

- **What it added: one genre token, and nothing else.** `sequence` joins the
  genre set at `figdown 0.4` (`SEQUENCE-GENRE-VOCABULARY`). It adds **no keyword, no option key,
  no enum value and no character** — §10's registry is byte-unchanged, and so
  is every renderer path. `LANG_VERSIONS` becomes `['0.1','0.2','0.3','0.4']`
  and `GENRES_BY_VERSION` gains a `'0.4'` row that is `'0.3'`'s plus the one
  token. The engine constant moves 0.3 → 0.4: `N` counts
  source states and never resets (§13.0.4), so 91 is followed by 92.
- **It removed nothing**, which is `Y`'s definition (§13.0). Every `figdown
  0.1`, `0.2` and `0.3` document produces the same model under 0.4
  that it produced under 0.3, and every golden of all three versions
  passes unmodified.
- **Why the LANGUAGE number had to move, and not only the release.** A genre
  token is language surface: it is a value the header's second position accepts,
  and §13.0 defines `Z` by whether the language moved, not by how big the
  change is. Shipping `sequence`
  under a `v0.3.z` would make `figdown 0.3` name two different languages, the
  same argument §13.7.2 made for `note=`. `STATECHART-GENRE-SCOPE` settled this for `statechart`;
  nothing about the second genre changes it.
- **The version gate is free, and stating that is the point.** No code was
  written for `figdown 0.3 sequence`. The later-version search that `STATECHART-GENRE-SCOPE` added
  for `statechart` — "is this token in the genre set of some version above the
  one declared?" — answers `genre "sequence" requires figdown 0.4 (this
  document declares 0.3) — write: figdown 0.4 sequence` on its own. A
  mechanism that generalises to the *n*-th case without an *n*-th branch is the
  return on having built it once.
- **What the genre did NOT yet do, stated rather than left to
  be found — and CLOSED.** `sequence` had no row in the engine's
  per-genre allowlist (`GENRE_KW`), and the allowlist guard is written to skip a
  genre with no row. So for one increment a `figdown 0.4 sequence` document
  could write any registered keyword and it parsed: the genre **stated a
  reading and constrained nothing**. The document that increment meant to land
  was the header alone. **0.4 lands the vocabulary and the allowlist row
  together**, as promised: five keywords (`lifeline` `message` `state`
  `fragment` `operand`), no new option key, three named refusals (`gap`,
  `group`, `lost=`), and a reading rule in `read/0.4/experimental/sequence.md`.
  **One thing is still owed and is stated the same way: there is no RENDERER.**
  A `sequence` document parses to a complete model, reports zero errors, and
  draws an EMPTY CANVAS. Until the ladder layout lands, this genre states a
  reading and draws nothing — which is why its reference figure ships with no
  `.svg` beside it.
- **It DID owe a new `read/<X.Y>/`**, on §13.7.1's rule unchanged: the reading
  contract is indexed by language version, `read/0.1/`–`read/0.3/` are frozen,
  and `figdown 0.4` is a version a reader may now meet. `read/0.4/` is written
  from `read/0.3/`, and it carries **one substantive change beyond the copy**:
  erratum **E1** ([ERRATA.md](ERRATA.md), `GENRE-NAMESPACE`) is discharged in it. §13.7.3
  said the correction lands in the next read tree written, and this is that
  tree: `read/0.4/reading.md` states the layout rule by **namespace
  membership** — ignore every member of the layout namespace wherever it
  appears — and does **not** carry the position-based sentence forward. The
  three frozen trees keep the wording they shipped, which is the freeze working
  and not a second defect. `tools/make-skill.js` and `tools/skill-coverage.js`
  now mirror and gate against `read/0.4/`, so the vendored
  `skill/figdown/reference/reading.md` carries the corrected wording too.
- **It owes an `archive/0.4/figdown.html` at publish, not here**, on §13.5's
  rule unchanged.
- **A PRE-EXISTING archive debt, recorded here so the `0.4` publish does not
  silently inherit it.** `archive/MANIFEST.tsv` holds **`0.1` rows only**.
  `read/0.2/` and `read/0.3/` are frozen by this section and by §13.5 — nothing
  edits them and nothing may — but they are hashed by **nothing**: `gate:archive`
  reports its prefixes as `archive/0.1, read/0.1`, so its green is a statement
  about `0.1` and about no other version. The freeze on `0.2` and `0.3` is
  therefore a policy the project keeps, not a property a tool proves. The debt
  was contracted at the `v0.2.0` and `v0.3.0` publishes (the `--write` step
  §13.7.1 and §13.7.2 each describe was not run) and it is **recorded, not
  fixed,** — writing `0.2`/`0.3` rows now would hash the bytes as
  they are *today* and assert they are the bytes those releases shipped, which
  is exactly the claim the manifest exists to make honestly and exactly the
  claim nobody can make retroactively. The correct discharge is a publish-time
  act, and the record here is what keeps `0.4` from adding a fourth
  unhashed tree without anyone noticing.

#### 13.7.5 The fourth `Y`, and the first key gated under the restated rule

`v0.5.0` is the project's fourth `Y`, and it is the smallest one yet:
**one option key, on four directives, optional everywhere.**

- **What it added.** `id=` on `edge`, `flowline`, `transition` and
  `message` — the connector's handle, trailing, keyword-only, OPTIONAL,
  unique within the section, joining the section's existing id namespace
  (§10, §12.2, `CONNECTOR-IDENTITY-KEY`). One consumer lands with it: a `bundle` member may
  be a connector id as well as an endpoint pair
  ([experimental.md](experimental.md) §E5, `EDGE-IDENTITY-CONSUMERS`). No keyword, no genre, no
  enum value, no escape; `figdown 0.5` adds **no genre** and its genre set
  is `figdown 0.4`'s exactly.
- **It removed nothing.** Every `figdown 0.1`–`0.4` document produces the
  same model under `v0.5.0` that it produced under `v0.4.0`, and their
  goldens pass unmodified. The published `.fd` corpus changes zero lines,
  because the key is optional and nothing had to be renamed to make room
  for it.
- **Why the LANGUAGE number had to move.** §13.0 defines `Z` by whether
  the language moved, not by the size of the change, and a new option
  key **is** a language construct. Shipping `id=` under a `v0.4.z` would make `figdown
  0.4` name two different languages — the one `v0.4.0` published and the
  one with `id=` — which is §13.7.2's argument for `note=`, unchanged. The
  engine constant is 0.5: `N` counts source states and never
  resets (§13.0.4), so 111 is followed by 112.
- **It is GATED, and it is the first key gated on the rule rather than on
  a prior meaning** (§13.7.2a). `figdown 0.4 … id=e1` is a line error that
  names the version and offers the one-step fix, and the ACCEPTING half is
  pinned beside the refusing one, because a gate only one direction pins
  is not a gate.
- **It DID owe a new `read/<X.Y>/`**, on §13.7.1's rule unchanged:
  `read/0.1/`–`read/0.4/` are frozen and `figdown 0.5` is a version a
  reader may now meet. `read/0.5/` is written from `read/0.4/` plus the
  handle's reading rule — an id is what other constructs use to name this
  element, and its spelling means nothing (§12.7) — and it is the live
  contract from `v0.5.0` on.
- **It owes an `archive/0.5/figdown.html` at publish, not here**, on
  §13.5's rule unchanged; and it inherits §13.7.4's recorded manifest
  debt without adding to it silently — `read/0.5/` is the fifth tree the
  freeze covers and the fourth that nothing hashes.

### 13.8 What tier 2 costs, stated honestly

The `v1.0.0` promises are not free, and the price lands in one place:
**from `v1.0.0` on, every released language version's conformance cases
MUST keep passing, forever.** Each release adds a set of goldens that no
later release may break, because "a document declaring `figdown x.y`
keeps working as declared" is exactly the statement those goldens test.
The suite grows monotonically, and every future change is checked
against the whole of it.

That is the cost of the tier-2 promises, and it is recorded here so it
is chosen rather than discovered.

**Deliberately not built yet (0.x).** Two mechanisms implement the
`v1.0.0` promises and are deferred until they are owed: **version
dispatch in the parser** (selecting a language version's semantics from
the declared header rather than assuming the current one) and
**per-version partitioning of the conformance suite** (one golden set
per released language version, all of them run). Neither exists in 0.x,
and neither is needed while the archive carries the older-version
promise on its own (§13.5). Both are prerequisites for the tier-2
column, not optional refinements of it.

### 13.9 The renaming corollary — a rename is a removal

§13.0 gives `X` the whole removal budget and §13.2 says a frozen
construct MAY still change. Put together they force a rule the project
has not stated before, and it constrains every naming decision made
after `v1.0.0`:

> **Retiring a spelling IS removing support.** The old document does not
> break silently — it gets a named diagnostic, which is what §13.2's
> contract buys — but it **no longer produces a correct figure**, and
> §13.0.1 promises a correct figure, not a helpful error message.
> Therefore **after `v1.0.0` a frozen construct MUST NOT be renamed
> within `figdown 1.y`. A rename takes `figdown 2.0`.**

This is what "a frozen construct may still change" actually costs once
the guarantees begin. During `figdown 0.y` a rename is free — the
project has performed dozens, and the migration log is the evidence.
After `v1.0.0` each one is a major release.

#### 13.9.1 Why the deprecation cycle is refused

The obvious escape is the industry-standard one: deprecate in `1.3`,
warn through `1.4` and `1.5`, remove in `1.6`. **FigDown refuses it**,
and the refusal is recorded here so the argument is not re-fought every
few years and eventually lost by attrition.

**Python is the instructive case, and it does not hold this line.**
`distutils` and `imp` were removed in 3.12, the `collections` ABC
aliases in 3.10 — each by deprecate-then-remove across *minor* releases.
That is a defensible policy and a large, successful project chose it.
FigDown is deliberately stricter, for one reason:

> **Code is maintained; documents are archived.** A deprecation cycle
> assumes the author comes back, runs the thing, and sees the warning. A
> `.fd` written five years ago and never reopened has no such author. It
> is not "maintained software with a lapsed maintainer" — it is a
> figure inside a document someone is reading **right now**, and it
> simply has to render.

Python's assumption holds for its artifact: a package that nobody
imports for five years is a package nobody notices breaking, and one
that is imported daily has someone to read the `DeprecationWarning`.
FigDown's artifact inverts both halves. The figure with no maintainer is
the *normal* case, not the neglected one, and the moment of truth is a
reader opening a document, where no warning channel exists at all.

#### 13.9.2 The pressure valve — freeze is the deprecation decision

Stated alone, §13.9 reads as rigidity: a language that can never rename
anything accumulates its early mistakes forever. It does not, and the
mechanism is already in the process:

> **The thing that needs removing should never have been frozen.** The
> frozen/EXPERIMENTAL split (§10, [experimental.md](experimental.md)) is
> FigDown's deprecation mechanism, **applied before the fact instead of
> after it.**

`path` and `routing` came out of the language cleanly —
withdrawn outright, no replacement spelling, no cycle — **because they
were EXPERIMENTAL**, not because a deprecation process was followed.
That is the valve working as designed.

What the rule does, then, is **move the cost onto the decision to
freeze**, which is where measurement and argument can be brought to bear
(PROCESS §2's gate: semantic impossibility, corpus evidence, prior art
surveyed). Deprecation spends the cost after the fact, on every
downstream reader, at a moment when no evidence can be gathered and no
argument can be had. Freezing carefully spends it once, in advance, in a
room where someone can still say no.

## 14. Legibility floor (normative)

### 14.1 What the floor is, and what it is not

The standard secures **meaning** (§12: the semantic model, per-genre reading
rules) and **bytes** (§13: byte-identical output per renderer version, `RENDERING-DETERMINISM`). It
secures **nothing else** about whether the SVG a human opens can actually be
read. That was, until this section, a property of the reference engine and not
of the standard: a second conformant implementation could pass every §12 fixture
and draw an unreadable figure.

**The legibility floor** closes the checkable part of that hole. It is a set of
**output constraints of the form "a conformant renderer MUST NOT emit this,"
each decidable from the emitted SVG alone** — no knowledge of how the engine
reasoned, only a predicate over the geometry it produced, with a stated
tolerance. That decidability is the whole property that separates the floor from
aesthetics: whether a figure is beautiful, whether its routing is elegant,
whether its spine is a clean column are **implementation** questions the engine
owns under **`DOMAIN-CONVENTION-DIRECTIVES`** (the engine owns drawing conventions), and the floor does not
touch them. A floor is a floor, **never a ceiling**.

The floor is **not a fourth promise.** COMPATIBLE / REPRODUCIBLE / AVAILABLE
(§13.0.3) are each a clean guarantee under a stated condition. A "LEGIBLE"
promise would read "the render is legible," and this section deliberately does
not claim that — it covers only the SVG-checkable *part* of legibility, with a
boundary (§14.4) it is careful to name. Promising LEGIBLE would overclaim
exactly what the honest analysis withholds. The floor is therefore a **MUST-NOT
list**, not a promise.

Every rule in the floor is satisfiable **deterministically** (`RENDERING-DETERMINISM`); constrains
the engine's **OUTPUT, never the author's meaning** (`DOMAIN-CONVENTION-DIRECTIVES`); and reads only markers
that render a fact the model already carries, never one they introduce
(**`PRESENTATION-AS-MEANING-CARRIER`**). A rule enters the floor only once it is proven to catch a real,
maintainer-confirmed defect, is measured by a checked-in instrument, and does
**not** fire on a figure the eye reads cleanly — a floor rule that charges a
correct drawing is worse than no rule.

### 14.2 The floor's height: no worse than a figure the eye reads cleanly

The floor's calibration is stated once, as the height every rule is measured
against: **a conformant figure must be no worse than one the eye reads cleanly.**
The worked reference is [`turnstile`](../examples/statechart/turnstile.fd) — two
states, two self-loops, an anti-parallel `coin`/`push` pair — which scores **0 on
every axis of the reference instrument, F5 included**, precisely the geometry
(self-loops, a tight anti-parallel fan) a naive rule would trip on. Any floor
rule that flags `turnstile`, or any figure a reader takes in without hesitation,
is miscalibrated and is rejected. The rules below are tuned to that height and
verified against it.

### 14.3 F5 — label-association margin (the first and currently only rule)

> **F5 (normative).** A conformant renderer MUST NOT emit an edge label whose
> distance to its **nearest** edge and to its **second-nearest distinct** edge
> differ by less than **M = 4 px**, where every distance is measured from the
> **centre of the label's rendered box** to the nearest point of the edge.

**Why it is checkable from the SVG alone.** A reader who cannot tell which of two
near-equidistant edges a label names is confused by exactly the quantity the
predicate measures; the rule needs no knowledge of which edge the label *truly*
belongs to, because a reader has none either. A second implementation checks it
with three inputs all present in its own output: the label boxes, the edge
polylines, and the distance from a box centre to a segment.

**The measuring point is the rule.** The same label scored from its anchor, its
box centre, and its box's nearest corner gives three different margins; only the
**centre** matches the eye. A wide side-anchored label can graze its own line at
one corner (a near-zero corner distance that reads CLEAN) while its *identity* —
its centre, the locus a reader fixes on — floats out into a convergence where two
lines are equidistant. Measuring from the centre is what lets F5 catch the
flagship `tcp-state-machine` defect (`rcv ACK of FIN / x`, centre 64.5 px from
its own line and 65.0 px from the next: a 0.5 px margin) that an anchor or
corner metric passes.

**M = 4 px, from evidence.** Post-filter margins of the real defects run
**0.0–3.6 px**; the nearest *clean* things are an intended anti-parallel fan at
**4.9 px** and `turnstile`'s tightest label at **7.0 px**. M = 4 sits in that
gap: it catches every real defect and clears the fan and `turnstile`. M is not to
be moved to enlarge or shrink the flagged set; moving a tolerance to change a
finding is the move this standard refuses.

**Four filters make the predicate honest.** F5 is charged only after four
false-positive filters, each of which excludes a case the eye reads cleanly:

1. **Per-edge distance.** The distance to an edge is the minimum over that edge's
   own segments, so the two segments of one bent edge are never counted as a
   label's "second edge."
2. **Node-border proximity.** A label whose centre lies within **18 px** of any
   node border is an endpoint/port label the reader associates with the node, not
   an edge-vs-edge ambiguity. The test is **absolute**, not relative to the
   nearest edge: a label that floats far from every edge is not excused merely
   because a node happens to be nearer.
3. **Edge-label identification.** Only labels that name an edge are subject to
   F5; legends, titles, node labels and notes are not. In the reference engine an
   edge label is identified by its **halo twin** (each edge label is drawn twice
   at one point — a white halo then the coloured glyph — while non-edge text is
   drawn once). A second implementation MAY identify its edge labels by any means
   its own output makes decidable; the halo is the reference engine's signal, not
   a required one.
4. **Anti-parallel / self-loop exemption.** When a label's two nearest edges
   share **both** endpoints (an `A→B`/`B→A` pair, or two loops on one node), the
   small fan between them is intentional and colour disambiguates the pair, so F5
   does not charge it — the same shape as §12's declaration-driven exemptions,
   where an intended coincidence carries a marker the engine emits from the
   author's meaning rather than a heuristic guess.

On the current corpus F5 flags **9 labels across 6 figures** out of 201 edge
labels considered; every figure the eye reads cleanly, `turnstile` included,
scores 0.

**F5 is enforced as a RATCHET, not yet a hard zero.** The 9 flagged labels are
real defects, and they sit in figures still under layout repair (the ordering
and label-aware-placement work, `decisions/registry.md` items 26/27). F5 is
therefore **advisory now**: the reference gate (`tools/layout-lint.js`) prints
the F5 count on every run against a **checked-in per-figure baseline** and
**fails only on regression** — a figure whose F5 count exceeds its baseline,
including a currently-clean figure that gains its first F5 defect. This catches a
new ambiguity the moment it lands while not masking the known, filed residue as
clean. **When the baseline reaches 0, F5 becomes a hard `--strict` zero and the
ratchet is retired.** The baseline is not lowered by weakening M or a filter; it
is lowered only by fixing placement.

### 14.4 The boundary the floor deliberately does not cover (FRONTIER)

Naming what the floor cannot yet check is load-bearing, not a caveat. Three
things were measured this workstream and are recorded as **frontier, not floor**:

- **F6 — a "floating label" ceiling** (a label with a clear margin that
  nonetheless floats far from every line, so the reader hunts for its owner) is a
  real, orthogonal dimension but has **no clean single-threshold predicate**: a
  centre-distance ceiling is **width-confounded** (a wide peripheral label reads
  perfectly yet sits far from a short stub), and a nearest-corner ceiling is too
  lenient to see the harm. It needs a width-normalised or local-density measure
  that does not yet exist. F6 is not a floor rule.
- **The false-adjacency generalisation** ("nearest own edge vs nearest *any*
  unrelated node/label/endpoint") **collapses into crowding-in-general**:
  measured, **55 of 201** edge labels sit within 25 px of some foreign node
  border or edge endpoint, `turnstile` among them. A rule that charges a quarter
  of the corpus is not a floor predicate. It is not a floor rule.
- **Label-aware placement is the fix; the floor is only the objective
  function.** F5 *measures* an ambiguity; what *resolves* it is label-aware
  placement (`decisions/registry.md` items 26/27). On a placement-saturated
  figure the defect can only be moved, not removed, by the engine's current
  routing — which is exactly why the floor states the target and the backlog owns
  the optimiser. The floor is the objective, never the fix.

These are the floor's declared frontier and its top future candidates. None
becomes a rule until a proven, non-false-positive predicate exists for it —
holding the same bar F5 met.

## 15. Security considerations (normative)

### 15.1 The threat model, in one paragraph

A `.fd` is **untrusted text** and an `.svg` artifact is **untrusted markup a
host embeds**. Those are the only two objects, and each has one adversary. A
document may arrive from anywhere — a pull request, a model, a paste — and its
strings are author-controlled all the way down; the language has no way to
know, and deliberately does not ask, whether the person who wrote
`node a "<script>alert(1)</script>"` meant a figure about script tags or an
attack. An artifact is then inlined into a page, a wiki, a README preview,
where the host's own script and cookies are one DOM away. So the contract this
section states is a contract about the **boundary between the two**: what a
renderer MUST NOT put into an artifact whatever the source says, what the
artifact carries back out, and — said as plainly as the rest — what is
**UNSPECIFIED**, so a consumer knows which risks it still owns.

**This section states only what a gate assertion or a conformance fixture
demonstrates.** Every normative sentence below resolves in §15.7's
claim→evidence table to a check that runs on every change. Where nothing
checks a thing, this section says so and calls it unspecified or not currently
checked, rather than promising it. A security claim nobody tests is worse than
no claim: it is a claim an adopter will rely on.

### 15.2 The Safe SVG profile

A conforming renderer's artifact MUST satisfy all of the following, **for
every input, including hostile input**. Each is a property of the output
document alone, decidable by reading it — the same decidability rule §14
holds its floor rules to.

1. **No script.** No `script` element, by any spelling or namespace.
2. **No event handlers.** No attribute whose name begins `on`.
3. **No `foreignObject`.** The element that reintroduces the whole host
   markup language inside a picture is not in the output vocabulary.
4. **No fetching references.** No `href`, `xlink:href` or `src` attribute; no
   `url(…)` in any attribute value naming anything but a same-document
   `#fragment`; no `@import`. An artifact that renders makes **no network
   request**.
5. **A closed output vocabulary.** The renderer emits element and attribute
   names from a fixed, enumerated set. This is the assertion that makes 1–4
   durable rather than a snapshot: 1–4 forbid the constructs known to be
   dangerous today, and 5 fails the day any *new* output construct appears,
   dangerous or not, so it is looked at before it ships.

**On the current tree the set is sixteen element names and sixty-three
attribute names.** The elements: `svg`, `defs`, `g`, `metadata`, `title`,
`desc`, `rect`, `line`, `path`, `polygon`, `circle`, `ellipse`, `text`,
`tspan`, `marker`, `pattern`. The attributes are structure, geometry, paint,
type, marker and pattern names, plus FigDown's own `data-*` provenance channel
(§7) — and no others. Both sets are **measured, not aspirational**, and the
evidence has two parts of unequal strength, named separately because only one
of them is standing:

- **Standing, every run.** `gate:safesvg` opens **every committed `.svg` in
  the tree** — its corpus is the query `git ls-files '*.svg'`, not a number,
  so an artifact added tomorrow is in it without anyone editing a count — plus
  the adversarial fixtures rendered FRESH, both ways, because the profile is a
  property of the renderer and a stored file only proves what the renderer did
  once. Any element or attribute name outside the enumeration fails the run.
  This is what keeps the set closed from here on.
- **One-shot, offline, 2026-08-19 (`XML-CHARACTER-LEGALITY`).** The enumeration was
  originally DERIVED by rendering every `.fd` in the tree that rendered at
  that date and taking the union with the committed artifacts. Nothing
  re-runs that sweep. The document corpus has grown substantially since, and
  the standing gate reaches those documents only through whatever artifacts
  of theirs are committed. It is recorded here as the set's provenance, not
  as a continuing measurement, and it must not be quoted as one.

A name that is allowed but never emitted would weaken
the assertion to nothing. `gate:safesvg` holds the enumeration; growing it is
a deliberate edit, which is the point.

**The `data-*` half of that set, enumerated here rather than left to the
gate's source (`LAYER-EXTENSION-DOORS`, 2026-08-23).** Twenty names are emitted, and until this
paragraph five of them were named in normative prose, one in passing, and
**fourteen existed only in an engine and a gate's set literal** — a
measurable gap between what the profile claims to enumerate and what a reader
of the specification could actually enumerate. It is closed by writing them
down, with each one's specification state stated rather than implied:

| state | count | names |
|---|---|---|
| **SPECIFIED** in normative text | 5 | `data-sha256`, `data-engine-version`, `data-render-options`, `data-edge` (all §7); `data-desc-state` ([`figdown-a11y.md`](figdown-a11y.md), `ACCESSIBLE-DESCRIPTION-SOURCES`) |
| **MENTIONED**, never defined | 1 | `data-note-for` (§2.9, one clause) |
| **ENUMERATED ONLY** — carried by this list and `gate:safesvg`, and promised nowhere | 14 | `data-node`, `data-x`, `data-y`, `data-group`, `data-gx`, `data-gy`, `data-gline`, `data-gtop`, `data-gbot`, `data-cell`, `data-bus`, `data-lasso`, `data-note-on`, `data-port-sq` |

**Listing a name here is not a promise about it.** The third row's fourteen
are the renderer's own working channel: they are what this engine emits
today, they are inside the closed vocabulary assertion, and **nothing in this
specification says any of them is stable, addressable, or safe to build on**
— which is the honest state and is stated so a consumer does not read the
enumeration as a contract. Promoting one of them to the first row is an
ordinary spec change with its own ruling.

**The namespace belongs to the producing engine.** A party that ADDS a
`data-*` attribute to a rendered artifact has produced a **derived file**,
not a FigDown artifact — see [`host-profile.md`](host-profile.md) §5 for the
rule and what may then be published (`LAYER-EXTENSION-DOORS`). `gate:safesvg` scans every
committed `.svg` in this repository, which is how that rule is kept true of
the tree rather than only of `examples/` and `figures/` (`RESERVED-PREFIX-ENFORCEMENT`).

**Three of those names belong to a render option, not to the default
output.** `desc`, `role=` and `data-desc-state=` are emitted only by a
`with-a11y` render (§7; the accessibility profile,
[`figdown-a11y.md`](figdown-a11y.md), `ACCESSIBILITY-PROFILE`); no default render and no
`with-title` render produces any of them. They are held to the same measured
standard as every other name: `gate:safesvg` renders the adversarial fixtures
a third way, under `with-a11y`, so the two new text sinks that option opens —
a root `<title>` and a derived `<desc>`, both carrying author text — are
exercised against hostile input rather than assumed safe. `aria-hidden=` and
`aria-label=` are **not** in the set, because this engine emits neither:
decorative hiding is a publisher's MAY with no producer (`ACCESSIBILITY-PROFILE`), and the
`role="img"` downgrade that would need `aria-label` is a publisher's choice
recorded in a manifest, never something the renderer writes.

**What IS in an artifact, recorded so that "clean" is not mistaken for
"empty".** Font names (`system-ui,sans-serif` and `monospace`) are generic
family *names*, not files: they select what the viewer already has and fetch
nothing. `style=` appears, and carries only `cursor:…` values from an
enumerated list. Exactly one absolute URI appears in any markup region of any
shipped artifact — `http://www.w3.org/2000/svg`, as the value of `xmlns` — and
it is a **namespace name**, which no reader dereferences. `gate:safesvg`
asserts that it is the only one.

**The profile is a renderer obligation, not a host's whole defence.** It says
what FigDown's own renderer puts in the file. It says nothing about an `.svg`
that arrives claiming to be a FigDown artifact and was written by something
else; a host that embeds third-party SVG needs its own sanitiser, and this
profile is what makes FigDown's output pass one rather than a reason to skip
it.

### 15.3 Hostile text is DATA, at every stage

The language does **not** refuse hostile-looking text, and MUST NOT: a figure
about markup has to be able to say `<script>`, and an engine that stripped
angle brackets would silently draw a different figure from the one the source
states — the one failure §12's model exists to make impossible. The guarantee
is not refusal. It is that author text stays **data** at every stage:

- **In the model** it is the source's own bytes, unescaped and unsanitised.
  Conformance fixtures 950 and 951 pin `<script>alert(1)</script>`,
  `" onclick="alert(1)`, `javascript:alert(1)`, a `foreignObject` element and
  an `@import` rule as label, class-meaning, edge-label, `note=` and
  `description=` strings, byte for byte, in `.model.json` goldens.
- **In the artifact** it is escaped text content, never markup. `gate:safesvg`
  renders those fixtures fresh and classifies every byte of the output into
  markup, text or CDATA; the assertion is that the hostile tokens are present
  in the document — nothing was quietly stripped — and appear in **no markup
  region**.

### 15.4 The embedded source is data, never markup

§7 requires every artifact to embed its full source in
`<metadata id="figdown-source">`. Embedding a document inside a document is
the shape injection attacks are made of, and the CDATA section that holds it
has exactly one terminator, `]]>`. So:

- A renderer MUST make the embedded source **unable to terminate its own
  container**. The reference engine rewrites each `]]>` in the source as
  `]]]]><![CDATA[>`, which splits the section *inside* the terminator; the two
  adjacent sections concatenate back to the original, so the rewrite is
  exactly reversible and no other byte is touched.
- The recovered source MUST equal the source byte for byte, and `data-sha256`
  MUST be the hash of those same bytes. Fixture 952 is a document whose title,
  labels and comments contain `]]>`, the pre-spliced form `]]]]><![CDATA[>`, a
  closing `</metadata>` tag and a `<script>` element positioned to run if any
  of them landed; `gate:safesvg` renders it, recovers the source and requires
  both equalities.

**A note for consumers, because it is where the real risk now sits.** The
escaping is the renderer's job and it holds. *Reading it back* is the
consumer's, and a consumer that lifts the source out with a lazy regex up to
the first `</metadata>` gets the wrong bytes: on fixture 952 that recovers 687
of 2289 characters, stopping at a closing tag written inside a comment. **An
extractor MUST be CDATA-aware.** Recovering the source and then checking it
against `data-sha256` catches this — which is the strongest argument for
performing the check rather than trusting the extraction.

### 15.5 Untrusted input: what is refused, and what is UNSPECIFIED

**What the closed grammar refuses.** §0's closed line-oriented grammar is the
first and largest filter, and it is a filter on *shape*, not on content: an
unregistered line-start token, an unregistered option key, an out-of-domain
value and a malformed number are all errors with a line number (§8), and no
document reaches the renderer without passing it. This refuses the
extreme-numeric class outright, and every part of that is pinned:
`367-pin-extent-strict-value` requires each of `width=0`, `width=-50`,
`width=1e3`, `width=12em` and `width=120abc` to fail with *width must be a
positive number (px)*; `361-pin-errors` requires `at=` to take a parenthesised
point; and `954-pin-extreme-numeric-refusals` writes the values a generator
actually reaches for once it knows that shape — `at=(1e12,1e12)`,
`at=(NaN,NaN)`, `at=(Infinity,0)`, `at=(-1e12,-1e12)`, `at=(1e400,0)` — and
requires all five to fail. **None of this is a security feature that was
designed;** the grammar has no special case for `NaN`, it simply never had a
production `NaN` could match, and that is what makes the refusals durable.

**The boundary is the grammar's, not a magnitude limit, and the difference
matters.** A large *plain decimal* is accepted: `pin at=(999999999999999999999,0)`
renders, to a document whose `viewBox` is `0 0 1e+21 76`. That is the
unbounded-canvas case below, and a reader who mistook the refusals above for a
bound on magnitude would be trusting a limit that does not exist. (Also
measured, not pinned: an unterminated string is rejected at any length, a 1 MB
one included; group nesting is refused beyond one level.)

**Resource limits are UNSPECIFIED.** There is no bound, in this specification
or in the reference engine, on any of:

- **time** — rendering cost is superlinear in element count. Measured on the
  reference engine: 1 000 bare nodes render in ~2.5 s, 5 000 in ~40 s, 10 000
  in ~214 s; a 5 000-edge chain in ~122 s; a complete graph on 120 nodes
  (7 140 edges) in ~73 s;
- **output size** — a `bitfield` with `word=262144` renders in ~2.2 s to a
  23 MB artifact, and larger widths do not complete;
- **canvas extent** — `pin at=(999999999999999999999,0)` renders to a document
  whose `viewBox` is `0 0 1e+21 76`; a 1 MB label renders in under a second to
  a canvas 7.2 million units wide;
- **input size** — a 1 MB single label, and 200 000 comment lines, both render.

A consumer that renders untrusted `.fd` **at scale, or on request, MUST impose
its own limits** — wall-clock timeout, output-size cap, and a source-size cap —
because this specification imposes none and a conforming engine may impose
none. That is the honest statement of the posture: FigDown's answer to
resource exhaustion is *external*, and saying otherwise would be inventing a
guarantee.

**XML-illegal characters in a source are REFUSED (`XML-CHARACTER-LEGALITY`).** An artifact is an
XML document — §7 embeds the source inside one — and XML forbids a fixed set
of code points **outright**: they may not appear literally and may not be
written as a character reference either, so no escaping rescues them. A source
containing one therefore cannot produce a well-formed artifact, and a renderer
MUST refuse it rather than write a figure no conforming reader will open. The
refusal is a **parse-time line error naming the code point**, on §8's existing
principle and not a new one: a document with errors renders nothing, because a
drawing the engine cannot stand behind is worse than no drawing.

The refused set is XML's, not a taste for printability:

- **U+0000–U+0008, U+000B, U+000C, U+000E–U+001F.** U+0009 (tab), U+000A
  (newline) and U+000D (carriage return) are **legal and stay legal**.
- **U+FFFE and U+FFFF.**
- **An unpaired surrogate.** A correctly paired astral character is a
  character and is accepted.

**U+007F and the C1 range are legal XML characters and are NOT refused.** A
rule that also ate them would be a different and worse defect — silently
dropping text an author wrote — so the gate asserts acceptance of those in
the same breath as it asserts refusal of the others.

Two diagnostics carry this, one per rule, because a single sentence would
misdescribe half the set and send the author to the wrong fix:

```
Line N: illegal character U+0000 — XML forbids it, so the artifact could not be
        well-formed (below U+0020 only tab, newline and carriage return are allowed)
Line N: illegal character U+FFFE — XML forbids it, so the artifact could not be
        well-formed (U+FFFE, U+FFFF and unpaired surrogates are not characters —
        no escape writes one)
```

One error per **distinct** character per line: a 1 MB label of NULs is one
defect and one fix (§8.1's duplication rule). Before 0.4 such a source
parsed clean, rendered clean, and produced an artifact no XML reader would
open; `spec/migrations.md` records the behaviour change.

~~**One thing is still UNSPECIFIED, recorded rather than omitted.** **Error
recovery** (§8.3), so the *set* of diagnostics a hostile document produces is
not a contract.~~ **CORRECTED at `ERROR-RECOVERY-MODEL` (2026-08-22): error recovery IS
specified** (§8.3), so the *set* of diagnostics a hostile document produces is
now a contract, up to §8.3.8's residue — which is a choice between two
messages on one line, never a difference in how many. Nothing in this section
depended on it either way; the sentence is struck rather than deleted because
a reader who came here for the honest limit must see that it moved.

### 15.6 What `data-sha256` proves, and what it does not

`data-sha256` is an **integrity** value and nothing more. Precisely:

- It is the SHA-256 of the **source**, never of the artifact (§7). It answers
  one question: *is the picture in this file the picture this source
  describes?* A mismatch means the artifact is stale — the source moved and the
  figure was not rebuilt — which is the failure `gate:artifact` exists to
  catch and has caught twice in this repository's history.
- **It authenticates nothing.** It is not a signature, not a MAC, and carries
  no key. Anyone who can change the artifact can change the hash inside it, so
  it detects *drift and corruption*, never *tampering by someone who wanted it
  undetected*. An attacker who edits the embedded source and recomputes the
  hash produces a perfectly self-consistent artifact.
- **It says nothing about the drawing.** The hash covers the source text; the
  rendered geometry is covered only through `data-engine-version` plus `RENDERING-DETERMINISM`'s
  same-source-same-version byte-identity promise, which under §13 does not hold
  *across* 0.x versions. A matching hash does not mean two renderings agree.
- **A self-consistent artifact can still be wrong.** Every artifact in the two
  incidents §7 records was internally consistent — its recorded hash matched
  its own embedded source — and every one of them showed a figure its `.fd` no
  longer described. That is why the check that matters compares the artifact
  against the **paired `.fd` on disk**, not against itself.

Provenance and authenticity are out of scope for v0.x and are not planned as
core syntax; a publisher that needs them signs the artifact by its own means,
outside this specification.

### 15.7 Claim → evidence

Every normative claim above, and where it is demonstrated. A row with no
evidence is not in this table because it is not in this section.

| # | Claim (§) | Evidence |
|---|---|---|
| 1 | No `script` element in any artifact (§15.2.1) | `gate:safesvg` B, over every committed `.svg` in the tree (86: 73 renderer-output artifacts judged, 9 hand-edited counter-examples named and skipped, 4 fork-written files pinned as PENDING — `RESERVED-PREFIX-ENFORCEMENT`) + 3 fixtures × 3 render modes every run; negative control: a doctored artifact with `<script>` fails B |
| 2 | No attribute beginning `on` (§15.2.2) | `gate:safesvg` C (absolute rule, independent of the allowlist); negative control: `<svg onload=…>` fails C |
| 3 | No `foreignObject` (§15.2.3) | `gate:safesvg` B; negative control fails B |
| 4 | No `href`/`xlink:href`/`src`, no external `url(…)`, no `@import` (§15.2.4) | `gate:safesvg` C and D; negative controls (`<image href=https://…>`, `style="fill:url(https://…)"`, `@import`) each fail |
| 5 | Output vocabulary is closed at 16 elements / 63 attributes (§15.2.5) | `gate:safesvg` B and C enumerate it and re-assert it every run over `git ls-files '*.svg'` plus the adversarial fixtures rendered fresh; the set's original derivation (a one-shot 2026-08-19 sweep of the rendering documents, `XML-CHARACTER-LEGALITY`) is recorded in §15.2 and is not re-run. The last three names are emitted under `with-a11y` only; any name outside the set fails. `style=` values are enumerated too (three `cursor:` values) |
| 6 | The only absolute URI is the SVG namespace, as `xmlns` (§15.2) | `gate:safesvg` E, over markup regions only — an absolute URI written in a *label* is text and is not a finding |
| 7 | Hostile text reaches the model unescaped and unstripped (§15.3) | `conformance/cases/950-adversarial-label-markup.model.json` (title, class meaning, group/node labels, three edge-label positions, six `note=` strings); `951-adversarial-description-markup.model.json` (four `description=` strings) |
| 8 | Hostile text reaches the artifact as text, never markup (§15.3) | `gate:safesvg`, fixtures 950–952 rendered fresh in **all three** render modes (default, `with-title`, `with-a11y`): every byte classified, hostile tokens present in the document and in no markup region. The third mode is the one that carries a hostile `title` into a root `<title>` and a derived `<desc>` |
| 9 | The embedded source cannot terminate its own CDATA container (§15.4) | `conformance/cases/952-adversarial-metadata-breakout.fd` + `gate:safesvg` F: source recovered byte-identical, `data-sha256` equal to the hash of those bytes, no `script` element among the scanned tags |
| 10 | Extreme numerics are refused by the grammar (§15.5) | `conformance/cases/954-pin-extreme-numeric-refusals` (`1e12`, `NaN`, `Infinity`, `-1e12`, `1e400` inside `at=(…)` — five golden error lines), `367-pin-extent-strict-value` (five width values) and `361-pin-errors` (the paren-point rule) |
| 11 | Resource limits are UNSPECIFIED (§15.5) | **No gate — by construction.** The measurements quoted are a record, not a bound; nothing asserts a ceiling because none is specified |
| 12 | XML-illegal characters in a source are refused, and legal ones are not (§15.5) | `conformance/cases/953-source-illegal-xml-characters` — eleven golden error lines over both diagnostics, with a same-file node carrying tab, U+007F, C1 and an astral character that must produce none. Plus `gate:safesvg` A upstream: 12 forbidden code points must error and 5 legal ones plus a paired astral character must not, asserted against the live engine every run |
| 13 | `data-sha256` detects staleness (§15.6) | `gate:artifact` — recorded hash vs the SHA-256 of the paired `.fd`, over every artifact it finds, every run |
| 14 | `data-sha256` authenticates nothing (§15.6) | **No gate, and none is possible** — this is a statement about what the mechanism is, not a behaviour to test |
