# DGMO Language Specification
> **Authoritative reference** for the DGMO diagram language. This document describes what is valid syntax. If it is not in this document, it is not valid DGMO.
> **1.0 freeze:** DGMO syntax freezes at the **1.0 release tag** — after 1.0, any change to valid syntax is a major-version bump. Pre-1.0 the language was deliberately tightened toward one canonical form per construct; the last cleanups shipped through 0.44 (channel-named value ramps in 0.42, chart-type consolidation 50→44 + alias removal in 0.43 per #23–28, `chord` folded into `arc` + `layout chord` in 0.44). Retired legacy/ambiguous forms include: bare `description`, PERT `analysis`, C4 bare same-line tail, timeline positional duration (→ `duration:`), gantt legacy scheduling, and the standalone `chord` / chart-type-alias keywords; bare tag values now auto-assign a palette color. Each removed form **fails loudly** — a generic parse error, or a targeted hint where one survives (e.g. a C4 bare tail names the `description:` fix and the diagram does not render). See `dgmo-language-spec-decisions.md` § "1.0 Freeze Decisions".
>
> **Diagnostic codes (0.43.0, decision #28):** the dedicated `E_*_REMOVED` family was **deleted** for minimal surface — removed forms fail however they naturally fail, not via a named code. Treat every `E_*_REMOVED` reference below as **historical**; re-verify behavior against the parser. Note two forms are deliberately **accepted, not errors**: data-row numeric values may use thousands-grouping commas (`1,000` → 1000) or underscores (`1_000` → 1000), and separator commas between values are tolerated (values are canonically space-separated). _(2026-07-05: the C4 bare-tail case, which regressed to a silent drop after #28, was restored to a hard error — see §"C4".)_
## Table of Contents
1. [Universal Constructs](#1-universal-constructs)
2. [Universal Name Handling](#2-universal-name-handling)
3. [Sequence Diagrams](#3-sequence-diagrams)
4. [Infrastructure Diagrams](#4-infrastructure-diagrams)
5. [Flowchart Diagrams](#5-flowchart-diagrams)
6. [State Diagrams](#6-state-diagrams)
7. [Org Charts](#7-org-charts)
8. [C4 Architecture Diagrams](#8-c4-architecture-diagrams)
9. [Entity-Relationship Diagrams](#9-entity-relationship-diagrams)
10. [Class Diagrams](#10-class-diagrams)
11. [Kanban Boards](#11-kanban-boards)
12. [Sitemap Diagrams](#12-sitemap-diagrams)
13. [Gantt Charts](#13-gantt-charts)
14. [Boxes and Lines Diagrams](#14-boxes-and-lines-diagrams)
15. [Timeline Diagrams](#15-timeline-diagrams)
16. [Data Charts](#16-data-charts)
17. [Visualizations](#17-visualizations)
18. [Mindmap Diagrams](#18-mindmap-diagrams)
19. [Wireframe Diagrams](#19-wireframe-diagrams)
20. [Tech Radar Diagrams](#20-tech-radar-diagrams)
21. [Cycle Diagrams](#21-cycle-diagrams)
22. [Journey Map Diagrams](#22-journey-map-diagrams)
23. [Pyramid Diagrams](#23-pyramid-diagrams)
24. [Ring Diagrams](#24-ring-diagrams)
- [RACI Matrices](#24a-raci-matrices-raci--rasci--daci)
- [Map Diagrams](#map-diagrams)
25. [Colon Usage Summary](#25-colon-usage-summary)
26. [Authoring Rules (Generators Read This First)](#authoring-rules)
27. [Swimlane Diagrams](#27-swimlane-diagrams)
28. [Event Line Diagrams](#28-event-line-diagrams)
29. [Version-Control Diagrams](#29-version-control-diagrams)
30. [Block Diagrams](#30-block-diagrams)
31. [Sketch Diagrams](#31-sketch-diagrams)
32. [Family Diagrams](#32-family-diagrams)
---
## 1. Universal Constructs
These patterns are shared across all or most diagram types.
### 1.1 Chart Type Declaration (First Line)
```
[Title]
```
- Space-separated, NO colon
- Title is optional
- Examples: `bar Treasure Hauls`, `sequence Auth Flow`, `gantt Product Launch`
### 1.2 Comments
```
// comment text
```
- Full-line only (no inline comments after code)
- `#` is NOT a comment character
- One documented carve-out: **PERT (§13A)** accepts both `#` and `//`, full-line and trailing-after-whitespace — the only chart type with inline comments. Everywhere else `#` is content.
### 1.3 Tag Declarations
```
tag GroupName as
Value1 color
Value2 color
```
- `tag` keyword, NO colon
- **Name**: a single identifier (`[A-Za-z_][A-Za-z0-9_-]*` — letters, digits, underscore, hyphen), e.g. `Team`, `Trust-Zone`. For a multi-word display name with **spaces**, quote it: `tag "Trust Zone" as tz`. The quoted text becomes the legend label; internally it slugs to a DOM-safe key (`Trust Zone` → `trust-zone`) used for assignment, matching, and `active-tag`. Always pair a quoted name with an `as ` so values stay terse (`Node tz: client`). An *unquoted* multi-word name is ambiguous (the trailing word is read as an alias) and is not a spaced name.
- Alias: optional postfix `as ` per §2A (universal alias syntax — `[A-Za-z][A-Za-z0-9_]{0,11}`)
- Inline values also supported: `tag Priority as p Low green, High red`
- Color follows the value as a bare trailing token (see §1.5). Capitalize the color word (`Red`, `Yellow`) to keep it as a literal value with no color.
- **Color is optional — a bare value auto-assigns a palette color.** A value declared without an explicit color (`High`, not `High red`) is not an error: at parse time it is given a deterministic categorical color drawn from the recognized color names (`red`, `orange`, `yellow`, `green`, `blue`, `purple`, `teal`, `cyan` — neutrals excluded), cycling in that order. Auto-assignment **skips any color already used by an explicit entry in the same group** (including explicit entries that appear *after* the bare one), so `High` / `Low red` gives `High` a non-red color. An explicit `Value color` always wins. This lets you sketch a group as a plain value list (`tag Priority as p High, Med, Low`) and let the palette pick distinct colors. This is the language's single canonical categorical rotation — chart sections that rotate colors (e.g. ring layers, §24.6) cite this order; pyramid's primary-mix ramp (§23.6) is the one documented exception.
- **First entry is the default** — applied to nodes that don't carry an explicit value for this group. Reorder the entries to change the default. Three charts opt out of first-value defaulting: **sketch** and **body** render untagged shapes neutral gray (meaning is absent until assigned — decision #33), and **swimlane** untagged nodes fall back to the lane shade (tag → symbol → lane cascade, §27.7)
- Must appear before diagram content
- `none` is reserved — cannot be used as a tag group name
- **The first declared group is active by default** — declaring a tag group colours nodes by that group immediately; the legend renders it expanded/active with the other groups as collapsed pills. `active-tag ` is therefore **only needed when ≥2 colouring dimensions exist** (multiple tag groups, or a group plus a score ramp) and you want one *other than the first-declared* active. With a single tag group, `active-tag` is redundant. In the app a reader can click any legend pill to flip the active group (live preview only — it does not edit the source)
- **Opt-out**: `active-tag none` **suppresses all tag colouring** — every node renders neutral and all groups stay collapsed. This is the *opposite* of omitting `active-tag` (which activates the first group); use it to author the quiet, no-colour default deliberately (case-insensitive: `none`, `None`, `NONE`)
- **Legacy bare shorthand** (`tag Priority p`) and **explicit `alias` keyword** (`tag Priority alias p`) emit `E_TAG_SHORTHAND_REMOVED` per TD-18
**Diagram types that support tags**: sequence, infra, state, org, mindmap, c4, er, kanban, gantt, sitemap, timeline, boxes-and-lines, swimlane, pert, journey-map, treemap, map, event-line, block, sketch, family, body, bracket
> This list is enforced, not hand-maintained: it must match `TAG_SUPPORTING_TYPES` in `dgmo/src/completion.ts` (derived from `STRUCTURAL_KEYWORDS`, the parser-validated source of truth) and is checked by the completion-conformance suite.
#### Strict ordering: tag declarations must precede content
Tag declarations (and their aliases) feed the chart's reserved-key registry (§1.4.3) — that registry must be **finalized before the parser enters content-line mode**. DGMO is a single-pass parser with no lookahead, so a tag alias declared after the first content line is unreachable: any earlier line that *could* have used the alias was already classified under the pre-declaration registry.
A `tag` block appearing after the first non-tag content line emits `E_TAG_DECLARED_AFTER_CONTENT`. The declaration is still parsed (so the legend renders), but no upstream content lines retroactively pick up the alias.
```
sequence Voyage
tag Concern as c // ✅ before content
Alice -login-> Bob c: Auth
[Backend]
tag Concern as c // ❌ E_TAG_DECLARED_AFTER_CONTENT
```
This applies uniformly across all chart types that support tags.
### 1.4 Metadata Grammar
DGMO has **one** metadata grammar that every chart type uses. Entity metadata may be written same-line after the name, or as indented lines beneath the entity. Both forms are valid wherever metadata is supported; per-chart-type preferred form is listed in §26.
```
// Same-line form (canonical for most chart types)
EntityName k: v, k2: v2
API Gateway c: Auth // name = "API Gateway"; meta = { c: Auth }
Alice -login-> Bob c: Auth // edge metadata, no delimiter
20bd Database Schema progress: 100 // gantt: leading-duration prefix preserved
// Indented form (canonical for org, c4; available everywhere)
API Gateway
c: Auth // attribute — `c` is a declared tag alias
description: Main gateway
```
The `|` operator that delimited metadata in pre-0.18.0 DGMO is **removed** as a metadata delimiter. Before 0.43.0 a stray `|` emitted `E_PIPE_OPERATOR_REMOVED`; as of 0.43.0 (#28) that guard is gone, so a `|` outside its surviving uses (§1.4.5) is no longer a dedicated error — it falls through to normal parsing (treated as a literal / ignored).
#### 1.4.1 Same-line form
The parser scans the line left-to-right and flips into **metadata mode** the first time it encounters a whitespace-delimited token whose shape is `:` (a key followed by a colon, with the key matching the chart type's reserved-key registry or a declared tag alias). Everything to the left of that token is the entity's **name region**; everything to the right is comma-separated `key: value` pairs.
```
Foo k: v // name = "Foo", meta = { k: v }
Foo k: v, k2: v2 // name = "Foo", meta = { k: v, k2: v2 }
Foo green k: v // name = "Foo", color = green, meta = { k: v } (color peeled from name region — §1.5)
Foo as f k: v // name = "Foo", alias = f, meta = { k: v }
```
**Whitespace around `:` is tolerated** — tokenization treats `c: Auth`, `c :Auth`, `c : Auth`, and `c:Auth` identically. The value start is after `:`, with leading whitespace trimmed.
**Quoted values escape the comma terminator.** Wrap a value in `"..."` when it contains commas.
```
Foo description: "My, complicated, value" // meta = { description: "My, complicated, value" }
Foo description: "with spaces", color: red // meta = { description: "with spaces", color: red }
```
**No line continuation.** Same-line metadata is single-line. A trailing comma on a metadata line is a parse error. For long metadata sets, use the indented form.
**Empty values are dropped.** `Foo c:` (no value) emits `W_EMPTY_METADATA_VALUE` and the key/value pair is discarded from the entity's metadata.
**Quoted names tokenize as one unit before the first-`:`-token scan.** A colon inside a quoted name does not trigger the metadata cut.
```
"Auth: Service" // name = "Auth: Service" (single token)
"Order | Items" k: v // name = "Order | Items", meta = { k: v }
```
#### 1.4.2 Indented form (reserved-key dispatch)
An indented line of shape `key: value` attaches as metadata to the parent entity **only when `key` matches the chart type's reserved-key registry or a declared tag alias**. Otherwise, the line falls through to the chart type's own grammar (class members, RACI role assignments, ER columns, function entries, sequence notes, etc.).
```
API Gateway
c: Auth // attribute — `c` is a declared tag alias
latency-ms: 50 // known-schema property (colon required — see §4.3)
Ship // class
+ name: string // member — `+` prefix; not a reserved-key match
description: Capital ship // attribute — `description` is reserved
[Departure] // RACI phase
Plot the course // task
Cap: A // role assignment — `Cap` is a declared role
Nav: R
```
Indented lines without a colon remain **structural children** (sub-nodes, columns, dependencies, cards, etc.). ER columns (`id int pk`) are space-separated known-schema properties that don't carry colons; the dispatch is unambiguous. Infra node properties require colons (`latency-ms: 50`) — see §4.3.
**Drift warning.** When an indented `key: value` line where `key` is in the reserved-key registry appears at the same indent level as preceding structural children, the parser emits `W_ATTRIBUTE_AT_PARENT_INDENT` — the attribute attaches to the parent above, but a reader could plausibly intend it to attach to the preceding structural child. The hint: indent further if you meant it on the preceding child.
#### 1.4.3 Reserved-key registry
Each chart type publishes a closed set of reserved attribute keys. These keys, plus any declared tag aliases (§1.3), trigger metadata dispatch on indented lines and the same-line first-`:`-token cut. Everything else falls through to chart-specific grammar.
| Chart type | Reserved keys (in addition to declared tag aliases) |
| --- | --- |
| sequence | `color`, `description`, `role`, `collapsed`, `position` |
| infra | `color`, `description`, `collapsed`, `icon` |
| flowchart | *(no metadata)* |
| state | *(tag-group aliases — tags newly granted, decision #48)* |
| org | `color`, `description`, `role`, `location`, `email`, `phone`, plus per-chart custom keys (free-form indented attributes) |
| c4 | `color`, `description`, `tech`, `type`, `collapsed` |
| er | `color`, `description`, `domain` |
| class | `color`, `description` |
| kanban | `color`, `description`, `wip`, `assignee`, `due`, `priority` |
| sitemap | `color`, `description`, `status` |
| gantt | `color`, `description`, `duration`, `offset`, `progress`, `start` |
| pert | `color`, `description`, `confidence`, `collapsed` |
| boxes-and-lines | `color`, `description`, `heat` |
| timeline | `color`, `description`, `duration` |
| mindmap | `color`, `description`, `collapsed`, plus per-chart priorities/status keys declared as tag aliases |
| tech-radar | `color`, `description`, `quadrant`, `ring`, `trend` |
| cycle | `color`, `span`, `description` (`width` is edge-only) |
| journey-map | `score`, `emotion`, `description`, `pain`, `opportunity`, `thought`, `color` (decision #13, persona long form) |
| pyramid | `color`, `description` |
| ring | `color`, `description` |
| raci | `color`, `description`, plus declared roles (which dispatch as role assignments, not attributes) |
| wireframe | *(uses trailing-keyword flag list — see §19.5)* |
| treemap | `heat`, plus tag-group aliases |
| swimlane | *(tag-group aliases; `note:` deferred)* |
| event-line | *(tag aliases; `collapsed` via era lines)* |
| version-control | `id`, `tag`, `type`, `order` |
| block | `span` |
| sketch | `shape`, `at` |
| family | closed GEDCOM set — see §32.6 |
| body | *(tag aliases)* |
| bracket | *(tag aliases; seed/score keys per §35)* |
| map | registry in §24B.9 (`heat`, `size`, `width`, `label`, `style`, `clock`) |
| goal | *(no metadata)* |
| countdown | *(no metadata)* |
| clock | *(no metadata)* |
Keys not in a chart type's registry are not metadata. Tag aliases declared via `tag Group as x` are added to that chart's effective registry for the duration of the parse.
#### 1.4.4 Diagnostic catalog
> The `*_REMOVED` rows below were **deleted in 0.43.0 (#28)** and no longer fire — see the 0.43.0 update note at the top of this spec. They are kept here, marked, for historical reference.
| Code | Severity | Fires when |
| --- | --- | --- |
| ~~`E_PIPE_OPERATOR_REMOVED`~~ | *removed 0.43.0* | (Historical) A `|` appeared outside its surviving uses (wireframe option braces `{A | B}`, in-arrow label characters per §1.10, quoted-string content). Now: a stray `|` falls through to normal parsing (literal / ignored), no dedicated error. |
| `W_EMPTY_METADATA_VALUE` | warning | A metadata pair has an empty value (`Foo c:`). The pair is dropped from the entity's metadata. |
| `W_ATTRIBUTE_AT_PARENT_INDENT` | warning | An indented attribute line (`key: value` where `key` is reserved) appears at the same indent level as preceding structural children, ambiguating which entity it attaches to. |
| ~~`E_SEQUENCE_BARE_POSITION_REMOVED`~~ | *removed 0.43.0* | (Historical) A sequence participant used the retired bare-keyword `position N` ordering shorthand; use colon-keyed `position: N` (§2.2). Now: no dedicated error — falls through to normal parsing. |
| ~~`E_DATA_COMMA_REMOVED`~~ | *removed 0.43.0* | (Historical) The short-lived 0.30.0 freeze hard-errored commas in data-chart VALUE position. Reversed by #28: commas are now **accepted** — thousands grouping (`Revenue 1,000` → 1000) and separator commas (`Q1 400, 700`) both parse. Values are canonically space-separated; `1_000` also works. Not an error. |
#### 1.4.5 Surviving uses of `|`
The `|` character is **not reserved** in DGMO 0.18.0; it survives as a literal in three specific contexts:
- **Wireframe dropdown options:** `{Option A | Option B | Option C}` — inside `{...}` braces, `|` separates dropdown options (§19.3).
- **In-arrow label characters:** `A -file|name-> B` — `|` inside an arrow label is preserved verbatim (§1.10).
- **Quoted name characters:** `"Order | Items"` — inside `"..."` quoted strings, `|` is a literal name character.
Outside these three contexts, `|` used to trigger `E_PIPE_OPERATOR_REMOVED`; as of 0.43.0 (#28) that guard is gone, so a stray `|` is simply parsed as a literal / ignored rather than flagged. `dgmo migrate` still mechanically converts legacy pipe content.
### 1.5 Color Suffixes
Color is set by typing the color name at the end of a label, lowercase. Example: `Done green` colors Done green. Eleven colors exist: `red`, `orange`, `yellow`, `green`, `blue`, `purple`, `teal`, `cyan`, `gray`, `black`, `white`. To use a color word as a literal label, capitalize it: `Red` stays as the word `Red`.
> **These eleven names are the ONLY valid colors. Hex (`#e6194b`), `rgb(...)`/`hsl(...)`, and CSS keyword colors (`crimson`, `pink`, …) are NOT supported anywhere in DGMO** — not on groups, series, nodes, edges, tags, or any other element. A hex or CSS value is rejected with an **error** diagnostic and the element falls back to its default / auto-assigned palette color. The only way to specify a color is one of the eleven names above. This is deliberate: a named color resolves to the right hex per active palette and light/dark theme, so the same source stays palette-portable — a hardcoded hex would not.
```
Label color // bare trailing color token
Done green // value=Done, color=green
Senior Engineer red // value="Senior Engineer", color=red
Red // value=Red, no color (capitalized → escape hatch)
```
**The universal rule** — color trails the label:
> Color is the trailing whitespace-delimited token of a label region, when that token (case-sensitive, lowercase) is one of the 11 names above. Otherwise the label region has no color.
The "label region" is everything left after the parser strips off structural terminators it owns: same-line metadata (§1.4.1), `as ` postfix, numeric values, date ranges, structural brackets, arrow constructs. The color rule operates only on the label region — never on a raw line — so `Tortuga Distillery orange 3000` is `{ label: "Tortuga Distillery", color: "orange", value: 3000 }`: the parser splits off `3000` as the flow value first, then `orange` peels off as the trailing color.
**Cut order is pinned.** On any line that could carry all of (a) metadata, (b) an `as ` postfix, and (c) a trailing-token color, the parser strips them in this order:
1. **Metadata cut (§1.4.1)** — the first `:` token where `key` is in the chart's reserved-key registry or a declared tag alias splits the line into name-region + metadata (left-to-right). Color tokens that appear *inside* a metadata value are NOT extracted as the entity's color — they belong to the value.
2. **Trailing-token color (§1.5)** — applied to the name region. The last whitespace-delimited token, when it is exactly a lowercase color name from the 11-name palette, is peeled as color.
3. **`as ` postfix** — stripped from the right-hand side of what remains of the name region. The alias is registered against the canonical name.
Step 2 runs before step 3 because in source order the alias sits *between* the label and the trailing color (` as `); peeling the color first exposes the alias at end-of-string. Both forms work, so `Spring as s green` and `Spring green` both produce the expected (alias, color) result.
```
Spring green c: Auth // name = "Spring", color = green, meta = { c: Auth } — color is in name region (before metadata cut)
Spring c: Auth green // name = "Spring", meta = { c: "Auth green" } — `green` is part of the metadata VALUE, not the entity color
Spring as s green c: Auth // name = "Spring", alias = s, color = green, meta = { c: Auth }
```
To color the *entity* alongside other metadata, write the color before the metadata cut. To color something inside a metadata value, use the explicit `color:` key in metadata (the metadata value can contain a color word as text, but it never feeds back into the entity's color slot).
**Aliases come between the label and the color in the name region.** `as ` sits *between* the label and the trailing color token — ` as `. After the metadata cut runs first, color is the trailing token of what remains of the name region.
**Where the rule applies**:
- Tag values: `Done green`, `Blocked red`. The color is **optional** here — a tag value with no trailing color (`Done`) auto-assigns a deterministic palette color at parse time (see §1.3). This auto-assignment is specific to declared tag-group values; everywhere else, omitting the color simply leaves the element uncolored.
- Kanban columns: `[Done] green` (color follows the closing bracket)
- Venn items: `Swordsmanship as sw red` (color trails the alias)
- Quadrant position labels: `top-right Promote green`
- Gantt / timeline eras and markers: `era 1710 -> 1716 Rise green`
- Data-chart series + data rows: `series Cloud blue, Legacy red` and `North 1500 red`
- Sankey nodes + link lines: `Sugar Plantations green`, `Source -> Target 3000 red`
- Cycle / pyramid / ring / RACI / boxes-and-lines node labels: `Spring green`
- ER tables
- Scatter `[Category]` headers and arc `[Group]` headers
- RACI phase headers + roles-block entries
- Swimlane `lane` lines
- Version-control branch declarations
- Bracket `[Side]` headers
- Map regions and POIs: `Texas red`
- Title lines of goal / countdown / bracket (see "Title-line accent" below)
**Title-line accent.** A trailing color token on the chart declaration line sets the chart accent on the types that have one: **goal** (meter/figure color, §34.5), **countdown** (figure tint, §36.1), and **bracket** (winner accent, §35 — its former `accent ` directive is a legacy alias). Other chart types currently ignore a title-line trailing color.
**Long form `color: ` is reserved for multi-key metadata.** Use `color: ` only when another metadata key (`description:`, `span:`, `width:`, `quadrant:`, …) needs to accompany the color. When color is the only thing being set, write it as a trailing token in the name region.
```
Spring green // canonical — color is the only metadata
Spring green description: First // trailing-token color + same-line metadata
Spring color: green, icon: ❄ // long form REQUIRED when color rides with another key in metadata
-Label-> color: red, width: 6 // edges have no trailing-token slot; long form is the only path
```
One narrow exception accepts long-form-only color:
- **Cycle edges** (and any other edges that carry a color) — edges have no trailing-token slot, so `color: ` is the only form.
Journey-map personas follow the universal rule: `persona Nadia green` colors the persona green; capitalize the word (`persona Nadia Green`) to keep it as literal name text. The long form `persona color: ` is also accepted.
**Accepted tradeoffs**:
- **No typo diagnostics**: an unrecognized trailing token is silently treated as label text. `Done grren` (typo) → `{ value: "Done grren", color: undefined }`, no warning. Internal corpus has its own near-miss smoke test; user content gets no help.
- **Case-sensitivity is the escape hatch**: only lowercase recognized. `Red`, `Yellow`, `Green` stay as labels — useful for traffic-light tag groups that want the color word as the literal name.
- **No edge color**: edges on flowchart, state, and sitemap diagrams have no color slot. `A -(red)-> B` and `A -yes-> B` both render with the default theme color.
- **11-name palette is a frozen public contract**: adding a 12th color name is itself a breaking grammar change because any user diagram that already has the new color word as a label would silently change behavior. See [decision log](./dgmo-language-spec-decisions.md).
- **No hex / CSS colors**: hex (`#e6194b`), `rgb(...)`, `hsl(...)`, and CSS keyword colors are rejected. Where a color is parsed from an explicit slot (e.g. a `[group]` header), a hex value emits an **error** diagnostic; where color comes from the bare trailing-token rule (which silently ignores any non-name), the hex token simply stays as label text. Either way it never colors the element.
### 1.6 Indentation
- Spaces or tabs (1 tab = 4 spaces)
- Determines hierarchy and block scope
### 1.7 Edge Notation — Prefer Indented-Under-Source
Many diagram types support both flat (`Source -label-> Target`) and indented forms for edges. **Always prefer the indented form** — it is more concise, avoids repeating the source name, and reads as a natural dependency tree:
```
// Preferred — indented under source
API
-routes-> UserService
-routes-> ProductService
// Avoid — repetitive flat form
API -routes-> UserService
API -routes-> ProductService
```
When a node has outgoing edges, declare the node first, then indent its edges below it. Use the flat form only for cross-links where the source node was already declared earlier — re-declaring a node just to indent an edge under it creates a duplicate.
**Combining metadata with indented edges.** Put same-line metadata on the declaration line and indent the edges below it — splitting metadata onto a separate node re-declaration emits a `Duplicate node` warning. Indented metadata (key in the chart's reserved-key registry per §1.4.3) may also appear among the indented children, intermixed with edges; the parser dispatches per-line.
```
// Preferred — one declaration carries both metadata and edges
API description: Main gateway
-routes-> UserService
-routes-> ProductService
// Also valid — metadata on declaration line, more metadata indented alongside edges
API description: Main gateway
tech: Node.js // indented attribute (reserved-key dispatch)
-routes-> UserService
-routes-> ProductService
// Avoid — declaring API twice produces a duplicate-node warning
API description: Main gateway
...
API
-routes-> UserService
```
When mixing indented attributes and structural children, watch the indentation: an attribute line at the same indent as a structural child attaches to the parent (the node declared above), not to the structural child. The parser emits `W_ATTRIBUTE_AT_PARENT_INDENT` when an attribute follows a structural child at the same indent level — indent the attribute one more level if you meant it on the child.
**Edge targets must already exist.** A `-> Target` reference (or labeled variant) must point at an entity that has been declared on a previous line or is being declared inline by the edge itself (e.g. pert's `-> dest 1 2 4` forward declaration with durations). Bare `-> Unknown` to an undeclared entity emits a `Target not found` error. The same rule applies to group references: `-> [GroupName]` requires the group to exist (see §1.8).
Diagram types supporting indented-under-source: **infra, flowchart, C4, ER, class, sitemap, boxes-and-lines, gantt, pert, swimlane, cycle, sketch, map** (POI hub edges and route legs).
Map **native** edges are the one anti-carve-out: outside a route, indented `-> dest` is a parse error (§24B.6) — map native edges are flat-only.
### 1.8 Groups / Containers
```
[Group Name]
[Group Name] k: v, k2: v2
```
- Bracket-enclosed name
- Optional same-line metadata after the closing bracket (per §1.4.1), or indented attributes below (per §1.4.2)
- Indented content below belongs to the group
- Trailing-token color on groups — examples include kanban columns (`[Column] green`) and era/marker labels (`era 1710 -> 1716 Rise green`)
- **Group references must be declared.** Edges that target a group via `-> [Name]` require the group to be declared elsewhere in the file; group references do not auto-create groups (an undeclared group reference emits a `Group not found` error). Forward references — using a group before its declaration line — are not allowed.
**Collapsing groups.** The canonical form is a bare `collapsed` trailing flag on the group/container line (e.g. `[Backend] collapsed`, `[Done] collapsed`). The older `collapsed: true` metadata (same-line or indented) remains accepted as legacy. Wireframe's flag-enum `collapsed` and block/sketch's comma-list `collapsed` are the same token in their native grammars.
| Chart type | Example |
| --- | --- |
| sequence (§2.2) | `[Auth System] collapsed` |
| infra (§4.6) | `[Backend] collapsed` |
| mindmap, pert | `Research collapsed` |
| gantt (§13) | `[Backend] collapsed` |
| kanban (§11) | `[Done] collapsed` (columns) |
| state | `[Fulfillment] collapsed` (composite groups) |
| c4 | `[API Layer] collapsed` |
| event-line (§28) | `[Golden Age] collapsed` (eras) |
| block (§30) | `[Wide] span: 2, collapsed` (comma-list) |
| sketch (§31) | `[Armory] at: 0 2, collapsed` (comma-list) |
| wireframe (§19.5) | trailing-keyword flag list |
The authoritative support list, stated once here: **sequence, infra, gantt, kanban, mindmap, pert, state, c4, event-line (eras), block, sketch, wireframe**. treemap and timeline collapse is interactive-only (no source marker; view-state, decision #32). §26.3 and §26.7 defer to this list.
A group whose name ends in the word "collapsed" uses the standard capitalize/quote escape (same class as trailing colors): capitalize it (`[Mission Collapsed]`) or quote the name (`["Mission collapsed"]`) to keep the word as name text.
The `collapsed` flag is **portable view-state**: because it lives in the source text, every renderer (the app, the `dgmo` CLI, remark-dgmo, Obsidian, code-fence embeds) reproduces the collapsed view from the `.dgmo` alone — a diagram in a code fence looks the way its author left it, not fully expanded. In the app, collapsing/expanding a node writes/removes this marker in the source (the source stays the single source of truth). A runtime `viewState.cg` (share-link / live app state) is applied *in addition* to the source markers. *(Mindmap honored this field starting 2026-07; see the decision log. The broader effort to give every view-state key a native spelling is tracked separately.)*
### 1.9 Boolean Options
```
option-name // on
no-option-name // off
```
- Bare keyword = on; `no-` prefix = off
- Must appear before diagram content
**Cross-cutting boolean directives** (recognized in every chart type that has the corresponding rendering surface):
| Directive | Effect |
| --- | --- |
| `fill-tint` | The default, spelled explicitly — shapes get the canonical 25% tint fill with a solid intent-color outline |
| `fill-solid` | Render nodes/bars at full intent saturation instead of the canonical 25% tint |
| `fill-outline` | No fill — shapes take the theme background fill; the intent color is carried entirely by the outline |
| `no-title` | Hide the diagram banner title in the rendered output (does not mutate the parsed model) |
| `no-legend` | Suppress the legend on any chart type that renders one (universal — includes data charts; added by decision #48) |
| `legend-inline` | Render the title and its tag/series legend on **one line** — title left, legend flushed right — instead of stacking the legend below a centered title, reclaiming a header row. Honoured by every chart with a top-center capsule legend: the data charts (bar, line, radar, scatter, function) and the structured tag-legend charts (state, treemap, block, event-line, boxes-and-lines, er, class, family, infra, sequence, sketch, bracket, gantt, pert). On any other chart type it emits a **warning** (`W_LEGEND_INLINE_UNSUPPORTED`) — "not supported for this chart type; title and legend render stacked" — so it never silently pretends to work; the chart still renders. Auto-falls back to the stacked header (and re-centers the title) when the legend can't fit on a single row beside the title — so the diagram is always valid regardless of title length, entry count, or width (decision #50) |
The three `fill-*` directives form one **fill family** and are mutually exclusive; when more than one appears, the last one wins. The family replaces the earlier `solid-fill` boolean (decision #46) — `fill-solid` is the exact successor. Charts whose fill **encodes data** ignore the family: map regions (choropleth — saturation is the data), infra severity tints, gantt progress bars, and tech-radar blips. A small group honors `fill-solid` but ignores `fill-outline` because hollowing the fill would erase the chart: the line/function area fill and sankey/chord ribbons (the surface *is* the flow). Everything else — including goal meters (hollow meter, colored rim, §34.5), heatmap cells (bg cells, full-intent ramp strokes), countdown chips (§36), clock's decorative surfaces (state tints like daylight stay), bracket, body, quadrant, and arc — renders outline for real (decision #47). Under `fill-outline`, colorless/untagged shapes render with a strengthened neutral outline so they don't vanish against the background.
**Layout direction** is likewise cross-cutting: every direction-bearing chart accepts the bare booleans `direction-lr` / `direction-tb` (last-one-wins), each chart keeping its own sensible default — flowchart, org, and c4 default TB; infra, state, sitemap, boxes-and-lines, swimlane, pert, version-control, and event-line default LR (event-line accepts only `direction-lr`; `direction-tb` is a reserved seam); clock defaults rows (`direction-tb`), where `direction-lr` selects columns mode. The key+value form `direction LR|TB` is accepted legacy. Cycle's `direction-counterclockwise` belongs to the same boolean family.
Example: `no-title` (all chart types with a banner title).
The `fill-*` family states one treatment for the **whole chart**, so it can never express a hierarchy *within* one. That axis — which elements are figure and which are ground — is the **emphasis family** (`highlight` / `dim`, §1.11). It is not a boolean family: both directives take a name list, so they live in their own section.
### 1.10 In-Arrow Message Labels
An **in-arrow label** is the text embedded inside an arrow between the opening delimiter and the arrow token, as in `A -label-> B`.
```
A -label-> B
^ ^---^ ^^
| | ||
| | |+- destination id
| | +- arrow token
| +- label text (plain, no markdown)
+- opening delimiter (matches arrow type)
```
**Chart types that support in-arrow labels**: sequence, flowchart, state, infra, c4, er, class, boxes-and-lines.
#### Cheat sheet
```
// happy-path: labels are plain text with punctuation allowed
A -location[]-> B // label = "location[]"
A -a[b]c-> B // label = "a[b]c"
A -{json}-> B // label = "{json}"
// unicode: all scripts and emoji preserved verbatim
A -café-> B
A -日本語-> B
A -🎉-> B
// punctuation is literal — no markdown interpretation
A -(parenthetical)-> B // label = "(parenthetical)"
A -*emphasis*-> B // label = "*emphasis*" (NOT bold)
A -`code`-> B // label = "`code`" (NOT a code span)
// forbidden: -> and ~> substrings inside a label
A -uses -> chain-> B // ERROR (E_ARROW_SUBSTRING_IN_LABEL)
// fix: rephrase the label, or move detail to same-line metadata
A -uses to chain-> B
A -uses-> B detail: chains to next
// migration from pre-gauntlet (legacy) syntax
A -Makes calls [HTTP]-> B // label is now the FULL "Makes calls [HTTP]"
A -Makes calls-> B tech: HTTP // preferred: technology on target metadata
```
#### Character-set contract
- **Allowed**: any Unicode codepoint except the forbidden list below. Brackets `[] {} ()`, pipes `|`, quotes `"' `, backticks, punctuation, digits, emoji, ZWJ sequences, combining marks — all pass through as literal characters.
- **Forbidden substrings**: `->` and `~>`. These terminate the arrow. If you need them inside a label, rephrase it or move the detail to same-line metadata (`A -uses-> B detail: chains to next`); there is no escape mechanism.
- **Forbidden characters**: C0 control characters U+0000–U+001F except U+0009 (tab), and U+007F (DEL). Silent renderer breakage and log-injection surface — no legitimate use case.
- **Whitespace**: leading and trailing whitespace is trimmed; internal whitespace runs (including tabs, non-breaking spaces, and zero-width spaces) are **preserved**, never collapsed.
- **Plain text only**: no markdown interpretation. `*foo*` renders as `*foo*`, not italicized. `[label](url)` renders as literal `[label](url)`, not a hyperlink. Clickable URLs belong in notes, not in in-arrow labels.
- **HTML-safe**: all renderers emit label text as a DOM text node. `` renders as literal text — the entire label is a sequence of codepoints, not a markup fragment.
#### Edge color is not a feature
Edges on flowchart, state, and sitemap diagrams have NO color slot. `A -(red)-> B` is a literal label with text `(red)`; `A -yes-> B` and `A -no-> B` no longer auto-color the arrow. Arrows render with the default theme color, period. To color a *node*, use tags (§1.3).
Sankey link lines DO accept a trailing-token color, because the link itself carries data:
```
Sugar Plantations -> Tortuga Distillery 3000 red // link is colored red
```
#### Migrating from pre-gauntlet syntax
One legacy form changed with this spec:
1. **C4 trailing `[technology]` sugar is removed.** A C4 arrow like `-Makes calls [HTTPS]-> API` used to extract `HTTPS` as the technology annotation. The full `Makes calls [HTTPS]` is now the label. Use the same-line form for technology: `-Makes calls-> API tech: HTTPS`.
No code migration is required for in-arrow label character escaping — any label that was valid before remains valid, with one exception: if your label happened to contain the literal substring `->` or `~>`, the parser now rejects it with `E_ARROW_SUBSTRING_IN_LABEL`. Move those labels to the post-colon form.
#### See also
- Engineering rationale, decision log, and party-mode trade-offs: [dgmo-language-spec-decisions.md](./dgmo-language-spec-decisions.md) → "In-Arrow Message Labels".
### 1.11 Emphasis — `highlight` / `dim`
Every diagram has a background element — losses, "other", last year's line, the control group — that must stay visible without competing for attention. **Emphasis** is the directive family that says so.
```
highlight [, …] // the named elements are the figure; everything else recedes
dim [, …] // the named elements recede; everything else is untouched
```
Both are **chart-level directives**, written on their own line anywhere in the diagram — never a trailing token on a data row. The line-trailing bare-token slot already belongs to color (§1.5), and on a chart like sankey a node is named by many link lines and declared by none, so there is no single row a per-element modifier could live on.
The two forms are **mutually exclusive** and **last-one-wins**, the same rule the `fill-*` family follows (§1.9). Stating both is contradictory, not additive.
**Chart types that support emphasis**:
| Chart type | Forms | Closure |
| --- | --- | --- |
| `sankey` | `highlight`, `dim` | Upstream + downstream flow closure (§15.5) |
| `family` | `highlight` only | The named person's bloodline (§32.18) |
Every further chart type is a separate decision — the family is not silently available elsewhere. On a chart type that has not adopted it, `highlight`/`dim` are not directives and the line is read as ordinary content.
#### What dimming does
A receded element is drawn at **reduced opacity, multiplied onto the paint it already had**. It is never recolored.
- Hue survives, so emphasis composes with color instead of competing with it — a receded brand-red flow is still red. This is precisely why receding cannot be spelled by recoloring an element gray: gray is a full-weight palette color (§1.5), and spending the color slot on it destroys the element's identity.
- Where a baseline translucency already exists (sankey ribbons), the dim **multiplies** it rather than replacing it. Replacing would render dimmed ribbons *more* solid than undimmed ones.
- Shapes and text recede by different amounts. Shapes carry the hierarchy; labels only need to stop competing, so they keep a legibility floor. The requirement is "recede", not "remove".
- The result is a **baked attribute** in the rendered output — no stylesheet, no script, no hover. PNG and SVG exports carry the emphasis exactly as the preview shows it.
There is no numeric opacity control. Two states — figure and ground — are the whole hierarchy a diagram can carry legibly, and the language spells states with named tokens rather than tuned values (the same reason it has no hex colors).
#### `highlight` lights a story, not a single element
`highlight` is the dual that names the **foreground** and derives the background from it. What counts as foreground is more than the named element alone: each chart type extends the named elements to the structure that explains them.
- **`family`** lights the named person's **bloodline** — that person plus all ancestors and all descendants.
- **`sankey`** lights the named node's **flow closure** — everything upstream that feeds it plus everything downstream it feeds. Without this, `highlight Barrel Aging` would recede Barrel Aging's own inflows, deleting the explanation the highlight exists to give.
`dim` deliberately takes **no** such closure. `dim Spoilage` recedes exactly Spoilage, not everything that feeds it — which on most charts is nearly everything. The duals are asymmetric because their intents are: `highlight` names a story, `dim` names one thing to push back.
Which form to reach for depends only on which list is shorter. Use `dim` when one or two elements should recede; use `highlight` when one story matters and the rest is context.
#### Naming elements
```
dim Spoilage // one element
dim Ship Provisions // ONE two-word element
dim Spoilage, Ship Provisions // two elements — the comma is authoritative
highlight Barrel Aging
```
- The **comma is the list separator** and is authoritative whenever it appears.
- Without a comma, the whole argument is read as **one name**, because element names routinely contain spaces. Only if that phrase matches nothing is it re-read as whitespace-separated names, so `dim Spoilage Losses` still works — but a comma is the reliable way to name several.
- Matching is **case-insensitive and whitespace-normalized**: emphasis names an element you already typed elsewhere in the diagram, so it does not demand an exact retype.
- A name matching no element is reported as a warning and ignored. A directive whose names **all** miss resolves to **no emphasis at all** — a typo must never blank the chart.
#### Not supported where every element is load-bearing
Emphasis restages a diagram; it must not misreport one. Chart types whose every element carries the reading by construction do not take the family: goal meters, countdown, clock, wireframe (fidelity, not emphasis), quadrant, and map choropleth (position and saturation *are* the datum — a receded region reads as a lower value). The same charts whose fill encodes data and therefore ignore the `fill-*` family — infra severity, gantt progress, tech-radar blips — are excluded for the same reason.
#### Reserved leading tokens
On a chart type that supports emphasis, `highlight` and `dim` are **reserved as the first token of a line**. An element genuinely named "dim …" needs the usual escape — capitalize it or quote it — the same hazard class as trailing colors (§1.5) and bare `collapsed` (§1.8).
#### See also
- Engineering rationale and the rejected alternatives (per-element trailing token, numeric opacity, guidance-only): [dgmo-language-spec-decisions.md](./dgmo-language-spec-decisions.md) → decision #49.
---
## 2. Universal Name Handling
DGMO uses one rule for entity names across every chart type. Names accept
spaces verbatim. Equality is forgiving — case-insensitive and
whitespace-collapsed. The first-seen casing/spacing wins for display.
Quoting is on-demand — required only when a name contains a reserved
character.
### 2.1 Pinned Algorithm
Two names are the same entity when they reduce to the same key under this
algorithm:
1. NFC normalize the input
2. Replace runs of Unicode whitespace with a single ASCII space
3. Trim leading/trailing whitespace
4. Case-fold via `toLocaleLowerCase('en-US')`
`'Auth Service'`, `'auth service'`, `'AUTH\tSERVICE'`, and `' auth service '`
all normalize to `'auth service'`. The first declaration wins for the
display label. Subsequent re-declarations with a different casing or
spacing fold into the first and emit `I_NAME_MERGED` (warning) so the
divergence is visible to the author.
The locale is pinned to `en-US` to keep behavior stable across build
environments — language-neutral case folding silently diverges on
Turkish dotted/dotless I and German ß the moment the default locale
changes. The shared utility lives at `dgmo/src/utils/name-normalize.ts`;
every parser imports from it so normalization can never drift between
chart types.
### 2.2 Reserved Characters
Bare names accept letters, digits, spaces, and hyphens. The following
characters are reserved and require `"..."` quoting if you want them in
a name:
| Character | Why it's reserved |
|-----------|-------------------|
| `\|` | Removed metadata delimiter (pre-0.18.0). Outside its surviving uses (§1.4.5) it used to emit `E_PIPE_OPERATOR_REMOVED`; that guard was dropped in 0.43.0 (#28), so it now falls through as a literal. Quote to use as a literal name character. |
| `:` | Type separator in members + various metadata forms |
| `->`, `~>`, `--`, `..` | Edge sigils. `<-` / `<~` are not standalone arrows — left-pointing arrows are removed (write `B -> A`); they appear only inside the bidirectional operators `<->` / `<~>` (and labeled `<-label->` / `<~label~>`) |
| `[`, `]` | Group brackets and shape brackets |
| `(`, `)` | Shape brackets (rounded), sankey parenthetical syntax |
| `{`, `}` | Shape brackets in flowchart |
| `<`, `>` | Decision shape brackets |
| Leading or trailing whitespace | Stripped by normalization |
There is no `"`-inside-`"` escape — names cannot contain a double
quote.
### 2.2a Quoting + Alias + Metadata — Combination Constraints
Bare names compose freely with `as ` (§2A) and same-line metadata
(§1.4) on the same declaration line. Quoted names are more restricted:
the following combinations are **rejected** in current parsers and
should not appear in generated DGMO.
| Combination | Bare name | Quoted name |
| --- | --- | --- |
| name only | ✅ | ✅ |
| name + `as alias` | ✅ | ❌ (parse error / `Unexpected line`) |
| name + `meta` | ✅ | ✅ |
| name + `as alias` + `meta` | ✅ | ❌ |
**Workarounds** when you need both quoting and aliasing:
- Quoting exists to escape reserved characters in a name. If the only
reason for quoting is multi-word, **drop the quotes** — bare names
already accept spaces and hyphens (`Auth Service as a` is valid).
- If the name genuinely needs a reserved character (e.g. `|` or `:`),
keep the quotes and **drop the alias** — references can use
forgiving normalization (§2.1) to match the full quoted name.
### 2.2b Sequence Participants — Declaration Forms
Sequence has an additional constraint layered on §2.2a: standalone
participant declarations (without `is a `) accept **only** the
bare-name-plus-optional-metadata form. Anything else needs the typed
declaration form.
| Form | Accepted | Notes |
| --- | --- | --- |
| `Alice` | ✅ | bare implicit |
| `Alice role: x` | ✅ | bare + metadata |
| `Alice as a` | ❌ | standalone alias without `is a` is rejected |
| `Alice as a role: x` | ❌ | same — needs typed form |
| `"Auth Service"` | ❌ | quoted standalone is rejected |
| `"Auth Service" role: x` | ❌ | same |
| `Alice is a person` | ✅ | typed |
| `Alice is a person as a role: x` | ✅ | typed accepts alias + metadata |
| `"Auth Service" is a database as a role: x` | ✅ | typed unlocks quoted + alias + metadata |
**Rule of thumb for sequence:** if a participant needs an alias, a
quoted name, or both, declare it with `is a ` (use `is a person`
or another valid type from §2.6). Bare implicit declarations are only
for simple cases.
Bare and typed declarations may be **freely interleaved** in the
declaration preamble — a typed line such as `User is an actor` does not
close the window for subsequent bare names. The bare-declaration window
closes at the first message, section (`== … ==`), or block keyword
(`if`/`loop`/`parallel`); after that, a bare word is reported as a stray
line rather than registered as a participant.
### 2.3 Side-by-Side Examples
| Source | Where | Renders as | Notes |
|--------|-------|------------|-------|
| `Auth Service is a database` | sequence | `Auth Service` | bare multi-word, no quoting needed |
| `Cache` then `cache` | flowchart | `Cache` | first-seen wins; warning emitted |
| `"first name" varchar` | er column | `first name` | quoting required to escape `:` type separator if you wanted it in name |
| `"Order \| Items"` | infra | `Order \| Items` | quote the pipe |
| `class "Customer Service"` | class | `Customer Service` | bare multi-word also accepts |
| `Auth Server is a database` then `auth server -hi-> DB` | sequence | one participant `Auth Server` | message resolves via normalization |
| `[Order Items]` | infra group | `Order Items` | brackets are the group sigil; name inside accepts spaces |
| `Customer Orders { ... }` | er | `Customer Orders` | multi-word table |
| `webapp api gateway` | infra | `webapp api gateway` | full string is the name |
### 2.4 Migration: aka Removed
Sequence's `Name is a type aka Alias` modifier is removed. The
forgiving-normalization rule above handles casing/spacing variants;
short-code aliasing for genuinely-different tokens is now the
universal `as` postfix — see §2A.
```
// before — removed
Alice is an actor aka Authenticator
// after — universal alias (when a short-code is wanted)
Alice is an actor as a
```
Encountering `\baka\b` in a participant declaration produces
`E_AKA_REMOVED` with a migration hint in the message.
### 2.5 Carve-Outs by Design
These are intentionally outside the universal rule:
- **D3 chart data rows** (slope, venn, quadrant, arc) — labels in data
rows are not entity names; they participate in the chart's data model
rather than the entity-graph that universal name handling governs.
- **`tags:` and `import:` directives in org** — values are file/tag
references, not entity names. Normalizing them would silently change
what file is imported or what tag is referenced.
- **Flowchart and state shape brackets `[]`, `()`, `{}`, `<>`** — these
are shape sigils, not name quoting. The text inside is normalized as
a name.
- **Tag suffix-alias `tag Priority p`** — the `p` is a tag-system
alias, not an entity name. It's resolved through a separate
alias map and stays unaffected.
### 2.6 Error Catalog
| Code | Severity | When | Message template |
|------|----------|------|------------------|
| `I_NAME_MERGED` | warning | Two source-distinct names normalize to the same key AND their displayed forms differ | `merged '{incoming}' (line N) into '{existing}' (line M) — names differ only in case/whitespace` |
| `E_NAME_RESERVED_CHAR` | error | A bare name contains a reserved character without `"..."` quoting | `name contains reserved character '{char}' — wrap in "..." to use literally` |
| `E_AKA_REMOVED` | error | The removed `aka` keyword appears in a sequence participant declaration | `'aka' is no longer supported — use the participant name directly` |
| `E_PARTICIPANT_TYPE_REMOVED` | error | A removed sequence participant-type keyword (`service`, `frontend`, `networking`, `gateway`, `external`) appears in an `is a X` declaration | `'{type}' is no longer supported — drop 'is a {type}'; the participant renders as the default rectangle` |
`I_NAME_MERGED` warnings are suppressible per-line with an
`# allow-merge` comment annotation at the call site for intentional
merges.
#### See also
- Engineering rationale: [dgmo-language-spec-decisions.md](./dgmo-language-spec-decisions.md)
- Implementation phases (A–D landed; E in progress): `_bmad-output/implementation-artifacts/tech-spec-universal-name-handling.md`
---
## 2A. Universal Aliases (`as` keyword)
A single postfix syntax — `Name as ` — applies anywhere a name
appears across every chart type with named entities. Replaces the prior
tag-shorthand and venn `alias` keyword forms with a uniform rule.
### 2A.1 Syntax
```
sequence
Alice is an actor as a
Bob is a database as b
a -hello-> b
b -ack-> a
```
```
infra
[Edge] as edge
Cloudflare
[Origin] as origin
AppServer
edge -routes-> origin
```
```
venn
Swordsmanship as sw red
Navigation as nav blue
sw + nav Sea Raiders
```
```
tag Priority as p
tag Concern as c
```
### 2A.2 Rules
- **Token shape**: `[A-Za-z][A-Za-z0-9_]{0,11}` — letter start,
letters/digits/underscore, length 1–12. Case-sensitive: `pm` and
`PM` are distinct aliases.
- **Modifier order on declarations**: ` [is a type] [as ] [color] [key: value, …]`.
Color is the trailing token of the name region; same-line metadata (§1.4.1), when present, follows and is comma-separated.
- **Strict ordering**: aliases must be declared on or before first use.
`E_ALIAS_BEFORE_DECL` flags out-of-order references.
- **Flat global namespace**: one alias literal has exactly one binding
per source.
- **Shape/type inference reads canonical**: `*Service` inference rules
fire on the resolved canonical, never on the alias.
- **Aliases are NEVER UNH-normalized** — they are exact-match ASCII
short-codes. Unicode canonicals (`Цена as p`) work because
normalization applies to the canonical only.
- **Reserved tokens** that cannot be used as aliases: DGMO grammar
keywords (`as`, `is`, `tag`, `alias`, `aka`) plus chart-type tokens
(`bar`, `flowchart`, `venn`, …). English articles (`a`, `an`, `the`)
are NOT reserved — `Alice as a` is valid.
- **SaaS-naming is safe**: `Storage as a Service`, `Backend as a Service`
do not match the alias regex (the trailing token can never reach
end-of-line) — they parse as canonicals untouched.
Two charts deviate from the alias token shape: **clock** `as `
accepts multi-word, punctuation-bearing labels that DO render as the row
label (`as Dani (NY)` — decision #43), and **map** keeps `as` as a
non-rendering ≤12-char routing token, using `label:` for multi-word
display names (§24B.14).
### 2A.3 When to use aliases
Aliases earn their keep on names that repeat 3+ times. Single-use
names should not be aliased. Two- and three-character source names
rarely benefit. Aliases should aid comprehension, not obscure it.
### 2A.4 Universal Name Handling vs. Aliases
- **UNH** = same-name typo tolerance. `Alice` ≡ `alice` ≡ `ALICE` ≡
`Alice ` (trailing space). Author intent is the same string with
cosmetic variation.
- **Aliases** = different-token short codes. `pm` ≡ `Product Manager`.
Author intent is two distinct strings bound at declaration time.
### 2A.5 Migration from legacy syntaxes
Pre-1.0 hard break — no compat shim:
| Was | Now |
|-----|-----|
| `tag Priority p` (bare shorthand) | `tag Priority as p` |
| `tag Priority alias p` (explicit) | `tag Priority as p` |
| `Swordsmanship(red) alias sw` (venn) | `Swordsmanship as sw red` |
Diagnostics:
- `E_TAG_SHORTHAND_REMOVED` fires on legacy bare tag shorthand.
- `E_VENN_ALIAS_KEYWORD_REMOVED` fires on legacy venn `alias` keyword.
Internal-fixture migration is automated via
`dgmo/scripts/migrate-as-aliases.mjs` (see also the CI guardrail
`dgmo/scripts/lint-no-legacy-alias.sh`).
### 2A.6 Diagnostic Catalog
| Code | Severity | Message |
|------|----------|---------|
| `E_ALIAS_BEFORE_DECL` | error | `Alias '' used before declaration. Declare ' as ' on or above this line.` |
| `E_ALIAS_COLLISION` | error | `Alias '' is already bound to '' (line ). Cannot rebind to ''.` |
| `E_ALIAS_SHADOWS_NAME` | error | `Alias '' would shadow an existing canonical name. Choose a different alias.` |
| `E_ALIAS_REBINDING` | error | `'' is already aliased as '' (line ). Cannot also alias as ''.` |
| `E_ALIAS_OF_ALIAS` | error | `'' is itself an alias for ''. Cannot alias an alias — alias the canonical instead.` |
| `E_ALIAS_RESERVED_KEYWORD` | error | `'' is a reserved keyword and cannot be used as an alias.` |
| `E_ALIAS_INVALID_FORMAT` | error | `Alias '' must match [A-Za-z][A-Za-z0-9_]{0,11} (letter start, letters/digits/underscore, max 12 chars).` |
| `E_ALIAS_AFTER_CANONICAL` | error | `'' was already used as a canonical name (line ). Aliases must be declared on or before first use.` |
| `E_TAG_SHORTHAND_REMOVED` | error | `Bare tag shorthand 'tag ' was removed. Use 'tag as ' instead.` |
| `E_VENN_ALIAS_KEYWORD_REMOVED` | error | `Venn 'alias' keyword was removed. Use 'as' instead — '(color) as '.` |
| `W_ALIAS_CASE_NEAR_MATCH` | warning | `'' differs only in case from declared alias ''. Did you mean ''?` |
| `W_ALIAS_UNDERUSED` | warning | `Alias '' is declared but referenced ≤1 time. Aliases earn their keep on names that repeat 3+ times.` |
#### See also
- Engineering rationale: [dgmo-language-spec-decisions.md](./dgmo-language-spec-decisions.md) → TD-18
- Tech-spec: `_bmad-output/implementation-artifacts/tech-spec-universal-alias-syntax.md`
---
## 2B. Universal Date Handling
Every chart type that reads a date accepts the grammar below. Input is
**liberal**; dates are stored internally as ISO, so rendering, sorting, and
snapshots stay byte-deterministic. Applies to gantt (§13), timeline (§15),
event-line (§28), countdown (§36), and pert (§13A). Family (§32) uses year-grain
only; clock (§37) has no date input.
### 2B.1 Accepted input formats
| You write | Reads as | Note |
|-----------|----------|------|
| `2026-07-04` | `2026-07-04` | ISO — canonical, always unambiguous |
| `2026-07-04 14:30` / `…T14:30[:SS]` | datetime | where the type supports a time |
| `2026-07` | month grain | day omitted |
| `2026` / `1716` | year grain | bare year |
| `07-04` | `{year}-07-04` | bare MM-DD → resolved year (§2B.3) |
| `7/4` | `{year}-07-04` | slash form, **US month-first default** (§2B.2) |
| `Jul 4` / `July 4` | `{year}-07-04` | month name — never order-ambiguous |
| `July 4, 2026` / `4 Jul 2026` | `2026-07-04` | full natural form (explicit year) |
| `753 BCE` / `44 BC` / `14 CE` | era-signed year | timeline BCE support |
### 2B.2 Slash order — US month-first
Numeric slash/dash forms (`7/4`, `07-04`) default to **US month-first (MDY)**.
- The **`date-order dmy`** directive flips the whole document to day-first
(`7/4` → Apr 7). `date-order mdy` is the default.
- **Out-of-range self-disambiguates** regardless of the directive: `13/2` can't
be month 13 → Feb 13.
- Applies **only** to ambiguous numeric forms. Month-name forms (`Jul 4`,
`4 Jul`) and ISO are never affected by `date-order`.
### 2B.3 Year resolution — first match wins
A date without a year resolves through this ladder:
1. **Explicit year on the date itself** — wins outright.
2. **`year 20XX` directive** anywhere in the document — the base year.
3. **Derived from a sibling dated row.** Timeline/event-line carry the year
forward from the nearest full date above and **roll to the next year** if a
bare month-day falls earlier (Nov start → a later Jan row = next year). Gantt
and pert are dependency-driven, so they instead **anchor to a single base
year** (the first explicit-year date in the document, usually `start` /
`start-date`).
4. **Current render year** — last resort, only when the chart has **zero** full
dates anywhere. Emits a soft, non-blocking hint suggesting `year 20XX` for
reproducibility. Suppressed by **`no-current-year`**, which instead makes a
fully-bare chart a hard error.
Levels 1–3 never read the clock, so those charts are snapshot-stable. Only a
chart with no full date at all touches the wall clock.
### 2B.4 Directives
Recognized in any date-bearing chart (colon-free; bare where boolean):
| Directive | Effect | Default |
|-----------|--------|---------|
| `year 20XX` | base year for bare month-days below | derive / current year |
| `date-order dmy` | numeric slash/dash dates read day-first | `mdy` (US) |
| `no-current-year` | a fully-bare chart is a hard error, not current-year | off |
All three are **position-independent** (pre-scanned), so they may appear before
or after the dates they govern.
Existing ISO diagrams are unaffected — the grammar is an additive superset and
ISO output is byte-identical.
---
## 3. Sequence Diagrams
### 2.1 Participants
```
Name is a [as ] [position: N]
Name key: value
```
Types: `actor`, `database`, `cache`, `queue` (plus default — the plain rectangle, used when `is a` is omitted).
Type names in `is a X` are **case-insensitive** (`is a Actor`, `is an ACTOR`, `is an actor` all parse identically). Both `is a` and `is an` are accepted regardless of the type's leading letter.
A participant *named* with a removed-type keyword (e.g. `service -> User: hi` declares a participant whose id is `service`) remains valid. The trim affects only the `is a X` declaration syntax, not name resolution.
**Inference rules** — the parser infers the type (and shape) from the participant name. Only use `is a` when the name does not match or you want to override:
| Inferred Type | Shape | Name Patterns (examples) |
|--------------|-------|--------------------------|
| actor | Stick figure | `User`, `Customer`, `Admin`, `Agent`, `Person`, `Buyer`, `Seller`, `Guest`, `Visitor`, `Operator`, `Developer`, Alice, Bob, Charlie, Fan, Purchaser, Reviewer, `*User`, `*Actor`, `*Analyst`, `*Staff` |
| database | Cylinder (vertical) | `*DB`, `Database`, `Datastore`, `*Store`, `Storage`, `*Repo`, `Repository`, `SQL`, Postgres, MySQL, Mongo, Dynamo, Aurora, Spanner, Supabase, Firebase, BigQuery, Redshift, Snowflake, Cassandra, Neo4j, ClickHouse, Elastic, OpenSearch, Druid, Trino, Pinecone, Weaviate, Qdrant, Milvus, Presto, `*Table` |
| cache | Dashed cylinder | `*Cache`, Redis, Memcache, KeyDB, Dragonfly, Hazelcast, Valkey |
| queue | Horizontal cylinder (pipe) | `*Queue`, `*MQ`, SQS, Kafka, RabbitMQ, `EventBus`, `MessageBus`, `*Bus`, `Topic`, `*Stream`, SNS, PubSub, `*Broker`, NATS, Pulsar, Kinesis, EventBridge, CloudEvents, Celery, Sidekiq, EventHub, `*Channel` |
| default | Rectangle | Everything else (no `is a` needed) |
Inference is intentionally conservative: only names whose role is unambiguous at a glance get a distinctive shape. Anything else falls through to the default rectangle. Adding new patterns should resist suffix catch-alls (e.g. `*-er` → service) that historically inflated the table and rarely held up across diverse diagrams.
**Inference handles it (skip `is a`):**
```
PostgresDB // database (matches *DB)
Redis // cache (exact match)
User // actor (exact match)
Kafka // queue (exact match)
```
**Inference would miss (use `is a`):**
```
Vault is a database // "Vault" matches no rule, but you want database
Notifications is a queue // "Notifications" matches no rule
```
Names that previously inferred to a removed type — `AuthService`, `WebApp`, `Cloudflare`, `API Gateway`, `Stripe`, `Webhook`, `Upstream` — now fall through to default (plain rectangle). That is the intended outcome of the trim: the visual differentiation is gone because the underlying distinction did not pull its weight.
#### Removed Type Keywords (0.16.0)
The keywords `service`, `frontend`, `networking`, `gateway`, and `external` were removed in 0.16.0. Using any of them in `is a X` emits `E_PARTICIPANT_TYPE_REMOVED` (one diagnostic per offending line — the parser collects all errors in a single pass rather than stopping at the first):
```
// All of these fail to parse:
Auth is a service // E_PARTICIPANT_TYPE_REMOVED
WebApp is a frontend // E_PARTICIPANT_TYPE_REMOVED
LB is a networking // E_PARTICIPANT_TYPE_REMOVED
API is a gateway // E_PARTICIPANT_TYPE_REMOVED
Stripe is an external // E_PARTICIPANT_TYPE_REMOVED
// Fix: drop the override — participant renders as default rectangle
Auth
WebApp
LB
API
Stripe
```
The trim is **sequence-only** — `is a external` / `is a database` / etc. remain valid in C4, infra, and org diagrams under their own taxonomies (see §7).
### 2.2 Participant Ordering
Participants are laid out left-to-right. The default order is **first appearance in messages** — the first participant mentioned in a message gets the leftmost position, and so on.
Declaring a participant (§2.2b) assigns its tag, type, or alias but does **not** pin a column — a declared participant that appears in a message is still placed by first appearance, in whatever order the arrows reach it. Declaration order decides placement only for participants that never appear in any message. So you can declare just the participants you need to tag (e.g. the External or Customer exceptions) without those declarations dragging them out of message-flow order; use an explicit `position:` when you do want to pin one.
**Explicit position override:**
```
Name position: N
```
- `position` is colon-keyed same-line metadata (§1.4) — the colon is **required**. The legacy bare-keyword form `Name position N` was removed; a surviving bare `position N` raises `E_SEQUENCE_BARE_POSITION_REMOVED`.
- `0` is leftmost, `1` is second from left, etc.
- Negative values count from the right: `-1` is rightmost, `-2` is second from right
- When two participants target the same position, the later one shifts to the nearest free slot
- Position overrides take priority over all other ordering
- May combine with other metadata (`DB position: -1, role: Storage`). When also using `as `, the alias precedes the metadata (`Alice is an actor as a position: 1`) — metadata runs to end of line.
**Group coherence:** members of the same group (§2.3) always stay adjacent. The group is placed where its first member would naturally appear in message order.
**Priority (highest to lowest):**
1. Explicit `position: N`
2. Group adjacency
3. First appearance in messages
4. Declaration order (for participants not mentioned in any message)
### 2.3 Participant Groups
```
[Group Name]
Participant1
Participant2
```
- Metadata goes outside brackets: `[Backend] t: Eng`
#### Collapsible Groups
```
[Group Name] collapsed
Participant1
Participant2
```
The bare `collapsed` trailing flag (§1.8) declares a group as collapsed by default (legacy: `collapsed: true`). Collapsed groups render as a single participant lifeline with the group name as label. A drill-bar accent bar on the participant box signals expandability.
- Messages to/from group members remap to the group lifeline
- Messages between members of the same collapsed group render as self-referential on the group lifeline
- Unlabeled internal returns between members are suppressed
- Notes attached to members display on the group lifeline
- Fragment blocks (`if`/`loop`/`parallel`) resize to reflect collapsed participant positions
- In the app, clicking the collapsed participant expands it; clicking an expanded group label collapses it
- Export/share and CLI rendering honor the collapse state
### 2.3 Messages (Arrows)
| Type | Syntax | Example |
|------|--------|---------|
| Sync (labeled) | `A -label-> B` | `Client -login-> API` |
| Sync (bare) | `A -> B` | `Client -> API` |
| Async (labeled) | `A ~label~> B` | `API ~notify~> Queue` |
| Async (bare) | `A ~> B` | `API ~> Queue` |
- Whitespace around arrows is optional: `A-label->B` works
- Labels can contain spaces, hyphens, special chars
- Labels cannot contain arrow chars (`->`, `~>`)
- Same-line metadata: `A -msg-> B c: Caching`
### 2.4 Section Dividers
```
== Label ==
== Label
```
Trailing `==` is optional.
Section dividers exist to separate a diagram into **two or more** distinct phases (e.g. `== Authentication ==` then `== Checkout ==`). Use them only when there is more than one phase to delimit — a lone divider above a single run of messages adds a redundant band and a title that just restates the diagram's subject. If the whole diagram is one phase, omit the divider entirely and let the diagram's own title (or the surrounding prose) carry the name.
### 2.5 Notes
```
note Text
note right Text
note left of ParticipantID Text
```
**Multi-line notes** use an indented body below the `note` heading:
```
note right of API
- First bullet point
- Second bullet point
**Bold text** and *italic text*
`inline code`
[link text](https://example.com)
https://example.com
```
Content formatting:
- `- ` prefix on indented lines = bullet points
- Inline markdown: `**bold**`, `*italic*`, `` `code` ``
- Links: `[text](url)` and bare URLs (auto-truncated in display)
### 2.6 Structural Blocks
Sequence diagrams support three structural blocks: `if`, `loop`, and `parallel`. Blocks
nest via indentation.
**Conditional (`if` / `else if` / `else`):**
```
if registered user
User -login-> WebApp
WebApp -authenticate-> Auth
else if guest
User -browse-> WebApp
else
WebApp -403 forbidden-> User
```
- `else if` (two words) introduces additional branches; you can have zero or more.
- `else` (no label) is optional and must come last.
- `else if` and `else` align with the opening `if` indent.
**Loop:**
```
loop until success
Client -poll-> Server
```
**Parallel** (requires a label, does not accept `else`):
```
parallel fan-out
Coordinator -task A-> WorkerA
Coordinator -task B-> WorkerB
```
**Common errors:**
- `elif foo` → use `else if foo`
- `else foo` (label after bare `else`) → use `else if foo`
- `loop` and `parallel` blocks do not support `else` branches
- `end` (or `deactivate`/`activate`/`autonumber`) → **not dgmo syntax.** Blocks (and multi-line notes) close by dedenting, never with a terminator keyword; a bare `end` line is rejected. Activation is controlled by the `activations` option (§2.7), not per-message keywords.
- A tag/color on a **message** (`A -msg-> B tag: x`) → tags color **participants**, not messages. Declare a `tag as ` group and assign ` : `.
### 2.7 Sequence Options
- `no-activations` — hide inferred activation bars (on by default)
- `active-tag GroupName` — set active tag group for coloring
- `active-tag none` — suppress tag coloring
---
## 4. Infrastructure Diagrams
### 4.1 Declaration
```
infra [Title]
```
### 4.2 Nodes
```
NodeName
NodeName key: value
NodeName as alias
NodeName as alias key: value
"Node name with | reserved chars"
"Node name with reserved chars" key: value
```
Nodes are plain names. Capabilities come from properties (see §4.3), not type declarations.
- **Aliases** (§2A): `NodeName as alias` binds a short alias used by edges and group references. Alias must start with a letter/underscore and be ≤12 chars.
- **Quoted names**: wrap the label in double quotes when it contains reserved chars (`|`, `:`). Multi-word names without reserved chars do **not** need quoting (`webapp api gateway` is a valid bare name).
- **Quoted names cannot combine with `as `** on the same declaration line (see §2.2a). If both quoting and aliasing are required, redesign the name to avoid the reserved character.
### 4.3 Node Properties (Indented, Colon-Separated)
```
NodeName
latency-ms: 50
max-rps: 8000
uptime: 99.99%
cache-hit: 75%
description: My API gateway
firewall-block: 10%
instances: 3
```
Properties use a known schema with colon-separated values:
| Property | Capability | Effect |
|----------|-----------|--------|
| `cache-hit` | Cache | % requests served from cache, reduces downstream RPS |
| `firewall-block` | Firewall/WAF | % requests blocked, reduces downstream RPS |
| `ratelimit-rps` | Rate limiter | Max RPS passed through |
| `latency-ms` | Latency | Adds to path latency |
| `uptime` | Availability | Multiplied along path for SLO |
| `instances` | Horizontal scaling | Multiplies capacity (number or `min-max` range) |
| `max-rps` | Capacity ceiling | Max RPS node handles |
| `cb-error-threshold` | Circuit breaker | Error rate trip threshold |
| `cb-latency-threshold-ms` | Circuit breaker | Latency trip threshold |
| `concurrency` | Concurrency limit | Max concurrent requests (serverless) |
| `duration-ms` | Processing time | Time spent processing (serverless) |
| `cold-start-ms` | Serverless | Cold start penalty |
| `buffer` | Queue | Buffer size |
| `drain-rate` | Queue | Consumption rate |
| `retention-hours` | Queue | Message retention |
| `partitions` | Queue | Partition count |
| `description` | Display | Description text |
**Mutually exclusive:** `concurrency` ≠ `instances` ≠ `max-rps`; `buffer` ≠ `max-rps`. A node is serverless, traditional, or a queue — not two at once.
### 4.4 Connections
| Type | Syntax |
|------|--------|
| Sync (bare) | `-> Target` |
| Sync (labeled) | `-/api-> Target` |
| Async (bare) | `~> Target` |
| Async (labeled) | `~event~> Target` |
- Connection metadata: `split: 50%, fanout: 3` (same-line, comma-separated)
- **Indent under source node** (see §1.7) — avoid flat `Source -> Target` form
- Async edges (`~>`) render with a wiggle pattern and a Fan-Out badge is added when fanout > 1
- Target may be a node id, an alias, or a group ref `[Group Name]`
### 4.5 Fanout
```
SearchAPI
-> SearchShards fanout: 6
```
`fanout: N` multiplies the per-edge RPS delivered to the target by `N` (request amplification — scatter-gather, shard fanout, pub/sub).
- Effect: `target_rps = source_post_behavior_rps × fanout` (then split-distributed across declared targets)
- Combine with `split`: `-> Target split: 50%, fanout: 3`
- `N` must be ≥ 1; sub-1 values are warned and clamped
- Sources with at least one `fanout > 1` outgoing edge gain the **Fan-Out** capability badge
- The legacy `xN` suffix (e.g. `... -> Target x5`) is removed — use `fanout: N`
### 4.6 Groups
```
[Group Name]
[Group Name] as alias
[Group Name] key: value
[Group Name] collapsed
```
- Bracket syntax only. Group coloring via tags (§2).
- Optional `as ` postfix (§2A) and same-line metadata.
- **No nesting.** A group cannot contain another `[...]` group; only indented components.
- **Collapse:** a bare `collapsed` trailing flag on the group line (`[Backend] collapsed`, §1.8) starts the group collapsed — it renders as a single node showing the worst child health. (Legacy: indented `collapsed true`.)
- Group properties (indented under the bracket line, colon-keyed like node properties §4.3):
- `instances: N` or `instances: N-M` — capacity multiplier on children (auto-scaling). The space forms `instances N` / `instances N-M` are accepted legacy.
### 4.7 Infra Options (Space-Separated, NO Colon)
- `direction-lr` / `direction-tb` (booleans, §1.9; default is LR)
- `default-latency-ms N`
- `default-rps N` — fallback edge RPS when no `rps` is declared on the edge node
- `default-uptime DECIMAL`
- `slo-availability DECIMAL` — target availability for SLO compliance highlighting
- `slo-p90-latency-ms N` — target p90 for SLO compliance highlighting
- `slo-warning-margin DECIMAL` — margin below SLO that triggers warning state
- `active-tag GroupName` / `active-tag none` — pre-select a tag filter on render
The universal options `no-title` and the `fill-*` family (§1.9) also apply — but severity/SLO status tints are data and are never restyled by the fill family.
### 4.8 Edge Nodes
```
edge
internet
```
Special top-level entry points. Either name works; `internet` only accepts `rps` and the `description` is silently ignored on entry-point nodes.
### 4.9 Node Descriptions
```
API Gateway
description: Handles routing and auth
description: Supports rate limiting
latency-ms: 50
max-rps: 8000
```
- `description: text` (colon required; a description always needs the `description:` keyword — bare prose lines are NOT auto-promoted)
- Multiple `description:` lines accumulate into a multi-line description
- Supports inline markdown: `**bold**`, `*italic*`, `` `code` ``, `[links](url)`
- `- bullet text` renders as `• bullet text`
- Descriptions are ignored on `edge` and `internet` nodes
---
## 5. Flowchart Diagrams
### 4.1 Declaration
```
flowchart [Title]
```
### 4.2 Node Shapes
| Shape | Syntax | Example |
|-------|--------|---------|
| Terminal | `(Label)` | `(Start)` |
| Process | `[Label]` | `[Do Task]` |
| Decision | `` | `` |
| I/O | `/Label/` | `/Read Input/` |
| Subroutine | `[[Label]]` | `[[Validate]]` |
| Document | `[Label~]` | `[Report~]` |
- Node coloring is **automatic by shape** — there is no manual node color. Start terminals render green, end terminals red, processes blue, decisions yellow, I/O purple, subroutines teal, documents orange. Flowcharts have **no tag groups and no node metadata**: a `tag … as …` block, a trailing color word, and a `Node s: value` suffix are all unsupported (a trailing suffix is ignored with a warning).
### 4.3 Arrows
| Type | Syntax |
|------|--------|
| Unlabeled | `->` |
| Labeled | `-label->` |
Flowchart edges have no color slot. Node colors are automatic by shape (§5.2) — flowcharts have no manual node color and do not use the tag system.
### 4.4 Groups
```
[GroupName]
[Child nodes...]
```
Bracket syntax only.
### 4.5 Edges (Indented Under Source)
> **Prefer indented-under-source form** (see §1.7).
Decision branches are indented under their source node — the most common and readable pattern:
```
(Start) -> [Validate Input] ->
-yes-> [Process Data] -> (Done)
-no-> [Show Error] -> [Validate Input]
```
Nested decisions:
```
-yes->
-yes-> [Process Request] -> (Done)
-no-> [Return 403] -> (End)
-no-> [Return 401] -> (End)
```
Inline chains (flat form) work for simple linear flows:
```
(Start) -> [Step 1] -> [Step 2] -> (End)
```
### 4.6 Options
- `direction-lr` / `direction-tb` (booleans, §1.9; default is TB — flowcharts read top-down). The key+value form `direction LR|TB` is accepted legacy.
- `fill-solid` / `fill-outline` (§1.9 fill family; default is the 25% tint — `fill-solid` renders shapes at full intent color, `fill-outline` drops the fill and carries color on the outline alone)
- `no-notes` (boolean; default off — suppress all note boxes, see §4.7)
### 4.7 Notes (Nodes)
Attach a hide-able annotation to a node with `note text`. The
`NodeId` is the node's label (quote it for multi-word labels). The note
renders as a folded-corner box near the node, tethered to it with a solid
connector line (UML note convention). The note floats beside the node —
defaulting to the right for top-down graphs and below for left-right
graphs — and is **collision-aware**: it flips to the opposite side (or
pushes further out) to keep a comfortable distance from other shapes and
other notes. It **never moves the node it annotates**, so the node keeps
its position and edge connections. Notes are **expanded at rest** (in both
the app and exports).
```
flowchart Order Pipeline
(Start) -> [Validate] -> (Done)
note Validate checks the payload schema
note "Start" entry point
```
**Multi-line body** — indent lines below the `note` heading (same as
sequence notes, §2.5). Bullets (`- `) and inline markdown
(`**bold**`, `*italic*`, `` `code` ``, `[link](url)`, bare URLs) are
supported:
```
note Validate
- rejects unknown fields
- **400** on bad schema
see `validate.ts`
```
**Color** — end the `note` heading line with a lowercase palette color word
(the universal trailing-token convention, §1.5) to recolor the note (faded
fill + matching border). Default is yellow.
```
note Validate checks the manifest red
note Launch all hands on deck green
```
Because the color word is the **last** token of the heading, capitalize it
to keep it as literal body text (`note Sky the sea turns Red` keeps "Red").
Rules:
- A note may be declared **before** its target node (forward references
resolve at end-of-parse).
- One note per node — a second note on the same node is ignored with a
warning. An unknown `NodeId` is an **error** (with a "did you mean?"
hint), never silently dropped. A note with no text is ignored with a
warning.
- Arrows (`->`) are allowed inside a note body. Only a line where `->`
immediately follows `note` (e.g. `note -> Active`) is treated as an
edge rather than a note — in state diagrams that is a transition from a
state named `note`.
- `no-notes` (diagram directive) suppresses every note box and reclaims
the space they reserve.
> Notes are wired into **flowchart**, **state**, **class**, **er**, and
> **boxes-and-lines** diagrams — the same `note ` syntax, multi-line
> body, trailing-color word, forward references, and `no-notes` opt-out
> throughout. The `NodeId` is the node's label: a class name, table name, or
> box label (quote multi-word labels). **Org and sitemap are intentionally
> excluded** — their indentation *is* the tree structure, which conflicts with
> the indented-body grammar (a note would silently swallow following nodes).
> Other chart types adopt the same syntax in later work.
---
## 6. State Diagrams
### 5.1 Declaration
```
state [Title]
```
### 5.2 States
```
StateName
[*] // initial/final pseudostate
```
### 5.3 Transitions
| Type | Syntax |
|------|--------|
| Unlabeled | `Idle -> Active` |
| Labeled | `Idle -submit-> Processing` |
### 5.4 Groups
```
[Group Name]
```
### 5.5 Notes (States)
State diagrams support the same `note text` annotation as
flowcharts — single-line or indented multi-line body, forward references,
and the `no-notes` opt-out. See §4.7 for the full rules.
```
state Connection
[*] -> Idle -> Active
note Idle waiting for input
```
### 5.6 Options
- `direction-lr` / `direction-tb` (booleans, §1.9; default is LR)
- `fill-solid` / `fill-outline` (§1.9 fill family; default is the 25% tint — collapsed groups follow the same mode, e.g. full saturation under `fill-solid`)
- `no-notes` (boolean; default off — suppress all note boxes, see §4.7)
- `active-tag GroupName` / `active-tag none`
### 5.7 Tags
State diagrams support the universal tag system (§1.3). Declare `tag as ` blocks above the diagram body and apply values via same-line `key: value` metadata on state lines:
```
state Document Lifecycle
tag Phase as ph
Intake blue
Review orange
Terminal green
Draft ph: Intake
Draft -submit-> In Review
In Review ph: Review
Published ph: Terminal
```
Tags color states exactly as in org and boxes-and-lines — 25% tint fill + solid outline, with the `fill-*` family (§1.9) applying as usual. Untagged states take the group's first value per §1.3.
> Historical note: state diagrams were long excluded from the tag system ("color the transitions instead"). That rationale predated the removal of edge colors (§1) and no longer holds — decision #48 brings state in line with the other node-and-edge charts.
---
## 7. Org Charts
### 6.1 Declaration
```
org [Title]
```
### 6.2 Nodes (Indentation = Hierarchy)
```
CEO
CTO
Engineer1
Engineer2
CFO
```
- Node coloring: use tags (§2)
- Same-line metadata: `Alice role: CEO, t: Exec`
### 6.3 Metadata (Indented, Colon REQUIRED)
```
Alice
role: Senior Engineer
location: NYC
```
This is key-value metadata assignment, consistent with same-line metadata syntax.
### 6.4 Containers
```
[Team Name]
members...
```
### 6.5 Options
- `direction-lr` / `direction-tb` (booleans, §1.9; default is TB)
- `sub-node-label Text`
- `show-sub-node-count` (the language's one documented positive opt-in flag — the badge is an auto-derived count, not authored data, and default-on would be noisy for small orgs; decision #48)
- `hide`
- `active-tag GroupName` / `active-tag none`
---
## 8. C4 Architecture Diagrams
### 7.1 Declaration
```
c4 [Title]
```
### 7.2 Elements
```
Name is a
Name is a container is a database // shape override
```
Types: `person`, `system`, `container`, `component`
Shape overrides: `database`, `cache`, `queue`, `cloud`, `external`
### 7.3 Element Metadata (Indented, Colon REQUIRED)
```
Web App is a container
description: SPA built with React
tech: React
```
Indented metadata uses colon-separated `key: value`, consistent with org charts and same-line metadata.
### 7.4 Same-Line Metadata
```
Web App is a container description: SPA, tech: React
```
Same-line metadata **requires** `key: value`. A non-empty tail with no `key:` (e.g. `Captain is a person Commands the fleet`) is a **hard error** and the diagram does not render — use `description: Commands the fleet`. (History: dropped silently before the 0.30.0 freeze; the freeze added `E_C4_BARE_TAIL_REMOVED`; #28 deleted that code and the case regressed to a silent drop again; restored to a hard error — with a generic `description:` hint, no dedicated code — on 2026-07-05.)
### 7.5 Relationships (Indented Under Source Element)
> **Prefer indented-under-source form** (see §1.7).
| Type | Syntax |
|------|--------|
| Sync labeled | `-Makes API calls-> API` |
| Sync with tech | `-Uses [HTTPS]-> API` |
| Async labeled | `~Sends emails~> Email` |
Relationships are indented under the source element:
```
Web App is a container
description: SPA built with React
-Makes API calls-> API
-Loads-> CDN
API is a container
-Reads/writes-> Database
~Sends events~> Queue
```
### 7.6 Sections
```
containers
...
components
...
deployment
container Web App // reference existing container
```
### 7.7 Options
- `direction-lr` / `direction-tb` (booleans, §1.9; default is TB)
- `active-tag GroupName` / `active-tag none`
### 7.8 Element Descriptions
```
// Indented metadata form (colon required)
Web App is a container
description: SPA built with React
description: Supports SSR and client-side routing
// Bare keyword form (DEPRECATED — emits a warning; prefer the colon form above)
API is a container
description Handles all REST endpoints
// Same-line metadata form
Database is a container description: PostgreSQL with read replicas
```
- The `description` keyword **requires a colon** (`description: text`). The bare `description text` form is removed at 1.0 and emits `E_DESCRIPTION_BARE_REMOVED` (an error; the value is still applied so the diagram renders).
- Multiple `description` lines accumulate into a multi-line description
- `description` is extracted as a dedicated field, not stored in general metadata
- Supports inline markdown: `**bold**`, `*italic*`, `` `code` ``, `[links](url)`
- `- bullet text` renders as `• bullet text`
---
## 9. Entity-Relationship Diagrams
> **Notes.** Annotate any table with `note text` — the same hide-able
> note box as flowcharts (multi-line body, trailing-color word, forward refs,
> `no-notes` opt-out). It floats beside the table without moving it. See §4.7.
### 8.1 Declaration
```
er [Title]
```
### 8.2 Tables
```
users
users blue
users domain: Core
```
- Same-line metadata on declaration line only
- Indented lines are columns or relationships
### 8.3 Columns (Indented, Space-Separated, NO Colon)
```
users
id int pk
name varchar
email string unique
created_at timestamp nullable
```
Format: `name [type] [constraints...]`
Constraints: `pk`, `fk`, `unique`, `nullable`
### 8.4 Relationships (Indented Under Source Table)
> **Prefer indented-under-source form** (see §1.7).
```
ships
id int pk
name varchar
captain_id int fk
1-aboard-* crew_members
?-frequents-1 ports
```
Columns and relationships can be intermixed under the same table. Cardinality symbols: `1` (one), `*` (many), `?` (optional)
### 8.5 Options
- `notation labels` — render relationship cardinalities as text labels. **Default is crow's-foot markers** (the coded default; any `notation` value other than `labels` falls back to crow's-foot)
- `active-tag GroupName` / `active-tag none`
- `no-semantic-colors` — bare flag; suppress the semantic PK/FK role colors (on by default). View-state opt-out written by the app's Semantic-colors toggle.
---
## 10. Class Diagrams
> **Notes.** Annotate any class with `note text` — the same
> hide-able note box as flowcharts (multi-line body, trailing-color word,
> forward refs, `no-notes` opt-out). It floats beside the class without moving
> it. See §4.7 for the full rules.
### 9.1 Declaration
```
class [Title]
```
### 9.2 Classes
```
Ship
abstract Vessel
interface Serializable
Ship extends Vessel
Galleon implements Serializable
Galleon extends Ship implements Serializable
enum ShipType
```
`extends` and `implements` may co-occur on the same declaration line. Order is fixed: `extends` before `implements`. Each may name only one parent / interface.
### 9.3 Members (Indented, Colon for Types)
**Fields:**
```
+ name: string
- speed: number
# protectedField: int
```
**Methods:**
```
+ sail(): void
- calculate(x: number): boolean
+ getName() {static}: string
```
Visibility: `+` public, `-` private, `#` protected
**Enum values:**
```
enum ShipType
Galleon
Sloop
```
### 9.4 Relationships (Indented Under Source Class)
> **Prefer indented-under-source form** (see §1.7).
| Relationship | Arrow |
|-------------|-------|
| Inheritance | `--|>` |
| Implementation | `..\|>` |
| Composition | `*--` |
| Aggregation | `o--` |
| Dependency | `..>` |
| Association | `->` |
Relationships are indented under the source class:
```
Ship
--|> Vessel
*-- Cannon
```
Optional label: `--|> Vessel : extends` (colon optional before label)
`extends` and `implements` on class declarations still work as part of the declaration syntax.
### 9.5 Options
_(No chart-specific options. The universal directives `no-title` and the `fill-*` family (§1.9) apply.)_
---
## 11. Kanban Boards
### 10.1 Declaration
```
kanban [Title]
```
### 10.2 Columns
```
[Column Name]
[Column Name] color wip: 3
[Column Name] collapsed
```
A column may carry the bare `collapsed` trailing flag (§1.8) to start folded to
a narrow strip; the app writes/removes it on the column toggle and it
round-trips to any renderer (legacy: `collapsed: true`).
### 10.3 Cards (Indented Under Columns)
```
[To Do]
Card title priority: High, c: Owner
Detail text (indented deeper)
```
### 10.4 Options
- `hide`
- `active-tag GroupName` / `active-tag none`
- `lane-by GroupName` — slice the board into swimlanes by a tag group (the swimlane axis). Named `lane-by`, not `swimlane`, because `swimlane` is itself a chart type. View-state directive written by the app's swimlane picker.
---
## 12. Sitemap Diagrams
### 11.1 Declaration
```
sitemap [Title]
```
### 11.2 Pages (Indentation = Hierarchy)
```
Home
About
Pricing Auth: Public
Enterprise
Blog
```
### 11.3 Arrows
> **Prefer indented-under-source form** (see §1.7).
```
Home
-pricing-> Pricing
-login-> Login
```
Arrows can target containers using bracket syntax:
```
Home
-> [Port Market]
```
All permutations are supported: node→group, group→node, group→group.
```
[Port Market]
Shop
-> [Warehouse]
[Warehouse]
Storage
```
When a group is the source, place the arrow directly after the group header (before any child pages). Brackets are required to distinguish group targets from page targets.
### 11.4 Containers
```
[Marketing]
Pricing Auth: Public
```
A container holds a **flat list of pages** — pages inside a `[Container]` may
**not** have indented sub-pages. Over-indenting a page under another page within
a container is an **error**:
```
[Marketing]
Docs
Guides ← ERROR: pages inside a container can't have sub-pages
API Reference ← ERROR
Blog
```
> `Pages inside container '[Marketing]' cannot have indented sub-pages.`
Sub-page hierarchy via indentation is supported only at the **top level** (§11.2),
outside any container. Indented **arrows** under a container page are still
allowed (they declare edges, not sub-pages):
```
[Workspace]
Dashboard
-projects-> Projects ← OK: arrow, not a sub-page
Projects
```
### 11.5 Options
- `direction-lr` / `direction-tb` (booleans, §1.9; default is LR)
- `active-tag GroupName` / `active-tag none`
### 11.6 Node Descriptions
```
// Bare keyword form (DEPRECATED — emits a warning; prefer description: above)
About
description Company history and team bios
// Same-line metadata form
Pricing description: Compare plans and features
// Multi-line
Blog
description: Engineering and product updates
description: Published weekly
```
- `description: text` (colon required; bare prose lines are not auto-detected as descriptions)
- Multiple `description:` lines accumulate into a multi-line description
- Supports inline markdown: `**bold**`, `*italic*`, `` `code` ``, `[links](url)`
- `- bullet text` renders as `• bullet text`
---
## 13. Gantt Charts
> Dates (`start-date`, task `start:`, eras, markers, holidays, `today-marker`, `sprint-start`) follow the liberal grammar + year ladder in [§2B Universal Date Handling](#universal-date-handling).
### 13.1 Declaration
```
gantt [Title]
```
### 13.2 Options (Space-Separated, NO Colon)
```
start-date 2026-03-15
today-marker
today-marker 2026-03-27
critical-path
no-dependencies
// swimlane axis (canonical); equivalent to `sort tag:Team`
lane-by Team
// back-compat spelling of lane-by
sort tag:Team
active-tag GroupName
active-tag none
sprint-length 2w
sprint-number 5
sprint-start 2026-01-05
```
`start-date` anchors the chart start (canonical — shared spelling with PERT, §13A.2); the historical bare `start` is accepted legacy.
### 13.3 Holidays
```
holiday
2024-02-19 Presidents Day
2024-05-27 -> 2024-05-29 Memorial Weekend
```
### 13.4 Workweek
```
workweek mon-fri
workweek sun-thu
```
Top-level directive (not nested under `holiday`).
### 13.5 Eras
**Flat form:**
```
era 2026-04-06 -> 2026-04-10 Conference purple
```
**Block form:**
```
era
2026-04-06 -> 2026-04-10 Conference purple
2026-06-01 -> 2026-06-05 Sprint Review blue
```
### 13.6 Markers
**Flat form:**
```
marker 2026-03-27 Board Review
```
**Block form:**
```
marker
2026-03-27 Board Review
2026-06-15 Release green
```
### 13.7 Groups (Swimlanes)
```
[Backend] t: Engineering
```
Bracket syntax only.
### 13.8 Tasks
Duration is positional — the last whitespace-separated token (right-to-left scan):
```
Database Schema 20bd
API Integration 10bd t: Engineering
Launch Day 0d
Research Phase 10bd?
Data Migration 30bd progress: 80
```
**Positional duration:** `Task Name 30bd` — duration is the rightmost token matching ``.
**Trailing metadata:** `Task Name 30bd t: Engineering, progress: 80` — comma-separated `key: value` pairs after the duration.
**Milestones:** `Launch 0d` — zero-duration task renders as a diamond marker.
**Uncertain:** `Task Name 10bd?` — trailing `?` on the duration token.
**Progress:** `progress: 80` in trailing metadata (integer 0–100; implicit percent — write `progress: 80`, not `progress: 80%`).
**Offset prefix:** `+4w Task Name 30bd` — task starts at chart-start + 4 weeks. Useful for tasks not chained by dependencies. Offset token is `+` before the task name.
**Group offsets:** `+2w [Mobile]` — floors all tasks inside the group to chart-start + 2 weeks.
**Scope-based metadata:** indented `key: value` on the line below attaches to the parent task:
```
API Integration 10bd
t: Engineering
progress: 50
```
**Fallback metadata keys:** `duration:` and `start:` are accepted as same-line metadata for ambiguous names:
```
Setup start: 2026-03-15, duration: 30d
Phase 2 duration: 10bd
```
Duration units: `min`, `h`, `d`, `bd` (business days), `w`, `m`, `q`, `y`, `sp` (sprints — see §13.11); bare `s` accepted legacy in gantt/pert
### 13.9 Dependencies (Arrow Graph)
Arrows create dependencies AND define tasks in one line:
```
[Backend]
Database Schema 20bd
-> API Integration 10bd
-> E2E Testing 5bd
Auth Module 15bd
```
**Bare siblings are parallel by default** — `Database Schema` and `Auth Module` above start at the same time (no `parallel` keyword needed).
**Arrow = sequential:** `-> Next Task 10bd` indented under a task means "Next Task starts after parent finishes."
**Lag (delay):** `-3w-> Next Task 10bd` — target starts 3 weeks after source finishes.
**Lead (overlap):** `--2w-> Next Task 10bd` — target starts 2 weeks before source finishes.
**Cross-group references:** `-> [Backend].API Design` — reference-only dependency (does not create a new task):
```
[Frontend]
Wireframes 10bd
-> [Backend].API Design
```
**Multiple arrows from one task:**
```
API Integration 10bd
-> E2E Testing 5bd
-> Documentation 3bd
```
### 13.10 Scheduling Model
- **Bare siblings** = parallel (start at chart start, or group start if offset).
- **Arrow** = sequential (target starts after source finishes, plus optional lag/lead).
- **Offset prefix** = pinned start relative to chart start.
This replaces the legacy `parallel` keyword — groups and arrows express all scheduling relationships.
**Removed at 1.0 (error `E_GANTT_LEGACY_REMOVED`):** the four pre-1.0
scheduling shorthands no longer parse as valid syntax — each raises
`E_GANTT_LEGACY_REMOVED` (error; the value is still applied best-effort so
the diagram renders):
1. The `parallel` keyword — sibling tasks already run in parallel by default; delete it.
2. Duration-before-name — `30bd Database Layer` → positional `Database Layer 30bd` (or `Database Layer duration: 30bd`).
3. Explicit-date task — `2024-01-15 Kickoff` → `Kickoff start: 2024-01-15`.
4. Legacy timeline-duration — `2024-01-15 -> 30d Build` → `Build start: 2024-01-15, duration: 30d`.
**Kept (NOT legacy):** `duration:` and `start:` are canonical **fallback
metadata keys** (§13.8) — they parse cleanly with no error. Use them for
ambiguous names where the positional/offset forms are awkward.
### 13.11 Sprints
The `sp` duration unit expresses task duration in sprints. Sprint length is configurable via `sprint-length` (default `2w`). Using `sp` in any task auto-enables sprint mode; explicit `sprint-*` options also enable it.
**Options** (space-separated, no colons):
| Option | Default | Description |
|--------|---------|-------------|
| `sprint-length ` | `2w` | Duration of one sprint. Only `d` and `w` units allowed. |
| `sprint-number ` | `1` | Which sprint the chart starts at (positive integer). |
| `sprint-start ` | chart `start-date` or today | YYYY-MM-DD anchor date for `sprint-number`. |
**Duration resolution:** `2sp` with `sprint-length 2w` = 4 calendar weeks (28 days). Fractional sprints work: `0.5sp` = half a sprint. Changing `sprint-length` rescales all `sp`-based durations across the chart.
**Sprint bands:** When sprint mode is active, alternating shaded vertical bands render on the timeline with adaptive labels ("Sprint N", "N", or every 5th). Dashed boundary lines mark sprint edges.
**Eras vs sprints:** Eras and sprints share the same visual lane. When both are defined, eras display by default (with a toggle in the app). In static SVG, eras always win.
**Note:** `sp` means sprints; the historical bare `s` still parses in Gantt/PERT context but is no longer documented — timeline's `s` is seconds. Sprints use calendar time, not business days. For teams with irregular sprint calendars, use eras instead.
```
gantt
sprint-length 2w
sprint-number 5
sprint-start 2026-01-05
start-date 2026-03-01
[Phase 1]
Design 2sp
-> Build 3sp
-> QA 1sp
```
---
## 13A. PERT Diagrams
> Anchor dates (`start-date`, `end-date`, `sprint-start`) follow the liberal grammar + year ladder in [§2B Universal Date Handling](#universal-date-handling). Anchors require a full (day-grain), calendar-valid date.
PERT diagrams visualize project networks with three-point duration estimates and surface critical path, slack, and project μ/σ. Activity-on-Node notation: each activity is a rectangle node; dependencies are arrows between activities.
### 13A.1 Declaration
```
pert [Title]
```
### 13A.2 Directives (Space-Separated, NO Colon)
```
time-unit w # Unit for bare-number durations (default: d)
default-confidence medium # M-only heuristic level: high | medium | low (default: medium)
# …or a custom O/P factor pair: default-confidence 0.6/2.5
# Per-activity override via same-line `confidence: low` metadata
direction-lr # Layout direction booleans (§1.9): direction-lr (default) | direction-tb
# Key+value form `direction LR|TB` accepted legacy
node-detail compact # Visual density: compact (default) | full
no-analysis # Bare flag — hide the analysis layer (tornado + S-curve); on by default — see §13A.10
trials 10000 # Canonical-MC trial count
seed 42 # Mulberry32 PRNG seed for determinism
scrubber-trials 300 # Fast-MC trials for the duration scrubber
start-date 2026-06-01 # Anchor project start (forward pass) — see §13A.12
end-date 2026-09-15 # Anchor project end (backward pass) — see §13A.12
# Specify start-date OR end-date (not both); 'now' valid for start-date only
sprint-length 2w # Sprint length (default 2w; only when sprint mode active)
sprint-number 5 # Starting sprint label N — cells render as S (default 1)
sprint-start 2026-06-01 # Optional ISO date the starting sprint begins on
active-tag Crew # Activate a non-first tag group (first is active by default) — see §13A.2a
```
**Sprint mode** is activated automatically when `time-unit sp` is set (`sp` = sprints, mirroring the Gantt duration unit §13.11; bare `s` accepted legacy), or explicitly when any `sprint-*` directive appears. In sprint mode, ES/EF/LS/LF cells render as `S5`, `S7`, `S12` etc. instead of numeric offsets or ISO dates, and the project-stats summary reports duration in *sprints*. Mirrors the Gantt sprint surface.
### 13A.2a Tags
PERT supports the universal tag system (§1.3). Declarations live above the diagram body and apply to activities (and groups) via same-line metadata.
```
pert Pirate Voyage by Crew Role
time-unit w
default-confidence medium
tag Crew as c
Captain red
Bosun orange
Quartermaster blue
recruit crew 1 2 4 c: Quartermaster
load powder 0.5 1 2 c: Bosun
```
- **Activities** carry `: ` (or `: `) inside the existing same-line metadata segment alongside other keys like `confidence` and `collapsed`.
- **Group headers** also accept tag metadata: `[outfit ship] c: Bosun` colors the cluster header.
- **The first declared group is active by default** (per §1.3). `active-tag ` only matters with ≥2 groups when you want a non-first group active; `active-tag none` suppresses all colouring (cards render neutral, all groups collapsed).
- **Visual placement**: when a group is active, the activity card's **middle (name) band** picks up the tag color. The card border continues to communicate criticality (red/orange/etc bands per §13A.10), so tag fill and crit border compose without conflict. Milestone diamonds adopt the tag color across the full pill.
The **analysis layer** (tornado + S-curve, §13A.10) renders **by
default** in every render — CLI, embedded docs, share link, export —
whenever Monte Carlo ran (at least one O/M/P estimate). The bare
`no-analysis` flag (mirrors `no-title`) suppresses it for a cleaner,
network-only view. Precedence at render time: an explicit `viewState.an`
(desktop-app toggle or share link) wins; otherwise `no-analysis` decides;
otherwise the layer is on. In analytical mode (no MC) the layer is
silently omitted regardless. The legacy `analysis` directive (historically
`analysis monte-carlo`, to opt into simulation — now auto-derived from
data, §13A.9) is **removed at 1.0**: it emits an `error`-severity
diagnostic (`E_PERT_ANALYSIS_REMOVED`) and is otherwise ignored (the
diagram still renders). Use the bare `no-analysis` flag to hide the layer.
### 13A.3 Activities
An activity is ` [] [as ] [metadata]`. Durations follow the name, separated by spaces or commas. Three forms:
| Form | Meaning |
| ---- | ------- |
| `recruit crew 1 2 4` | Three-point estimate: O M P (in `time-unit` weeks) |
| `recruit crew 2` | M-only: parser fills O and P from `default-confidence` factors |
| `celebrate` | TBD: no estimate; downstream activities inherit `?` |
Two-number durations are an error (the parser cannot disambiguate between O+M, M+P, or O+P).
```
pert
time-unit w
default-confidence medium
recruit crew 1 2 4 as rc confidence: low
careen hull 1.5
load powder
celebrate
```
### 13A.4 Dependencies
Indented `-> dest` lines under an activity declare a dependency from that activity to `dest`. The destination must reference a **previously-declared** activity name or alias — inline durations on the arrow line (`-> name 1 2 4`) are **rejected** by the parser. Declare the destination on its own source line first, then reference it.
```
// ✅ Declare destinations first; arrows reference by name/alias only
load powder 0.5 1 2
careen hull 1.5
recruit crew 1 2 4 as rc
-> load powder
-> careen hull
// ❌ Inline forward-declaration is rejected:
// "Inline forward-declaration not allowed — move attributes to a source line"
recruit crew 1 2 4 as rc
-> load powder 0.5 1 2
```
Each activity has **one declaration site**. Repeating an activity's name on a non-indented line is allowed (it acts as a "source line" for further `->` arrows) but providing conflicting duration tokens raises a diagnostic that names both line numbers.
#### Dependency types and lag/lead
Edges default to **Finish-to-Start (FS) with zero lag** — the dominant case. The arrow may carry an inline label between two dashes to override either piece:
```
A -> B # FS, 0 lag (default — unchanged)
A -SS-> B # Start-to-Start
A -2d-> B # FS with +2d lag (lag-only shortcut)
A -SS+2d-> B # SS with +2d lag
A -FF-1d-> B # FF with -1d lead (negative lag)
A -SF+3d-> B # SF with +3d lag
```
Grammar: `-[TYPE][±LAG]->` — both pieces optional, but at least one must be present when a label is given.
| Type | Constraint | Use case |
|------|------------|----------|
| **FS** | `B.ES ≥ A.EF + lag` | Default; sequential work |
| **SS** | `B.ES ≥ A.ES + lag` | Parallel start (`SS+2d` = B starts 2 days after A starts) |
| **FF** | `B.EF ≥ A.EF + lag` | Synchronized finish (ship-together activities) |
| **SF** | `B.EF ≥ A.ES + lag` | Rare; included for completeness |
Type names are case-insensitive (`SS`, `ss`, `Ss` all valid). Lag amount inherits the diagram's `time-unit`; per-edge unit overrides are accepted (`-SS+2d->`, `-FF+4h->`). A `-` sign denotes a **lead** (overlap): `-FS-1d->` = successor starts 1 day before predecessor finishes.
The renderer paints a small midpoint label on every non-default edge (`SS +2d`, `FF -1d`, `+2d` for FS-only lag). FS+0 edges remain visually clean.
A diagnostic warning fires when a lead's magnitude exceeds the predecessor's duration (an FS edge that implies the successor starts before the predecessor — logically impossible).
There is no `default-edge-type` directive: every `->` is FS regardless of what's been written elsewhere in the file. This keeps each edge self-evident to readers and prevents copy-paste from silently flipping semantics.
### 13A.5 Aliases (`as `)
The universal `as ` suffix (§2A) applies. PERT aliases share namespace with activity names. Names containing the literal token `as` parse cleanly when no actual alias suffix is appended (e.g. `serve as quartermaster 2 3 5`).
### 13A.6 Milestones
Milestones are zero-duration nodes rendered as diamonds. They participate in the dependency graph and critical-path computation and are excluded from criticality summary statistics.
**There is no `milestone` keyword.** Declare a milestone as a zero-duration activity using the standard activity grammar — ` 0` (M-only zero) or ` 0 0 0` (three-point zero):
```
voyage approved 0
landfall 0 0 0
```
Both forms render as diamonds. The parser explicitly rejects the legacy `milestone ` keyword with `Unknown keyword 'milestone'`.
### 13A.7 Groups
Bracketed `[group-name]` blocks group activities. Whether a group renders as a hammock super-edge or a cluster bounding rectangle is auto-detected from edge topology:
- **Hammock**: single entry + single exit → collapses to a labeled super-edge with rolled-up μ ± σ
- **Cluster**: multi-entry or multi-exit → renders as a tinted bounding rectangle
```
[outfit ship]
recruit crew 1 2 4
-> load powder
careen hull 1 1.5 2.5
-> load powder
load powder 0.5 1 2
-> sail to atoll
sail to atoll 3 5 8
```
Groups can author a bare `collapsed` trailing flag (§1.8) to start collapsed (legacy: `collapsed: true`).
### 13A.8 Same-Line Metadata
| Key | Where | Meaning |
| --- | ----- | ------- |
| `confidence` | activity | Per-activity override of `default-confidence` (`high` / `medium` / `low` / `O/P`) |
| `collapsed` | group | bare trailing flag (§1.8) — start the group collapsed |
| tag aliases (e.g. `c: Captain`) | activity, group | Resolves to the declared tag group (`tag Crew as c …`); drives node fill when the group is active. See §13A.2a |
### 13A.9 Analysis
Mode is auto-derived from the data:
The forward/backward pass, slack, M-world critical path, project μ along that path, and project σ from the variance sum on that path are always computed. Activities downstream of a TBD activity inherit `null` for ES/EF/LS/LF/slack; the project end likewise becomes `?` until all upstream TBDs are estimated.
On top of that base analysis, Monte Carlo simulation runs whenever **at least one non-milestone activity has a duration** — either an explicit three-point (O M P) estimate or an M-only value whose O/P are filled from `default-confidence`. The canonical run uses N = `trials` Beta-PERT samples with seed = `seed`, and reports P50/P80/P95 plus per-activity criticality index. The interactive duration scrubber uses `scrubber-trials` samples for live re-analysis.
When no non-milestone activity has any duration (or every terminal is poisoned by TBD propagation), MC is skipped and the diagram renders in **analytical** mode — caption shows expected duration only, no percentile rows or S-curve.
> Heads-up: an all-M-only diagram still produces real MC percentiles, but the *spread* is a mechanical function of `default-confidence` (every activity's σ scales with M). The simulation is honest about its inputs; it cannot be more informed than the confidence directive it was given.
`trials < 100` clamps mode back to analytical (with a one-sentence caveat in the project-stats caption) — ten-trial samples produce nonsense percentiles. The `trials`, `seed`, and `scrubber-trials` directives stay valid but are silently inert in analytical mode.
### 13A.10 Visual Encoding
| Channel | When applied |
| ------- | ------------ |
| Project subtitle | always (when the analyzer produces μ or an anchor) — rendered as a single muted line under the diagram title (or in the title slot when `no-title` suppresses the title). Restates the project's total duration with σ in parentheses so the headline number stays visible independent of the Analysis-row toggle. Shape per mode: **forward** → `Expected finish: · ≈ <μ> of work (± <σ>)`; **backward** → `Expected start: · ≈ <μ> lead time (± <σ>)`; **unanchored** → `≈ <μ> (± <σ>)`. TBD upstream renders `?` placeholders in the same slots so the line stays consistent shape-wise. The `± <σ>` parenthetical drops when σ is zero or MC didn't run |
| Project-stats caption | always (when the analyzer succeeds) — rendered **below the diagram body** as a node-styled rounded rectangle with bullet-list, left-aligned content. Reports expected duration and — in Monte-Carlo mode — standard deviation and percentile dates. The percentile framing is mode-dependent: **forward / no anchor** → P50/P80/P95 *finishes* (start fixed, when do we finish?); **backward** → P50/P80/P95 *latest-safe starts* (end fixed, when must we start?). See §13A.12. Each percentile gets its own bullet. Critical path, bottleneck, and hidden-risk activities are conveyed visually via red/orange border bands rather than text |
| Analysis layer (tornado + S-curve) | Monte-Carlo only. Renders **by default**; precedence is `viewState.an` (app toggle / share link) if set, else suppressed by the bare `no-analysis` directive, else on. Auto-omitted in analytical mode (no MC). See §13A.2, §13A.9 |
| S-curve (completion probability) | part of the analysis layer (above). **Forward / no anchor:** x = finish date, y = `P(finish ≤ x)`, rising 0 → 1 ("chance we're done by date X"). **Backward:** x = candidate start date with x_max = `end-date`, y = `P(finish ≤ end-date \| start = x)`, falling 1 → 0 ("chance we still hit the deadline if we start by date X"). Same Monte-Carlo trial set under a different time axis. Vertical reference lines mark the percentile dates from the caption (P50/P80/P95 finishes vs latest-safe starts) so the chart and caption are visually linked. See §13A.12 |
| Tornado (sensitivity ranking) | part of the analysis layer (above). Horizontal bars ranking activities by Schedule Sensitivity Index — the swing in project completion between the activity's optimistic (O) and pessimistic (P) ends. Sorted longest-first, top-N shown, each bar colored by the activity's criticality band |
| Name + μ | always |
| Diamond | milestones only |
| Dashed border | TBD activity or downstream-of-TBD |
| Red border + edge stroke (`palette.colors.red`) | binary critical path (analytical mode, no MC) |
| Red / orange / yellow / green / blue border + edge bands | criticality index when MC is on — red ≥ 0.80, orange ≥ 0.50, yellow ≥ 0.25, green ≥ 0.10, blue ≥ 0.02 |
| Slack bar | only when `node-detail full` |
| σ-as-border-thickness | only when `node-detail full` |
The project-stats caption is rendered below the title (or at the top when no title) as a centered, multi-line `text` block. It reports expected duration and — in Monte-Carlo mode — standard deviation and percentile dates (finishes in forward / no-anchor mode, latest-safe starts in backward mode; see §13A.12). Critical-path, bottleneck, and hidden-risk emphasis is delegated to the diagram itself (red/orange node-border bands).
Comments use `#` line-leading (or trailing-after-whitespace) for PERT specifically — both `#` and `//` are recognized.
### 13A.11 Worked Example
```
pert Pirate Voyage
time-unit w
default-confidence medium
voyage approved 0
-> recruit crew
[outfit ship]
recruit crew 1 2 4 as rc confidence: low
-> load powder
careen hull 1 1.5 2.5
-> load powder
load powder 0.5 1 2
-> sail to atoll
sail to atoll 3 5 8
-> count gold
-> repair hull
count gold 0.5d
-> divvy shares
repair hull 2 3 5 confidence: low
-> divvy shares
divvy shares 1 2 3
```
**Variant: with date anchoring.** Add `start-date` to render every ES / EF / LS / LF as a real calendar date instead of a numeric offset. Slack still reports as a duration (calendar days when anchored). Critical-path coloring is unchanged.
```
pert Pirate Voyage
time-unit w
default-confidence medium
start-date 2026-06-01 # NEW: anchor
voyage approved 0
-> recruit crew
recruit crew 1 2 4 as rc
-> load powder
load powder 0.5 1 2
-> sail to atoll
sail to atoll 3 5 8
-> count gold
-> repair hull
count gold 0.5
-> divvy shares
repair hull 2 3 5
-> divvy shares
divvy shares 1 2 3
```
With `time-unit w` and `start-date 2026-06-01`, the `recruit crew` activity (μ ≈ 2.17w ≈ 15 days) renders `ES: 2026-06-01`, `EF: 2026-06-16`. Slack normalizes to `Xd` everywhere.
### 13A.12 Date Anchoring
PERT charts compute ES / EF / LS / LF in the diagram's `time-unit`. The `start-date YYYY-MM-DD` and `end-date YYYY-MM-DD` directives anchor those numbers to a calendar so all four cells render as real dates. They are **mutually exclusive** — author one or the other, not both.
**Terminology.** *Anchor* refers to the user-supplied date (`start-date` or `end-date`). *projectStart* refers to the date the formatter uses internally — equal to the start-date in forward mode, derived as `end-date − projectMu` in backward mode. The chart never displays projectStart directly except via the corner-cell labels of source/sink activities.
**Critical-path framing.** On critical-path activities, `LS = ES` and `LF = EF` — there is no slack — so the corner-cell labels are unambiguous regardless of forward/backward mode. The "earliest possible" framing of backward mode applies only to **non-critical** activities, whose `ES`/`EF` represent the earliest start/finish given the deadline. The chart's title annotation establishes this framing once for the whole diagram, but readers can rely on critical-path nodes (red/orange band coloring) being mode-invariant.
**Visual node-card specimens.** The same activity (`recruit crew`, μ ≈ 2.17w) under each mode:
*No anchor:*
```
┌──────┬──────┬──────┐
│ 0 │ 2.17w│ 2.17w│
├──────┴──────┴──────┤
│ recruit crew │
├──────┬──────┬──────┤
│ 0 │ 0w │ 2.17w│
└──────┴──────┴──────┘
```
*Forward anchor* (`start-date 2026-06-01`):
```
┌────────────┬──────┬────────────┐
│ 2026-06-01 │ 2.17w│ 2026-06-16 │
├────────────┴──────┴────────────┤
│ recruit crew │
├────────────┬──────┬────────────┤
│ 2026-06-01 │ 0d │ 2026-06-16 │
└────────────┴──────┴────────────┘
```
*Backward anchor* (`end-date 2026-09-15`, derived projectStart = 2026-06-01):
```
┌────────────┬──────┬────────────┐
│ 2026-06-01 │ 2.17w│ 2026-06-16 │ ← critical-path: LS=ES, LF=EF
├────────────┴──────┴────────────┤
│ recruit crew │
├────────────┬──────┬────────────┤
│ 2026-06-01 │ 0d │ 2026-06-16 │
└────────────┴──────┴────────────┘
(italic title note above chart: "Backward-anchored from end-date 2026-09-15 (as of 2026-05-10)")
```
`mu` always renders as a duration (`2.17w`) regardless of mode — it's a timespan, not a date. `slack` normalizes to days (`0d`) when anchored.
**Latest-safe start (backward mode).** When `end-date` is set and Monte Carlo is active, the project-stats caption reframes its percentile rows from *finishes* to *latest-safe starts*. Each row reports `end-date − duration_PX` — the latest date the project can begin and still hit the deadline with that confidence:
```
Pirate Voyage — Backward-anchored from end-date 2026-09-15 (as of 2026-05-10)
• Expected duration: 15w
• σ: 2w
• P50 latest-safe start: 2026-06-08
• P80 latest-safe start: 2026-05-27
• P95 latest-safe start: 2026-05-13
```
Higher confidence demands an earlier start: P95 is *earlier* than P50, mirroring how P95 finish is *later* than P50 finish in forward mode. Analytical mode (no MC) omits the percentile rows; the μ-derived projectStart already appears in source-activity ES cells, so the caption would only duplicate that single date.
**Rounding direction.** When sub-day precision arises (e.g. fractional days from `time-unit h` arithmetic), latest-safe-start dates round **down** to the nearest whole day — earlier date = more schedule buffer = more conservative. Forward-mode finishes round **up** for symmetric conservatism. This rule ensures that a reported date never *overstates* the safety margin.
**Past-date annotation.** When a latest-safe-start date falls in the **past** relative to the parse-time today date (i.e. `duration_PX > end-date − today`), the row appends `(latest-safe start has passed)` so readers don't silently misread a pre-today date as actionable:
```
• P95 latest-safe start: 2026-04-01 (latest-safe start has passed)
```
The annotation is factual — no verdict on whether the project is "infeasible"; the math is reported honestly and the reader judges. P50/P80 rows in the same caption may still be feasible and render without the annotation.
**"Today" capture timing.** The past-date check uses the **parse-time** today date — captured once when the analyzer runs, then frozen. This matches the `start-date now` precedent (which substitutes its resolved date before share-link compression) so that a shared backward-anchored diagram preserves the author's view rather than re-evaluating against each recipient's local date. To make this freshness signal legible, the backward-anchored title annotation includes the parse-time today date in an `(as of YYYY-MM-DD)` suffix:
> *Backward-anchored from end-date 2026-09-15 (as of 2026-05-10)*
Recipients who see a stale `(as of …)` know to re-render for an updated past-date check.
**Analytical mode (no MC) + past projectStart.** Analytical-mode backward anchoring renders only the μ-derived projectStart in source-activity ES cells — there are no percentile rows to annotate. The ES cell date itself communicates "past" via its calendar value; no separate past-date annotation is emitted. (If you need a confidence-banded readout, add three-point estimates to switch to Monte-Carlo mode.)
If any TBD activity makes `projectMu` indeterminate, all percentile rows render `?` (the analyzer has no duration distribution to subtract from `end-date`); the expected-duration row likewise renders `?`.
**Completion-probability S-curve axes.** The optional S-curve widget (§13A.10) interprets the Monte-Carlo trial distribution differently per mode:
| | Forward / no anchor | Backward |
| --- | --- | --- |
| x-axis | finish date, x_min = `projectStart + min(trial_durations)`, x_max = `projectStart + max(trial_durations)` | candidate start date, x_min = `end-date − max(trial_durations)`, x_max = `end-date` |
| y-axis label | `P(finish ≤ x)` | `P(hit deadline \| start by x)` |
| Formula | `CDF_duration(x − projectStart)` | `CDF_duration(end-date − x)` |
| Direction | Rising 0 → 1 | Falling 1 → 0 |
| Reads | "chance we're done by date X" | "chance we still hit the deadline if we start by date X" |
The backward curve is the forward curve mirrored horizontally and shifted — same trial distribution, different time axis. Both modes annotate the curve with vertical reference lines at the percentile dates reported in the caption (P50/P80/P95 finishes in forward mode, P50/P80/P95 latest-safe starts in backward mode). Authors should be able to read the chart and the caption as one cross-referenced unit.
**S-curve degenerate cases:**
- Monte Carlo did not run → S-curve silently omitted.
- `projectMu` indeterminate (any upstream TBD) → empty axes with a `?` placeholder.
- A reference line in backward mode that resolves to a past date (matching the caption's past-date annotation) renders dashed instead of solid; the percentile label includes a `(past)` suffix.
**Special tokens and degenerate cases.**
- `start-date now` resolves to today's local date at parse time. Share-link encoding substitutes the resolved date BEFORE compression so the recipient sees the dates the author saw, not "today, wherever you are." `end-date now` is a parse error.
- Backward-anchored diagram with any TBD activity upstream → `projectStart` cannot be derived; all four schedule cells render `?`. The title annotation still names the end-date.
- `time-unit bd` + anchor → warning (`bd` is treated as calendar days for date math; for business-day scheduling use Gantt).
- `time-unit min` / `h` + anchor → warning (date display rounds to whole days).
**Diagnostic codes.**
| Code | Severity | Triggered when |
| ---- | -------- | -------------- |
| `E_PERT_BOTH_ANCHORS` | error | `start-date` AND `end-date` both present (clears any partial anchor state) |
| `E_PERT_INVALID_DATE` | error | YYYY-MM-DD malformed or fails round-trip date check |
| `E_PERT_END_DATE_NOW` | error | `end-date now` (only `start-date now` is valid) |
| `W_PERT_BD_WITH_ANCHOR` | warning | `time-unit bd` combined with an anchor |
| `W_PERT_SUBDAY_WITH_ANCHOR` | warning | `time-unit min`/`h` combined with an anchor |
---
## 14. Boxes and Lines Diagrams
> **Notes.** Annotate any box with `note text` — the same hide-able note
> box as flowcharts (multi-line body, trailing-color word, forward refs,
> `no-notes` opt-out). It floats beside the box (respecting the diagram's
> LR/TB direction) without moving it. See §4.7 for the full rules.
### 13.1 Declaration
```
boxes-and-lines [Title]
```
Requires explicit first line — no heuristic detection. Default direction is left-to-right.
### 13.2 Nodes
```
NodeLabel
NodeLabel key: value, key2: value2
NodeLabel description: Some text here
```
Nodes are created explicitly or implicitly (when referenced in edges). All nodes render as uniform rounded rectangles.
The `description` key is extracted as a dedicated field and not stored in metadata.
### 13.3 Edges
> **Prefer indented-under-source form** (see §1.7). Declare the node, then indent its outgoing edges below it.
```
Source -> Target
Source -> Target key: value
Source -label-> Target
Source <-> Target
Source <-label-> Target
```
Indented shorthand (source from preceding node):
```
API description: Main gateway
-routes-> UserService
-routes-> ProductService
```
Same-line metadata on edges:
```
A -reads-> DB frequency: High
```
**Group-targeted edges:** Use bracket syntax `[Group Name]` to reference groups as edge endpoints:
```
API -> [Backend]
[Backend] -> [Frontend]
```
Indented shorthand also supports groups. Place the arrow directly after the group header:
```
[Backend]
-> [Frontend]
DB
Cache
```
Top-level group-to-group edges:
```
[Region A] -> [Region B]
[Region A] <-> [Region B]
[Region A] -label-> [Region B]
```
### 13.4 Groups
```
[Group Name]
indented nodes...
[Group Name] key: value
indented nodes...
```
Nested groups (max depth 2):
```
[AWS]
[us-east-1]
API
DB
```
Group metadata cascades to children (node metadata overrides). Nodes already declared above can be referenced inside groups to assign membership.
### 13.5 Group-to-Group Edges
```
[Region A] -> [Region B]
[Region A] <-> [Region B]
[Region A] -VPN-> [Region B]
```
### 13.6 Directives
- `direction-lr` / `direction-tb` (booleans, §1.9; default is LR). The key+value form `direction LR|TB` is accepted legacy.
- `heat [low] [high]` — name a numeric color ramp (see §13.9); one trailing color sets the high (full-value) hue over a neutral low, two set explicit `low high` ramp endpoints
- `no-value` — suppress the per-box numeric value labels (values render by default; a box value is authored data). Legacy `show-values` is accepted as a no-op opt-in from the off-default era.
### 13.7 Node Descriptions
```
// Same-line metadata form
API description: Main gateway for all services
// Multi-line via indented description: lines
UserService
description: Handles user CRUD
description: Manages sessions and auth tokens
```
- `description` is extracted as a dedicated field (not stored in generic metadata)
- Multiple `description` lines accumulate into a multi-line description
- Supports inline markdown: `**bold**`, `*italic*`, `` `code` ``, `[links](url)`
- `- bullet text` renders as `• bullet text`
### 13.8 Options
- `active-tag GroupName` — set active tag group for coloring
- `active-tag none` — suppress tag coloring
- `active-tag ` — make the heat ramp the active dimension (see §13.9)
- `hide team:Backend, team:Frontend` — hide nodes with matching tag values (colon syntax for tag:value)
### 13.9 Heat (numeric color ramp)
Boxes can carry a numeric measure that drives a continuous color ramp — a
choropleth-style "heat dimension" alongside the categorical tag groups.
```
boxes-and-lines Fleet Crews
heat Crew blue
Flagship heat: 120
Frigate heat: 40
Sloop heat: 12
Flagship -> Frigate
Flagship -> Sloop
```
- `heat: ` on any box line records its measure (a reserved metadata
key — lifted out, never rendered as a tag). Non-numeric values are an error.
- `heat [low] [high]` names the dimension and optionally sets the
ramp endpoint colors. Pre-content directive.
- **No color** → high hue is the palette's primary color over a neutral low.
- **One color** → that color is the high (full-value) hue; the low end stays
the neutral floor (unchanged from the single-color form).
- **Two colors** → explicit `low high` endpoints, in that order — a diverging
ramp like `heat Risk green red` reads green (low) → red (high). The
order is taken literally; **which end means "good" vs "bad" is your choice**
(dgmo encodes only "low color = the low-value end"). A wide-hue-gap pair
(e.g. green→red) routes through a neutral midpoint so mid values stay clean
instead of muddy; analogous pairs (e.g. yellow→red) blend directly.
- Up to two trailing color names are peeled from the right and never empty the
label — `heat Red blue` keeps the label `Red` with high `blue`.
- The ramp anchors at the **data minimum** (low end = lowest value), maximising within-diagram contrast; equal-value data renders at the top of the ramp.
- **Active dimension:** the heat ramp is the resting-active dimension whenever
any box has a `heat:` (so heat shading works in static export with no
interaction). `active-tag ` switches to a tag group; `active-tag
none` suppresses all tinting; `active-tag ` forces the heat ramp. On
a name collision between a tag group and the heat label, the tag group wins.
- When the heat ramp is active the legend shows a gradient capsule and each box
is coloured by its value, following the standard box convention — a **25% faded
(muted) fill + a solid colour outline** (and `fill-solid` opts into the full
fill), consistent with tag-coloured boxes. Boxes without a `heat:` get a
neutral fill. The outline differs by ramp kind:
- **Two-colour ramp** (`heat Risk green red`): value is carried by HUE, so
each box's outline is its own ramp colour (green→red) and the fill is the
muted tint of it.
- **Single-colour ramp** (`heat Crew blue`): one hue, so the outline is the
constant ramp hue and value reads from the muted fill's depth (low → near-bg,
high → 25% hue tint).
(Unlike the map, which fills regions with the full saturated ramp colour — a
choropleth — boxes are never solid-filled by default.)
- Each box's number additionally prints as text by default (independent of
which dimension is active); suppress with `no-value` (§13.6).
### 13.10 Tag groups (categorical color)
Boxes carry categorical color via standard tag groups (§1.3). Declare `tag as ` with one color per value — **values indented under the `tag` header**, color as a trailing token — then reference the alias in box metadata (`: `). The ` ` line form is **not** valid grammar (it parses as a content node).
```
boxes-and-lines Service Map
tag Tier as t
Client blue
Edge teal
Compute green
Data purple
[Client]
Web App t: Client
[Edge]
API Gateway t: Edge
[Compute]
Auth Service t: Compute
[Data]
Postgres t: Data
```
Categorical tag color and the numeric heat ramp (§13.9) are separate dimensions; `active-tag` selects which one colors the boxes. Tag declarations must precede content (§1.3).
---
## 15. Timeline Diagrams
> Event, era, and marker dates follow the liberal grammar + year ladder in [§2B Universal Date Handling](#universal-date-handling) (including BCE/CE era-signed years).
### 15.1 Declaration
```
timeline [Title]
```
### 15.2 Events
Events use **date-first syntax** — the date (or date range) leads, followed by the event name, with optional trailing metadata (§1.4).
**Point event** (single date):
```
1718-05 Blockades Charleston
```
**Range event** (`->` between dates, spaces around `->` optional):
```
1716 -> 1717 Sails under Hornigold
1716->1717 Sails under Hornigold
```
**Duration event** (date + name + `duration:` same-line metadata):
```
2026-03-20 Sprint 1 duration: 30d
```
**Datetime** (date with `HH:MM` time component):
```
2026-03-20 14:30 Standup Meeting
```
**Uncertain ending** (`?` suffix on end date or duration value):
```
1718 -> 1719? Rackham builds crew
2026-03-20 Sprint 1 duration: 30d?
```
**Trailing metadata** (§1.4 same-line form, after name):
```
1718-05 Blockades Charleston p: Blackbeard, color: red
1716 -> 1717 Sails under Hornigold p: Blackbeard
```
Event type is determined by positional structure:
- Single date → point event
- `date -> date` → range event
- Single date + `duration:` metadata → duration event
Date formats: any liberal form from [§2B Universal Date Handling](#universal-date-handling) — year, year-month, or day-grain — with an optional `HH:MM` / `HH:MM:SS` time component on day-grain dates.
Duration units: `s` (seconds), `min`, `h`, `d`, `w`, `m`, `y`. Here `s` is **seconds** — not sprints; gantt/pert use `sp`.
**Time:** the time component accepts an optional seconds field (`14:30` or `14:30:45`); hours 0–23, minutes/seconds 0–59 (out-of-range warns). Sub-minute spans get adaptive second-level axis ticks.
**BCE / pre-year-1 dates:** suffix a year with `BCE` (or `BC`) for a pre-Common-Era date — `753 BCE`, `44 BC`. `CE`/`AD` are accepted as positive no-ops (`14 CE`). Markers allow 1–4 digit years (the marker is what distinguishes an ancient year from a stray number — a bare `753` is not a date). Ranges may cross the boundary (`27 BCE -> 14 CE`). Display normalizes to `753 BCE`. Numbering is astronomical-naive (`N BCE` ↔ internal year `-N`), so ordering is correct but there is no year-0 adjustment.
#### Duration syntax (1.0)
`duration:` is the **canonical** (and only) form. A bare trailing duration-like token is **not** parsed as a duration — it is treated as part of the event name (so `2026-03-20 Sprint 1 30d` is a point event named "Sprint 1 30d"). This is intentional: it keeps event names that begin with a duration-like word unambiguous. Use the colon-keyed form to apply a duration:
```
2026-03-20 Sprint 1 duration: 30d
```
Thus `1918 42d Street` is unambiguously a point event named "42d Street"; a duration is only ever expressed via `duration:`.
### 15.3 Eras
**Flat form:**
```
era 1716 -> 1718 Nassau Republic
```
**Block form:**
```
era
1716 -> 1718 Nassau Republic
1718 -> 1720 Woodes Rogers Era orange
```
### 15.4 Markers
**Flat form:**
```
marker 1718-07 Woodes Rogers arrives orange
```
**Block form:**
```
marker
1718-07 Woodes Rogers arrives orange
1720-01 End of Golden Age red
```
### 15.5 Groups
Groups use bracket syntax. Metadata on a group line is inherited by all child events; event-level metadata overrides group metadata. Events must be indented under the `[Group]` header to be associated with it.
```
[Royal Navy] t: British
1718-07 Woodes Rogers arrives
1718-11 Captures Vane color: red
```
In this example, both events inherit `t: British` from the group. The second event also carries `color: red`, which is event-specific.
**Default rendering: swimlanes.** When `[Group]` headers exist, the timeline renders each group as a horizontal swimlane with a labeled header band and a `▼` collapse toggle. Clicking the header toggles between expanded (events visible) and collapsed (single date-range summary bar showing `Group (N events)`). Ungrouped events appear in an implicit `(Other)` lane at the bottom. Use `sort time` to opt out of swimlanes and render events as a flat time-sorted list instead.
### 15.6 Options (Space-Separated, NO Colon)
```
// opt out of swimlanes — flat, time-sorted layout
sort time
// explicit grouped layout (default when [Group] exists)
sort group
// swimlane axis (canonical); equivalent to `sort tag:GroupName`
lane-by GroupName
// back-compat spelling of lane-by
sort tag:GroupName
active-tag CrewName
active-tag none
no-scale
```
**Precedence** (when multiple sort hints apply):
1. `sort tag:X` — tag swimlanes (cross-cutting), takes priority over groups
2. `[Group]` headers present (no `sort` directive) — group swimlanes (default)
3. `sort time` — flat interleaved, ignores groups
### 15.7 Tags
Tag groups follow §1.3. Tag aliases work in trailing metadata:
```
tag Crew as p
Blackbeard red
Bonnet green
1718-05 Blockades Charleston p: Blackbeard
1716 -> 1717 Sails under Hornigold p: Bonnet
```
---
## 16. Data Charts
### Conventions shared across all data charts
Every section under §15 follows the same two rules. Each sub-section may add chart-specific shape, but the rules below are authoritative and apply uniformly.
**Rule A — data-row values are space-separated (canonical); commas are accepted.**
Values within a row are canonically separated by spaces. Commas do not error: thousands grouping inside a number is normalized (`1,000` → 1000), and separator commas between values are tolerated (`Q1 400, 700` parses the same as `Q1 400 700`). An underscore digit-group separator is also accepted (`1_000`). Write new diagrams space-separated, without thousands commas.
> **History:** a 1.0-era rule briefly made commas a hard error (`E_DATA_COMMA_REMOVED`, 2026-06-14); decision #28 deleted the `E_*_REMOVED` family and settled on tolerant acceptance — spaces canonical, commas accepted.
```
Q1 400 700 300 500 ✅ canonical
Q1 400, 700, 300, 500 ✓ accepted — separator commas tolerated
Revenue 1000 ✅ canonical
Revenue 1,000 ✓ accepted — thousands grouping, parses as 1000
Revenue 1_000 ✓ underscore grouping accepted
```
Comma handling above concerns only the **value region** of a data row. Commas in metadata (`Foo t: a, b`), axis labels (`x-label Low, High`), series/column declarations (`series A, B`), quoted strings, and prose were never in question.
**Rule A-bis — labels ending in a number, and surplus values.**
The parser peels trailing numbers off a row as values. On a **single-value** row (no `series`/`stack`/`group` block) the *last* token is the value and everything before it is the label, so `Day 1 8` → label `Day 1`, value `8` — the common axis-label pattern (`Day N`, `Q3 2024`) works as written, no quoting needed. Quote a label only when it is *nothing but* a number, or to be explicit. In **multi-value** mode the series block fixes an exact count, so a surplus of trailing numbers is genuinely ambiguous and **warns**, naming the quoted rewrite (`Armor 50 60 70 80` with 2 series → *"has 4 value(s), but 2 series defined… quote it: `"Armor 50 60" 70 80`"*). This mirrors the treemap/ER leaf-label quoting rule (§24C, §9).
```
Day 1 8 ✅ single-value: label "Day 1", value 8 — no quoting needed
"Region 2" 400 ✅ quoted — label "Region 2", value 400
Armor 50 60 70 80 ⚠ multi-series (2 defined): surplus warns, suggests "Armor 50 60" 70 80
```
**Rule B — list-of-labelled-items directives prefer the indented block form.**
Directives that take a list of labelled items (`series`, `tag`, `categories`, and any similar construct) should use the indented one-per-line form. A comma-separated one-line form is tolerated for very short lists (≤3 items, no colour annotations, no spaces in names) but is not idiomatic.
```
series ✅ preferred
Cloud Platform blue
Legacy Suite red
Mobile App green
Analytics orange
series Cloud blue, Legacy red ⚠ tolerated (back-compat); prefer the block
```
Parsers accept either form everywhere. The rules above are authoring guidance, not parser constraints.
### 15.1 Simple Charts (bar, line, pie, polar-area, radar)
**Declaration:** `bar [Title]`, `line [Title]`, etc.
**Multi-series:**
- **bar** — a layout block header declares the series AND the layout: `stack`
(stacked bars, one bar per category) or `group` (clustered, side-by-side).
`series` is **not** accepted on a bar chart (decision #24).
```
stack // or: group
Cloud Platform blue
Legacy Suite red
```
- **line** — a `series` block (every series is plotted). Follows Rule B
(prefer the indented block); `series A, B` one-line form also parses.
```
series
Cloud Platform blue
Legacy Suite red
```
- **radar** — a `series` block plots one polygon per series over the shared
axes, each in its series color (palette index or the trailing color token),
with the standard series legend. Same `series` block form as line; the
dual-axis grouping (§15.1.1) is line-only. Each data row still carries one
value per series (`Firepower 85 70 95`).
```
series
Black Pearl blue
Flying Dutchman purple
```
**Data rows** — follows Rule A (space-separated, no colons):
```
Label 100
Label 100 200 300
Label color 100 // trailing color before numeric values
Q1 400 700 300 500
```
**Options (space-separated, NO colon):**
```
x-label X Label
y-label Y Label
orientation-horizontal // bar: horizontal bars
fill // line: fill the area under the line
hole // pie: doughnut ring (bare, or a ratio e.g. `hole 0.5`)
no-center-total // pie: hide the center total (shown by default with `hole`)
no-auto-y // line: anchor the y-axis at 0 (opt out of auto-fit)
```
- `orientation-horizontal` (bar; default is vertical bars)
- `fill` (line; fills the area under the line)
- `no-auto-y` (**line only**) — opt out of the data-driven y-axis. By default a
line chart **auto-fits** its y-axis to a padded window around the data
(min→max across **all** series and both axes, ~5% headroom each side, then
`.nice()`-rounded), so a tight cluster of values (e.g. weekly 315→395 lbs)
fills the plot instead of hugging the top of a forced 0→400 scale. `no-auto-y`
restores the **0 baseline** for magnitude honesty. Non-negative data never
auto-fits to a negative floor. **Bar charts always anchor at 0** (bar length
encodes magnitude) and ignore this flag — it is line-only.
- `hole` (pie; renders the doughnut ring — bare `hole`, or `hole <0–0.9>` for a
custom inner-radius ratio. The value total shows in the center by default;
suppress with `no-center-total`)
- Stacked vs. clustered bars are chosen by the `stack` / `group` header above —
there is no separate `stacked` option.
- `title` directive removed — the chart title is line 1 (`bar My Chart`). Using
`title` raises an error diagnostic with a fix-hint pointing at the declaration
line.
- Legend renders by default; suppress with the universal `no-legend` (§1.9)
**Value-display flags — show-everything default.** Every renderable part is on by default on every data chart. Authors suppress with `no-*`. Three flags:
- `no-name` — hide the segment / point / cell / node / set name
- `no-value` — hide the numeric value
- `no-percent` — hide the percentage (share-of-total on the pie family; stage-over-stage conversion on funnel)
Each chart family honors only the subset of flags that has a renderable atom on that chart. Flags that don't apply are silently no-op (typos, future flags, copy-paste from other chart types). Per-chart honoring at runtime:
| Family | Charts | `no-name` | `no-value` | `no-percent` |
| ------ | ------ | --------- | ---------- | ------------ |
| share-of-total | pie, polar-area | yes | yes | yes |
| funnel | funnel | yes | yes | yes (conversion %) |
| cartesian | bar, line, radar | — (axis ticks are separate) | yes | — |
| point-cloud | scatter | yes | — | — |
| density | heatmap | — (axis ticks are separate) | yes | — |
| flow | sankey, arc | name suppression deferred (Phase 2c) | — (Phase 2c) | — |
| spatial | slope, quadrant, venn | name suppression deferred (Phase 2d) | — (Phase 2d) | — |
| single-label | wordcloud | — | — | — |
| functional | function | — | — | — |
There are no `show-*` flags (org's `show-sub-node-count` is the one documented exception, §7.5). A flag exists only to suppress a default-on behavior. To restore a suppressed part, remove the flag.
**Cartesian value labels:** bar and line render values atop each bar / at each data point by default. On horizontal bars (`orientation-horizontal`), values render to the right of each bar. `labelLayout: { hideOverlap: true }` suppresses overlapping labels at high data-point density. Add `no-value` to opt out wholesale.
**Stacked-bar clarification:** on a `stack`-layout bar chart, `no-value` suppresses per-segment values inside each stack — not stack totals. Stack totals are a separate (currently unrendered) concept.
**Eras (line only):**
```
era Day 1 -> Day 3 Rough Seas red
```
#### 15.1.1 Dual y-axes (line)
A line chart can plot series against **two independent y-axes** — one on the left, one on the right — so two metrics with unrelated units and ranges share an x-axis (e.g. oil price in `$/barrel` vs. reserve size in `million barrels`). Each axis auto-scales to the range of the series assigned to it.
Dual axes are expressed by **grouping the `series` block** under axis-label headers. A `series` block whose indented children include a `y-label` and/or `y-right-label` header is in **grouped form**: each header opens an axis group, and the series indented beneath it are assigned to that axis. The header's text is that axis' label.
```
line Oil Price vs Strategic Reserve
series
y-label $ / barrel
Oil Price blue
y-right-label Million barrels
SPR Size green
China Reserves orange
2020 40 640 920
2021 68 620 1010
2022 95 372 1090
```
- **Left = `y-label`, right = `y-right-label`.** Side is keyword-driven, not positional; a group may list any number of series, and the right axis can hold more than one (above, `SPR Size` and `China Reserves`).
- **At most two axes.** A second `y-label` (or second `y-right-label`) header merges into the same side — there is no third axis.
- **Value order is unchanged.** Data rows stay positional across *both* groups in document order: `2020 40 640 920` → Oil=40 (left), SPR=640 (right), China=920 (right). Each row still needs one value per series (§15.1).
- **Single-axis is the flat form.** A flat `series` block (no headers) and a top-level `y-label` option behave exactly as before — single-axis charts are untouched. A grouped block with only a `y-label` (no right group) renders single-axis; the wrapper is unnecessary but harmless.
- **Rendering aids.** Only the left axis draws gridlines (a second grid would not align); an axis owned by exactly one series adopts that series' color (axis line, ticks, name) so each line pairs with its scale at a glance.
Diagnostics:
- `y-right-label` (or a right group) on a non-line chart type → warning; the secondary axis is dropped.
- A `y-right-label` with no series assigned to the right axis → warning (wrap the intended series in a `y-right-label` group).
> **Combo (bar + line on dual axes) is deferred.** A per-series mark token (bars on one axis, a line on the other) is intentionally *not* in 1.0 — see `dgmo-language-spec-decisions.md`. Today every series in a dual-axis chart renders as a line.
> **Removed in 1.0 (decisions #23–#27):** the standalone `area`, `multi-line`, `doughnut`, and `bar-stacked` chart types no longer exist. Use `line` + `fill` (area), `line` + a `series` block (multi-line), `pie` + `hole` (doughnut), and `bar` + a `stack` header (stacked bars; `group` for clustered). Authoring one of the removed type keywords is a hard error.
### 15.2 Scatter / Bubble Charts
**Data rows** — follows §15 Rule A (space-separated, no colon):
```
Name x y
Name x y size
```
**Categories:**
```
[Caribbean] red
Blackbeard 90 8500
```
**Options:**
```
x-label Weight
y-label Height
size-label Crew
no-name
```
Point names render by default. Use `no-name` to hide them.
### 15.3 Heatmap
**Columns** — follows §15 Rule B (prefer the indented block for multiple columns):
```
columns
Jan
Feb
Mar
```
Short one-line form is tolerated: `columns Jan Feb Mar`.
**Data rows** — follows §15 Rule A (space-separated, no colon):
```
RowLabel 5 4 3
```
### 15.4 Function Charts (Colon REQUIRED)
```
function Trajectories
x-label Distance
y-label Height
x 0 to 250
15 degrees blue: -0.001*x^2 + 0.27*x
45 degrees red: -0.003*x^2 + 0.75*x
```
The colon between name and expression is **required** — both sides can contain spaces, so colon is the unambiguous delimiter.
**Options:**
- `fill` (boolean; off by default, shades the area below each curve when enabled — same directive as `line` + `fill`, §14; 25% palette tint, or opaque with `fill-solid`)
### 15.5 Sankey Charts
**Tree structure (indented, space-separated):**
```
Sugar Plantations green
Tortuga Distillery orange 3000
Nassau Distillery 2500
```
**Explicit links:**
```
Source -> Target 3500
Source -- Target 2000
Source -> Target 3500 red // optional trailing-token link color
```
`->` = directed, `--` = undirected. Data values follow §15 Rule A. A trailing color word AFTER the numeric flow value colors the *link* (sankey is the one chart type where edges carry data, so they still accept a color slot).
**Emphasis** (§1.11) — sankey supports the `highlight` / `dim` family:
```
dim Spoilage // the Spoilage node and its ribbons recede
highlight Barrel Aging // the Barrel Aging flow stays lit; everything else recedes
```
`highlight` lights the named node's **flow closure** — everything upstream that feeds it plus everything downstream it feeds — so the path that explains the node stays visible. `dim` recedes exactly the named nodes and no more. A ribbon is receded when *either* endpoint is; the dim multiplies the ribbon's existing translucency rather than replacing it, so a dimmed flow is never more solid than an undimmed one.
### 15.6 Funnel Charts
**Data rows** — follows §15 Rule A (space-separated, no colon):
```
Visits 1200
Signups 800
Purchases 200
```
**Rendering:** each stage's name sits to the left of its band (in the band's color); its value is centered inside the band (falling back beside it when the band is too narrow); a muted stage-over-stage conversion % sits to the right — no leader lines. The first stage carries no % (nothing to convert from). `no-percent` hides the conversion %.
---
## 17. Visualizations
### 16.1 Slope Charts
```
slope Fleet Strength
period 1715 1725
Blackbeard 40 4
Roberts 12 52
```
- Period directive required: `period Label1 Label2` (one-line) or indented block for multi-token labels:
```
period
Before COVID
After COVID
```
- Data rows: `Label value1 value2` — follows §15 Rule A (space-separated canonical; commas accepted — thousands grouping `1,000` → 1000 and separator commas both parse, per #28)
- Underscore grouping `1_000` also accepted
- Color annotations: `Label color value1 value2` (trailing color word before numeric values)
- Minimum 2 periods required
### 16.2 Wordcloud
```
wordcloud Pirate Skills
rotate none
max 50
size 14 80
swordsmanship 95
navigation 88
```
- Data: space-separated only (`word value`)
- Options: `rotate none|mixed|angled`, `max N`, `size min max`
### 16.3 Arc Diagrams
```
arc Pirate Alliances
[Caribbean] red
Blackbeard -> Bonnet 8
Blackbeard -> Vane 5
order group
```
- Link: `Source -> Target [color] weight` — space before the optional trailing color and weight
- Options: `order appearance|name|group|degree`
- `orientation-vertical` (boolean; default horizontal) — draw the spine top-to-bottom with nodes down the axis. Standard boolean form (§4.6), matching `orientation-horizontal` on bars.
- Groups: `[Group Name] color` declares a band; its member links must be **indented** under the header (an un-indented link closes the group). The band is filled with the group's color, and every arc inherits its **source node's** group color — an explicit per-link color still wins, and a link whose endpoints are ungrouped falls back to the palette cycle.
- `layout arc|chord` — layout selector over the same pairwise edges. `arc` (default) draws nodes on a line with connecting arcs; `chord` arranges them around a circle (the circular "chord" layout). There is **no standalone `chord` chart type** — the circular layout is reachable only through `arc` + `layout chord` (decision #29).
```
arc Inter-Department Collaboration
layout chord
Engineering -> Design 85
Design -> Product 68
```
### 16.4 Venn Diagrams
```
venn Skill Overlap
Swordsmanship as sw red
Navigation as nav blue
Leadership as lead green
sw + nav Sea Raiders
sw + nav + lead Legendary Pirates
```
- Set declaration: `Name [as ] [color]` — color is the line-trailing token, AFTER the optional alias (universal modifier order, §2A.2)
- Intersections: `Set1 + Set2 Label` — label follows the last set reference (no colon)
- Legacy `Name(color) alias X` emits `E_VENN_ALIAS_KEYWORD_REMOVED` per TD-18
### 16.5 Quadrant Diagrams
```
quadrant Crew Assessment
x-label Low Skill, High Skill
y-label Low Loyalty, High Loyalty
top-right Promote green
top-left Train yellow
bottom-left Maroon red
bottom-right Watch Closely purple
Quartermaster 0.9 0.95
Navigator 0.85 0.8
```
- Axis labels: `x-label Low, High` — comma-separated (this is a low/high pair, not a data row; comma is the delimiter here by design)
- Position labels: `top-right Label` — space-separated
- Data points: `Label x y` — follows §15 Rule A (space-separated canonical; `Label x, y` also parses — separator commas are tolerated per #28. The axis-label comma above is a true delimiter by design)
---
## 18. Mindmap Diagrams
### 17.1 Declaration
```
mindmap [Title]
```
- First line declares chart type and optional title
- Title doubles as the root node — `mindmap Product Strategy` creates a root node labeled "Product Strategy"
- `mindmap` alone (no title) enables multi-root mode; title is inferred from the first root
### 17.2 Nodes
Nodes are defined by indentation:
```
mindmap Root
Level 1a
Level 2a
Level 2b
Level 1b
```
- Indentation establishes parent-child hierarchy
- When title is present, indent-0 lines are children of the title root
- When title is absent, indent-0 lines are independent roots (multi-root)
- Node coloring: use tags (§2)
### 17.3 Same-Line Metadata
```
Node priority: High, status: Done
Node description: Short note
Node collapsed: true
```
- Standard same-line metadata syntax (colons required)
- `description` is extracted as a dedicated field (not generic metadata)
- `collapsed: true` sets default collapsed view state for that subtree
### 17.4 Description Field
Two forms:
```
// Same-line form
Surveys description: Quarterly NPS survey
// Indented form (colon required; bare `description OAuth …` is REMOVED → error)
Auth System
description: Handle login, signup, and OAuth flows
description: OAuth includes Google and GitHub
Login Page
```
- `description: text` (colon required; the bare `description text` form is removed at 1.0 and emits `E_DESCRIPTION_BARE_REMOVED`, an error)
- Both same-line and indented forms set the same `description` field
- Same-line form takes precedence if both are specified
- Multiple `description` lines accumulate into a multi-line description
- Indented `description` must appear before child nodes (warning if after)
- Empty `description:` (no value) is silently skipped
- Bare `Description` (no trailing text) is treated as a child node name, not a description
- Only `description` / `description:` is recognized as indented metadata — all other indented lines are child nodes (e.g., `role: Engineer` becomes a child node, not metadata)
- Supports inline markdown: `**bold**`, `*italic*`, `` `code` ``, `[links](url)`
- `- bullet text` renders as `• bullet text`
### 17.5 Multi-Root
```
mindmap
Q1 Goals
Ship MVP
Q2 Goals
Launch marketing
```
- No title on first line enables multi-root mode
- Each indent-0 line starts a new root tree
- Title is inferred from first root's label
### 17.6 Tags
Standard tag declarations (§1.3):
```
mindmap Root
tag Priority as p
High red
Low green
Task p: High
```
**Parent → child cascade.** A tag value set on a node **cascades to its
descendants**: a sub-node with no value of its own for that group inherits the
value of its nearest tagged ancestor. A sub-node's own value always overrides
the inherited one and re-roots the cascade for its own subtree. This lets you
tag a category once and have the whole branch pick up the color:
```
mindmap Wine Varieties
tag Style as s
Red red
White white
Sparkling yellow
Whites s: White
Chardonnay // inherits White
Sauvignon Blanc // inherits White
Loire s: Sparkling // overrides → Sparkling (and its subtree)
```
Cascade only flows from an **explicitly tagged** ancestor. A node with no
tagged ancestor falls back to the group's global default (the first declared
value) — it does not inherit across an untagged parent.
### 17.7 Options
| Option | Effect |
|--------|--------|
| `active-tag GroupName` | Set default active tag group |
| `color-by-depth` | Bare flag; colour nodes by depth instead of by tag (off by default). View-state directive written by the app's Depth Colors toggle |
Options are bare keywords or `key value` on their own line, before content.
### 17.8 Collapse/Expand
- Nodes with children can be collapsed/expanded in the app
- `collapsed: true` in same-line metadata sets the default collapsed state
- Collapsed nodes show a drill-bar (accent bar at bottom)
- Collapse is **portable view-state** (§1) — the app writes/removes the marker on toggle and any renderer reproduces it from source
### 17.9 Full Example
```
mindmap Product Strategy
tag Priority as p
High red
Medium yellow
Low green
tag Department as d
Engineering blue
Design purple
Marketing orange
Research d: Marketing
User Interviews p: High
Surveys description: Quarterly NPS survey
Focus Groups
Competitor Analysis d: Engineering
Feature Matrix
Pricing Review
Development p: High, d: Engineering
MVP Features
Auth System
description: Handle login, signup, OAuth flows
Dashboard
Nice-to-haves p: Low, collapsed: true
Dark Mode
Export PDF
Go-to-Market d: Marketing
Launch Plan
Blog Post
Demo Video description: 2-min product walkthrough
```
---
## 19. Wireframe Diagrams
### 18.1 Declaration
```
wireframe [Title]
```
- First line declares chart type and optional title
- `wireframe Login Page` creates a wireframe titled "Login Page"
### 18.2 Form Factor
```
mobile
```
- `mobile` keyword switches to a narrow vertical layout (375px viewport)
- Default (no keyword) is desktop layout (1200px viewport)
- Must appear before diagram content
### 18.3 Visual-Mnemonic Element Syntax
Elements use bracket shapes that visually suggest their rendered control:
| Syntax | Element | Notes |
|--------|---------|-------|
| `[text]` | Text input (leaf) or group (if has indented children) | See §18.8 for disambiguation |
| `(Button)` | Button | |
| `{Option A \| Option B}` | Dropdown / select | Options separated by `\|` |
| `` | Checkbox (checked) | |
| `< >` | Checkbox (unchecked) | |
| `(*) Label` | Radio button (selected) | |
| `( ) Label` | Radio button (unselected) | |
| `# Heading` | Heading | |
| `## Subheading` | Subheading | |
| `---` | Divider | Horizontal rule |
| `- item` | List item | |
| Bare text | Text / paragraph | Any line not matching other syntax |
### 18.4 Keyword Elements
Nine special keyword elements render common UI patterns:
| Keyword | Description | Hints / Options |
|---------|-------------|-----------------|
| `nav` | Navigation bar | |
| `tabs` | Tab bar | |
| `table` | Data table | `table RxC` for skeleton shorthand (e.g. `table 5x3`) |
| `image` | Image placeholder | `round`, `wide` |
| `modal` | Modal dialog | Has indented children for modal content |
| `skeleton` | Loading skeleton | |
| `alert` | Alert / notification | |
| `progress` | Progress bar | Value 0–100 (e.g. `progress 65`) |
| `chart` | Chart placeholder | `line`, `bar`, `pie` |
**Table explicit rows**: comma-separated values, first row is the header.
```
table
Name, Email, Role
Alice, alice@co.com, Admin
Bob, bob@co.com, User
```
**Image and chart hints** appear after the keyword:
```
image round
image wide
chart line
chart bar
chart pie
```
### 18.5 Flags (Trailing-Keyword List)
```
(Submit) primary
[Email] disabled
(Delete) destructive ghost
```
Wireframe flags are a **trailing-keyword list** in the same slot occupied by the §1.5 trailing-token color rule on other chart types: any whitespace-delimited lowercase tokens at the end of an element line, drawn from the closed 16-keyword flag enum below, are extracted as flags. Multiple flags are space-separated (NOT comma-separated). Flag extraction stops at the first token that is not in the enum — everything to the left is the element's literal label.
```
(Submit) primary // label = "Submit", flags = [primary]
(Submit) primary destructive // label = "Submit", flags = [primary, destructive]
(Save Changes) primary // label = "Save Changes", flags = [primary]
```
**Available flags (closed enum — 16 keywords):**
| Flag | Effect |
|------|--------|
| `disabled` | Greyed-out, non-interactive appearance |
| `active` | Active / current state |
| `selected` | Selected state |
| `empty` | Empty / placeholder state |
| `ghost` | Ghost / outline-only button style |
| `destructive` | Destructive action (red-tinted) |
| `success` | Success state (green-tinted) |
| `warning` | Warning state (yellow-tinted) |
| `info` | Info state (blue-tinted) |
| `scrollable` | Scrollable container |
| `collapsed` | Collapsed container |
| `toggle` | Toggle switch appearance |
| `password` | Password input (masked) |
| `textarea` | Multi-line text input |
| `horizontal` | Horizontal layout for group children |
| `primary` | Primary / emphasized button style |
#### Indented fallback
For elements where the trailing-keyword form is ambiguous or visually noisy (long flag lists, labels that need to clearly read as labels), drop the trailing tokens and indent the flags one level deeper. Both forms produce the same parsed element.
```
(Submit) primary destructive // canonical trailing-keyword form
(Submit) // indented fallback — same semantics
primary
destructive
```
Indented flag lines under a wireframe element follow the same reserved-key-registry dispatch (§1.4.2): the parser recognizes lines whose first token is in the 16-keyword flag enum and attaches them as flags. Indented lines that don't match flag keywords fall through to child-element grammar.
#### Label-vs-flag collision
When an element's literal label happens to lexically match a flag keyword (e.g. a button literally named "Primary"), authors have two escape hatches:
1. **§1.5 capitalization rule.** Flag extraction is case-sensitive lowercase — `(Primary)` is a button labeled "Primary" with no flags; `(primary)` is the lowercase form and gets matched against the flag enum (parsed as a label with no flags too, because the lone `primary` is the label, not a trailing token).
2. **Indented form.** Drop the trailing tokens and indent flags — unambiguous regardless of label content.
```
(Primary) // label = "Primary", no flags
(Primary) primary // label = "Primary", flags = [primary]
(Primary) // indented fallback for clarity
primary
```
**Authoring guidance:** capitalize wireframe element labels (`(Submit)`, `(Cancel)`, `(Delete)` rather than `(submit)`, `(cancel)`, `(delete)`) to match real-world UI button conventions and to keep the lowercase flag enum from colliding lexically with labels. Lowercase labels remain valid but produce visually confusing source where label and flag share the same token shape.
### 18.6 Multi-Element Lines
Two or more elements on the same line, separated by 2+ spaces:
```
[First Name] [Last Name]
(Cancel) (Submit) primary
```
- **2 segments with bare text + element** = label-field pairing:
```
Email [user@example.com]
```
- **3+ segments** = inline items laid out horizontally:
```
(Back) (Next) (Skip)
```
### 18.7 Layout Model
- **Desktop** (default): 1200px viewport, horizontal regions. Region names like "sidebar" get smart sizing (~25% width); "main" or "content" fills remaining space.
- **Mobile**: 375px viewport, all regions stack vertically.
Regions are defined by top-level groups:
```
wireframe Dashboard
[Sidebar]
nav
- Overview
- Settings
[Main]
# Dashboard
chart line
```
### 18.8 Group Disambiguation
The `[text]` syntax is ambiguous — it renders as a **text input** when it has no children, or a **group/container** when it has indented children:
```
[Search] // text input (no children = leaf)
[Search Form] // group (has children below)
[Query]
(Search)
```
Group-only flags (`horizontal`, `scrollable`, `collapsed`) force group interpretation even without children:
```
[Empty Panel] collapsed // group (forced by flag)
```
### 18.9 Full Example
```
wireframe E-Commerce Product Page
[Header]
nav
[Search] empty
(Cart) (Account)
[Breadcrumb] horizontal
- Electronics
- Phones
- Galaxy S25
[Main Content] horizontal
[Product Images]
image wide
image image image
[Product Details]
# Galaxy S25
## $899.99
{Black | White | Blue}
Quantity [1]
(Add to Cart) primary
(Buy Now) ghost
---
## Description
The latest flagship phone with advanced AI features,
a stunning display, and all-day battery life.
---
## Specifications
table
Feature, Detail
Display, 6.2" AMOLED
Processor, Snapdragon 8 Gen 4
RAM, 12GB
Storage, 256GB
[Reviews]
# Customer Reviews
progress 85
table 3x4
[Footer]
- About
- Contact
- Privacy Policy
- Terms of Service
```
---
## 20. Tech Radar Diagrams
Tech radars visualize technology adoption decisions across an organization, inspired by the ThoughtWorks Technology Radar format.
### Declaration
```
tech-radar Title
```
### Rings Block
Ring names are declared in a `rings` block, one per indented line. Order is innermost (first) to outermost (last). Variable count supported.
```
rings
Adopt
Trial
Assess
Hold
```
### Quadrant Sections
Exactly 4 quadrants required, each as a top-level section header with same-line metadata specifying position:
```
Techniques quadrant: top-right
Tools quadrant: top-left
Platforms quadrant: bottom-left
Languages & Frameworks quadrant: bottom-right
```
**Positions:** `top-left`, `top-right`, `bottom-left`, `bottom-right`. Each position must be used exactly once.
**Optional color override:** `Tools quadrant: top-left, color: purple`
**Default colors by position:** top-left=blue, top-right=green, bottom-left=red, bottom-right=orange.
### Blips
Blips are indented under their quadrant. Each blip requires `ring` metadata matching a declared ring name. Optional `trend` metadata specifies movement direction.
```
Continuous Deployment ring: Adopt, trend: stable
Micro Frontends ring: Trial, trend: up
```
**`ring`** (required): Must match a declared ring name exactly (case-insensitive).
**`trend`** (optional): `new`, `up`, `down`, `stable`. No indicator rendered if omitted.
### Descriptions
Further-indented lines below a blip are collected as description text. Supports simple markdown (bold, italic, inline code, links).
```
Rust ring: Assess, trend: new
Evaluating for **performance-critical** services.
See [evaluation doc](https://wiki.example.com/rust).
```
### Global Numbering
Blips receive sequential global numbers for unambiguous reference. Order: quadrants clockwise (top-left, top-right, bottom-right, bottom-left), then by ring (innermost first), then by declaration order.
### Directives
Top-level bare keywords that toggle rendering behavior. Place after the chart-type declaration, before the `rings` block.
| Directive | Description |
|-----------|-------------|
| `no-blip-legend` | Suppress the four-column blip listing. The listing renders **by default on every surface** (CLI export and live/interactive alike; the old surface-dependent default is gone — decision #48). Legacy `show-blip-legend` accepted as a no-op. |
### Complete Example
```
tech-radar Engineering Tech Radar Q2 2026
rings
Adopt
Trial
Assess
Hold
Techniques quadrant: top-right
Continuous Deployment ring: Adopt, trend: stable
Fully adopted across all services.
Infrastructure as Code ring: Adopt, trend: stable
Micro Frontends ring: Trial, trend: up
Tools quadrant: top-left
GitHub Copilot ring: Trial, trend: new
Vite ring: Adopt, trend: up
Webpack ring: Hold, trend: down
Migrating to Vite across all projects.
Platforms quadrant: bottom-left
Kubernetes ring: Adopt, trend: stable
Cloudflare Workers ring: Trial, trend: up
Languages & Frameworks quadrant: bottom-right
TypeScript ring: Adopt, trend: stable
Rust ring: Assess, trend: new
Evaluating for performance-critical services.
React ring: Adopt, trend: stable
```
---
## 21. Cycle Diagrams
Cycle diagrams show a circular process flow where nodes are arranged on a circle and directed edges connect each node to the next, with the last node wrapping back to the first. Common use: OODA loops, PDCA, product lifecycles, continuous improvement.
**Render category:** `visualization`
### 20.1 Chart Type Declaration
```
cycle [Title]
```
### 20.2 Nodes
Top-level (non-indented) lines declare nodes. Nodes are positioned on a circle in declaration order.
```
NodeLabel
NodeLabel color: blue, span: 3, description: inline text
```
#### Node Same-Line Metadata
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `color` | palette name | first palette color (uniform) | Node fill color |
| `span` | positive number | `1` | Relative arc distance to next node. Decimals allowed (e.g., `1.5`). Zero or negative → parse error |
| `description` | string | — | One-liner description (alternative to indented text) |
### 20.3 Node Descriptions
Indented lines under a node are descriptions. Markdown inline formatting is supported: `**bold**`, `*italic*`, `` `code` ``, `[links](url)`. Bullet points: `- item` renders as `• item`.
```
Observe blue
Gather raw information from the environment
Monitor unfolding circumstances
```
If both `description: text` (same-line) and indented lines exist, they concatenate (same-line first, then indented).
### 20.4 Edges
Edges are **implicit** — every node connects to the next in declaration order, with the last wrapping to the first. The `->` syntax annotates edges with labels, descriptions, and styling; omitting `->` lines just means unstyled edges.
```
-Label-> color: red, width: 6
->
-> width: 4
```
Indented under a node, before or after description lines. Explicit targets after `->` (e.g., `-> Orient`) are accepted but silently ignored — cycle edges always connect to the next node. If the explicit target doesn't match the actual next node, an info diagnostic is emitted.
#### Edge Same-Line Metadata
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `color` | palette name | inherits source node color | Edge stroke color |
| `width` | number (px) | `3` | Edge stroke width in pixels (values below the 2 px floor are clamped up) |
#### Edge Descriptions
Indented lines under an edge line are edge descriptions:
```
Observe blue
-Unfold circumstances->
Synthesize raw data into actionable context
Identify **key patterns** and anomalies
```
### 20.5 Directives
| Directive | Description |
|-----------|-------------|
| `direction-counterclockwise` | Reverse cycle direction (default: clockwise) |
| `circle-nodes` | Render nodes as circles instead of rounded rectangles (uniform diameter) |
### 20.6 Parsing Constraints
- Node labels **cannot contain `->` or `<-`** — parse error with helpful message
- Lines starting with `-` followed by text and `->` are edges; bare `-` followed by non-arrow text is a description (bullet point)
- Minimum 2 nodes required — parse error on fewer
- `span` must be a positive number — parse error on zero or negative
### 20.7 Complete Example
```
cycle OODA Loop
Observe blue
Gather raw information from the environment
Monitor unfolding circumstances
-Unfold circumstances-> color: blue
Synthesize raw data into actionable context
Identify **key patterns** and anomalies
Orient green
Analyze and synthesize observations
Form a mental model of the situation
-Form hypothesis-> color: green
Decide orange
Select a course of action
-Commit to action-> color: orange
Act red
Execute the chosen course of action
Generate results that feed back
-Generate feedback-> color: red
Results flow back into observation
```
---
## 22. Journey Map Diagrams
### Chart Type Declaration
```
journey-map [Title]
```
The `journey-map` keyword **must** appear on the first line. No inference — explicit declaration required (avoids conflict with kanban's identical `[Column]` + indented items pattern).
### Persona
```
persona Name
Optional indented description text
```
Declares the journey's protagonist. Name is everything after `persona ` (rest-of-line). Bare `persona` with no name is an error. Only one persona per diagram. A §1.5 trailing-token color sets the card color (`persona Nadia green`); capitalize the word (`persona Nadia Green`) to keep it as literal name text, or use the `color:` long form (`persona Nadia color: green`). The indented description supports the same rich text as notes: bullet lines (`- ` / `* ` → a hanging "•") and inline markdown (`**bold**`, `*italic*`).
### Phases
```
[Phase Name]
Step Name score: 4
```
Phases are declared with bracket syntax at indent 0. Steps are indented within phases. Phases are optional — steps can appear at root level for a flat continuous flow.
### Steps
Steps appear indented within phases (or at indent 0 in flat mode).
**Score syntax** — score and emotion are explicit keys:
```
// score only
Step Name score: 4
// score + emotion label (single word)
Step Name score: 4, emotion: Delighted
// score + tag metadata
Step Name score: 4, ch: Web
// score + label + metadata
Step Name score: 4, emotion: Delighted, ch: Web
// no metadata = scoreless step
Step Name
```
- **Score scale**: 1–5 integer, high = good. Floats, negatives, out-of-range values produce parse errors.
- **Emotion label**: optional single word (e.g., `Frustrated`, `Delighted`, `Neutral`). Multi-word labels are rejected.
- **Scoreless steps**: render as cards but produce no emotion curve point. A diagnostic hint is emitted.
- **Legacy bare-score (`Step | 4 Delighted`) is removed.** A line of shape ` | ` (or `| `) emits `E_JOURNEY_BARE_SCORE_REMOVED` with hint to use `score: , emotion: `.
### Annotations
Annotations appear as deeper-indented lines under a step:
```
Step Name score: 2
pain: Wants guest checkout
opportunity: Add setup guide
thought: Excited to try it
description: Additional context
```
- `pain:` — pain point (rendered in red)
- `opportunity:` — improvement opportunity (rendered in green)
- `thought:` — user thought (rendered in italic)
- `description:` — general context text
Multiple annotations per step are allowed.
### Tag Blocks
Standard `tag` blocks with aliases and colored entries:
```
tag Channel as ch
Web blue
Mobile purple
In-Person green
```
Tag values referenced in step metadata via alias: `Step score: 4, ch: Web`
### Directives
| Directive | Effect |
|-----------|--------|
| `active-tag GroupName` | Set the active tag group for coloring |
### Visual Rendering
- **Emotion curve**: filled area chart (mood landscape) with gradient — green above midline (3), red below. The curve is the hero visual element.
- **Grid lines**: subtle horizontal lines at score levels 1–5 behind the curve.
- **Step cards**: color-intensity mapped to score (1=red → 5=green, palette-aware).
- **Phase headers**: color auto-derived from average step score within the phase.
- **Sharp-drop zones**: auto-accent where score drops by ≥ 2 between consecutive scored steps.
- **Score legend**: auto-generated.
### Flat Mode
Steps at root level (no `[Phase]` brackets) render as a continuous horizontal strip with emotion curve:
```dgmo
journey-map Quick Feedback
Opened app score: 4
Searched for feature score: 3
Hit error score: 1, emotion: Frustrated
pain: No helpful error message
Contacted support score: 2
Got resolution score: 5, emotion: Relieved
```
### Full Example
```dgmo
journey-map Buying a Laptop
persona Tech-Savvy Shopper
28yo developer, price-sensitive, does extensive research
tag Channel as ch
Web blue
Mobile purple
Email teal
In-Person green
[Research]
Compare specs score: 4, ch: Web
description: Checked 12 laptops across 4 review sites
Watch reviews score: 5, emotion: Engaged, ch: Mobile
Ask friends score: 4, ch: In-Person
[Purchase]
Add to cart score: 3, ch: Web
Forced account creation score: 1, emotion: Frustrated, ch: Web
pain: Wants guest checkout
pain: Password requirements too strict
Complete payment score: 3, ch: Web
[Delivery]
Track package score: 4, ch: Mobile
Unboxing score: 5, emotion: Delighted, ch: In-Person
opportunity: Include setup guide
thought: Excited to try it out
```
### Minimal Example
```dgmo
journey-map Coffee Shop Visit
[Order]
Walk in score: 4
Wait in line score: 2
Order drink score: 4
[Wait]
Find a seat score: 3
Wait for drink score: 2
[Enjoy]
Get drink score: 5
Drink coffee score: 5
```
---
## 23. Pyramid Diagrams
Hierarchical pyramid visualization with stacked layers, descriptions, and optional per-layer color. Source order reads apex-first (top of file = top of pyramid).
### 23.1 Chart Type Declaration
```
pyramid [Title]
```
### 23.2 Layers
One layer per non-indented line. Minimum 2 layers; maximum 15.
```
pyramid Maslow's Hierarchy
Self-Actualization
Esteem
Love & Belonging
Safety
Physiological
```
### 23.3 Layer Descriptions
Two forms, interchangeable:
**Same-line metadata:** `description:` key after the layer name, with values quoted when they contain commas.
```
Esteem description: "Respect, recognition, confidence"
```
**Indented body (multi-line):** indented lines below the layer become the description. Lines wrap naturally at render time.
```
Self-Actualization
Achieving one's full potential across
personal growth and creativity.
```
Indented `- item` lines are rendered as `• item`.
**Legacy bare-description shorthand (`Layer | Respect, recognition`) is removed.** A line of shape ` | ` (no `key:` prefix) emits `E_PYRAMID_BARE_DESCRIPTION_REMOVED` with hint to use `description: ` (quoted when the value contains commas) or the indented body form.
### 23.4 Metadata Keys
Keys are comma-separated, per §1.4.1.
| Key | Value | Example |
|-----|-------|---------|
| `color` | palette color name | `Safety color: blue, description: "Security, health"` |
| `description` | description (quote when containing commas) | `Safety description: "Security, health"` |
When color is the only thing you need to set, use the trailing-token form (`Safety blue`). The `color: ` long form is reserved for cases where color rides alongside another key.
```
Love & Belonging orange
Friendship, intimacy, family.
```
### 23.5 Directives
| Directive | Effect |
|-----------|--------|
| `inverted` | Flip the pyramid so the apex points down (funnel orientation). Source order is always top-to-bottom visually — the first layer in the file is the widest (visual top); the last layer is the apex (visual bottom). |
### 23.6 Rendering
- **Default colors:** each layer's fill is a ramp from `palette.primary` mixed with `palette.bg` — apex is the palest, base is full primary (the one documented exception to §1.3's canonical rotation — decision #48). Per-layer `color:` overrides that single layer.
- **Pitch:** base-to-height ratio is classical (roughly equilateral); layers are equal height.
- **Labels:** centered horizontally on the layer midline. Font size scales with layer height, clamped 12–20 px. Apex labels may extend beyond the triangle when the polygon is narrower than the text — this is intentional.
- **Descriptions:** rendered to the right of each layer, left-aligned, muted color, vertically centered on the layer midline. Scales cleanly regardless of apex width.
### 23.7 Complete Example
```dgmo
pyramid Maslow's Hierarchy of Needs
Self-Actualization
Morality, creativity, acceptance of facts,
meaning, inner potential.
Esteem description: "Respect, recognition, confidence"
Love & Belonging orange
Friendship, intimacy, family, a sense
of connection.
Safety description: "Security, employment, health, property"
Physiological description: "Food, water, warmth, rest"
```
### 23.8 Inverted Example
```dgmo
pyramid Funnel of Learning
inverted
Hear about it description: Passive awareness
Read about it description: Shallow familiarity
Try it once description: First-hand attempt
Practice it description: Building fluency
Teach it description: Deep mastery
```
---
## 24. Ring Diagrams
Concentric-ring visualization for nested or hierarchical categories — circles of influence, OSI-style layered models, organizational maturity rings. Source order reads core-out: top of file = innermost element (rendered as a filled disc), last line = outermost ring.
### 24.1 Chart Type Declaration
```
ring [Title]
```
### 24.2 Layers
One layer per non-indented line. Minimum 2 layers; maximum 15.
```
ring Captain's Sphere of Influence
Captain
Quartermaster
Crew
Allied Crews
The Open Sea
```
### 24.3 Layer Descriptions
Two forms, interchangeable with pyramid:
**Same-line metadata:** `description:` key after the layer name, with values quoted when they contain commas.
```
Quartermaster description: "Second-in-command, divvies the booty"
```
**Indented body (multi-line):** indented lines below the layer become the description. Lines wrap at render time.
```
Captain
Final word on heading and plunder,
keeper of the ship's charter.
```
Indented `- item` lines render as `• item`.
**Legacy bare-description shorthand (`Layer | text`) is removed.** A line of shape ` | ` (no `key:` prefix) emits `E_RING_BARE_DESCRIPTION_REMOVED` with hint to use `description: ` or the indented body form.
### 24.4 Metadata Keys
Keys are comma-separated, per §1.4.1.
| Key | Value | Example |
|-----|-------|---------|
| `color` | palette color name | `Crew color: blue, description: Deckhands and gunners` |
| `description` | description (quote when containing commas) | `Crew description: "Deckhands, gunners"` |
When color is the only thing you need to set, use the trailing-token form (`Crew blue`). The `color: ` long form is reserved for cases where color rides alongside another key.
```
Crew orange
Deckhands, gunners, and powder monkeys.
```
Unknown color names emit an error-severity diagnostic with a "Did you mean…?" hint, and the ring falls back to its series color so the chart still renders.
### 24.5 Directives
| Directive | Effect |
|-----------|--------|
| `fill-solid` / `fill-outline` | Render rings with full intent color, or outline-only, instead of the default 25% tint (§1.9 fill family). |
`inverted` is **not** valid on ring diagrams (rings are rotationally symmetric — visual inversion is meaningless). Using it emits an error-severity diagnostic and the line is discarded so it does not become a layer named "inverted".
### 24.6 Rendering
- **Concentric circles drawn core-out.** Each ring band has equal radial thickness (`outerRadius / N`). The innermost element is a filled disc, not an annulus.
- **Default colors:** layers cycle through the canonical categorical rotation (§1.3): red, orange, yellow, green, blue, purple, teal, cyan — reused when there are more than 8 layers.
- **Contrast stroke:** a 1px stroke at every ring's outer edge keeps adjacent tints from blurring together. Renderer-only; no syntax surface.
- **Labels:** centered horizontally on the diagram axis. The innermost label sits at the center of the disc; outer-ring labels stack vertically along the band's midline above center. Font size scales with band thickness, clamped 12–20 px.
- **Side description list:** rendered to the right of the diagram, top-to-bottom in source order. Each entry has a colored accent bar matching its ring. When no layer carries a description, the list is omitted and the diagram expands to fill the canvas.
### 24.7 Complete Example
```dgmo
ring Captain's Sphere of Influence
fill-solid
Captain purple
Final word on heading and plunder,
keeper of the ship's charter.
Quartermaster blue
Second-in-command, arbitrates
disputes, divvies the booty.
Crew green
Deckhands, gunners, and powder
monkeys aboard the ship.
Allied Crews orange
Loose alliances kept by oath
or shared bounty.
The Open Sea cyan
Weather, currents, and rival
flags beyond reach.
```
---
## 24C. Treemap Diagrams
Nested rectangles sized by value — the canonical way to show a hierarchy's proportions at a glance (budgets, disk usage, portfolios, taxonomies). Built on a squarified layout so cells keep good aspect ratios.
### 24C.1 Chart Type Declaration
```
treemap [Title with units, e.g. "Cloud Spend ($)"]
```
Title is free text (punctuation allowed) and optional. **Units go in the title** (`Cloud Spend ($)`) — there is no format or currency directive, matching `region-heat Sales ($M)` (§24B).
### 24C.2 Hierarchy & Value
- **Indentation sets the hierarchy** (the mindmap/org model, §18).
- A **bare trailing number on a leaf is its size** (`Platform 320`) — the funnel/sankey trailing-number idiom (§1.5). No thousands commas; `1_000` is fine.
- **Parents auto-sum** their descendants. A trailing number on a *branch* line (a line with indented children) is **ignored** with `W_TREEMAP_BRANCH_VALUE_IGNORED` — the auto-sum always wins, never silently mixed.
- A **leaf with no trailing number** gets value 0 + `W_TREEMAP_LEAF_NO_VALUE` (renders zero-area).
- A **negative leaf value** is an error: `E_TREEMAP_NEGATIVE_VALUE`.
**Value vs. a label ending in a digit (disambiguation):** the trailing whitespace-delimited numeric token is peeled as the value, so unquoted `Region 5` parses as label `Region`, value `5`. To keep a label that genuinely ends in a digit, **quote it**: `"Region 5"` → label `Region 5`, no value.
```
treemap Q3 Budget
Engineering
Platform 320
Mobile 180
Operations
Cloud 110
```
### 24C.3 Tags (color by category)
Declare a `tag` group before content (§1.3) and apply it inline; children inherit a tagged ancestor's value unless they override it.
```
tag Team as t
Eng blue
Sales green
Engineering t: Eng
Platform 320
```
### 24C.4 Heat (color by value)
A per-node `heat:` metric (a second numeric channel, distinct from size; negatives/floats ok) drives a value ramp. It is named with a `heat [low] [high]` directive (one word, mirroring the `heat:` key). **`heat:` is used instead of `score:`** — journey-map reserves `score` for its 1–5 emotion scale (§22). This directive↔key mirror is the **reference pattern** for every value-driven ramp: the directive and key share the channel word — `heat` (colour), `size` (marker area), `width` (stroke) — across treemap, boxes-and-lines (§13.9), and map (§24B.3/.5/.6); see decision-log #20.
```
heat Day change %
AAPL 180 heat: -2.4
NVDA 220 heat: 4.1
```
**Ramp colors are optional and data-aware:**
| Input | Ramp |
|-------|------|
| no colors, data crosses 0 | diverging `red · neutral · green`, midpoint pinned at **0** |
| no colors, one-sign data | sequential `neutral · accent` (palette primary) |
| one color `c` | `neutral → c` (map's one-color rule, §24B) |
| two colors `lo hi` | `lo → high` with a neutral midpoint |
**Named palette colors only — hex is rejected** (an unrecognized token is left as label text, never a color).
### 24C.5 Node Metadata
| Key | Value | Example |
|-----|-------|---------|
| `heat` | number (signed/float) | `AAPL 180 heat: -2.4` |
| `` | a declared tag value | `Engineering t: Eng` |
`value` is **not** a metadata key — size is the bare trailing number.
### 24C.6 Directives
| Directive | Effect |
|-----------|--------|
| `heat [low] [high]` | Name (and optionally color) the value ramp; pairs with the `heat:` key. |
| `active-tag ` | Source-level override of the resting colouring dimension — same semantics as map §24B.4: a tag group name selects categorical fill; the heat ramp's label (`Value` if unnamed — the legend name) re-selects the ramp. `active-tag none` forces branch mode. The runtime app switcher remains (§24C.7) — the directive makes the choice portable. |
| `depth N` | Render N levels; deeper subtrees collapse to a drillable solid block. A **render budget**, not a data limit (the full tree is always drillable; static export renders it whole). |
| `no-value` | Hide value labels (plural `no-values` accepted as silent alias). |
| `no-percent` | Hide percentage labels. |
| `no-headers` | Hide parent header bars. |
| `no-legend` | Hide the legend. |
Everything is **on by default**; subtract with `no-*`. Numbers auto-compact (1.2M, 940k).
### 24C.7 Color Mode Selection
The source-declared default mode is resolved **heat → tag → branch**: any `heat:` present → the heat ramp colours cells at rest; else tags declared → the first tag group; else `branch` (each top-level branch a distinct hue, tinted with depth). This is the universal precedence shared with map (§24B.4) and boxes-and-lines — decision #48. The `active-tag` directive (§24C.6) overrides the resting dimension from source. The desktop app adds a runtime switcher (heat/tag/branch) in the legend/settings pull-tab — **live preview only, it does not edit source** (same contract as the map's `active-tag` legend click).
### 24C.8 Depth Collapse
A depth-collapsed node keeps its full summed value (area unchanged); only its descendants are not drawn. Drilling re-applies the depth window from the new root.
> **Removed:** `other-below N` (small-leaf rollup into an "Other" bucket) was dropped — small children render as their own cells. The directive is still recognized but ignored with a warning so older sources don't break.
### 24C.9 Interactivity vs. Export
In the desktop app a treemap is interactive: click a parent to drill in (with a breadcrumb), hover for a tooltip (path / value / % of parent / % of root / heat), legend hover-dim, and the color-mode switcher. Static SVG / PNG export is the **clean, full tree** with none of that chrome (focus icons carry `data-export-ignore`) — the same interactive-vs-export split the map chart uses.
### 24C.10 Radial Mode (Sunburst / Hierarchical Pie)
A bare `radial` flag renders the same parsed hierarchy as a **sunburst** — a hierarchical, multi-ring pie — instead of nested rectangles. Same data model, polar geometry.
```dgmo
treemap Plunder Spend ($k)
radial
Sailing & Rigging
Rigging 320
Helm 180
Cannon Battery
Powder 90
Shot 130
```
- **Center disc** = the synthetic root: the **chart title + grand total** (never a node label; this is what makes multi-root sources render correctly). The top-level authored nodes form the first ring outward; descendants fill successive rings (one ring per depth).
- **Angle encodes value:** an arc's sweep = `node.value / grand-total × 2π`; each child arc nests inside its parent's wedge. **Radius encodes depth only — no magnitude.**
- **Source order, not value-sorted** (deliberately unlike the rectangular squarified layout, §24C.2), laid out clockwise from 12 o'clock — authors control order via source.
- **Carry-over:** heat (§24C.4), tags (§24C.3), and `no-value` / `no-percent` / `no-legend`. `no-headers` is a **no-op** (a sunburst has no parent header bars).
- **Labels:** hybrid orientation (tangential on inner rings, radial on outer); an arc whose sweep is below ~6° drops its inline label (kept in hover/legend) to avoid clutter.
- **`depth`** stays a **render budget, interactive-only** (§24C.9): static export renders the **full tree** (all rings) — consistent with the rectangular export contract. Interactive depth-collapse/drill is an app affordance.
- **Guidance:** radial suits **shallow trees (≤ ~3 levels)** and "share of a circle" reads; for deep trees or precise magnitude comparison, prefer the rectangular treemap (radius carries no magnitude → deep sunbursts distort). Flat, single-level data ≈ a `pie` (§15) — use `pie` there.
`radial` is a directive on `treemap`, not a separate chart type.
### 24C.11 Complete Example
```dgmo
treemap Q3 Budget
tag Team as t
Eng blue
Sales green
Ops orange
Engineering t: Eng
Platform 320
Mobile 180
Data 140
Go-to-Market t: Sales
Ads 90
Field Sales 130
Operations t: Ops
Cloud 110
Support 70
```
---
## 24A. RACI Matrices (RACI / RASCI / DACI)
A tasks × roles assignment matrix with author-time linting against the
defining structural rules of each variant. **One chart type — `raci` —
covers all three variants.** The variant is **inferred from the markers
used** — there is no directive to lock it.
| Variant | Marker alphabet | Constraint |
|---------|-----------------|------------|
| RACI | `R A C I` | Exactly one Accountable per task |
| RASCI | `R A S C I` | Exactly one Accountable per task |
| DACI | `D A C I` | Exactly one Driver and one Approver per task |
### Grammar
```
raci [Title]
[directives]
// optional bracketed phase header (trailing-token color, or same-line metadata when other keys ride along)
[Phase Label] [color]
Task name
// description lines are multi-line, before the first role
Optional description line
// markers from the resolved variant's alphabet
Role:
```
Three-level indented hierarchy: phase → task → role assignment / description.
Phase headers are optional; tasks without a phase are valid. Role-assignment
markers are space-delimited and must come from the resolved variant's alphabet
(e.g. `Cap: A R` is a combined-marker cell).
### Variant resolution
The variant is inferred from the markers used. The parser scans every marker
in the chart:
- Any `D` present → DACI.
- Any `S` present → RASCI.
- Otherwise → RACI.
**Mixed-marker conflict** — if both `D` and `S` appear in the same chart,
neither variant covers the markers, and the parser fires
`E_RACI_MIXED_VARIANTS`. (`D` is the DACI Driver; `S` is the RASCI Support —
they belong to different alphabets and cannot coexist.)
There is no directive to lock or override the variant — the markers are the
single source of truth, and there is no need to lock it.
The resolved variant is exposed in the rendered chart's header label
("RACI" / "RASCI" / "DACI") so authors can see which alphabet the linter
is enforcing.
### Directives
- `roles` — declares column order. Both inline and indented-block forms work:
```
roles Cap, QM, Bos // inline form (name-only)
```
```
roles // block form — supports per-role color
Cap red // trailing-token color, one role per line
QM orange
Bos yellow
```
When present, the `roles` directive enables the `W_RACI_UNKNOWN_ROLE` warning
for any role used in a task assignment that wasn't declared.
**Per-role color** uses the universal trailing-token form (`Cap red`) — same
as cycle / pyramid / ring / boxes-and-lines layers. Color names resolve through
the active palette. The inline form is name-only — to color roles, use the
indented block form. Same-line form (`Cap color: red`) is still accepted, but
reserve it for cases where another same-line key rides along.
- `active-tag ` — set the active tag group (§1.3).
### Phase metadata
Phase headers also accept a trailing-token color for per-phase styling:
```
[Departure] teal
[At Sea] purple
```
The phase bar tints to a soft mix of that color over the background. Useful
for clustering phases visually (planning vs. execution, etc.). Phases without
metadata fall back to the neutral gray bar.
### Example — RACI with phases (and optional colors)
```
raci Voyage Operations
roles
Cap red
QM orange
Bos yellow
Nav blue
Crew gray
[Departure] teal
Plot the course
Heading, currents, weather window
Cap: A
Nav: R
QM: C
Provision the hold
QM: A R
Cap: C
Crew: I
[At Sea] purple
Stand the watch
Bos: A
Crew: R
```
Per-role and per-phase color are optional — leave them off to get the neutral
gray defaults.
### Example — DACI inferred from markers
The `D` markers infer DACI — no directive needed.
```
raci Choose the next port
roles Cap, Nav, QM, Bos
Pick destination
Cap: D
Nav: A
QM: C
Bos: I
```
> Without a `[Phase]` header, tasks sit at **indent-0** with role cells one level in. Nest tasks under a phase only when a `[Phase]` header is present, otherwise the indented `roles` block consumes the following lines.
### Diagnostic codes
| Code | Severity | Fires when |
|------|----------|------------|
| `E_RACI_MULTI_ACCOUNTABLE` | error | A RACI/RASCI task has more than one A |
| `E_DACI_MULTI_DRIVER` | error | A DACI task has more than one D |
| `E_DACI_MULTI_ACCOUNTABLE` | error | A DACI task has more than one A |
| `E_RACI_INVALID_MARKER` | error | A marker isn't in the resolved variant's alphabet |
| `E_RACI_UNEXPECTED_LINE` | error | Free-text line appears after the first role assignment |
| `E_RACI_MIXED_VARIANTS` | error | Both `D` and `S` markers appear in the same chart |
| `W_RACI_MISSING_ACCOUNTABLE` | warning | RACI/RASCI task has no A |
| `W_RACI_MISSING_RESPONSIBLE` | warning | RACI/RASCI task has no R |
| `W_DACI_MISSING_DRIVER` | warning | DACI task has no D |
| `W_DACI_MISSING_ACCOUNTABLE` | warning | DACI task has no A |
| `W_RACI_UNKNOWN_ROLE` | warning | Role used without being declared in `roles:` |
Warnings are non-blocking; sketching authors can leave them noisy and address
them when polishing. There is no chart-wide or per-task suppression mechanism
in source — by design, source should not silently mute the linter.
### Display vs source ordering
Markers in cells are always **rendered in canonical alphabet order**
(`R A C I`, `R A S C I`, `D A C I`) regardless of source order. Source casing
and order are preserved in the file itself; mutations operate on source order
to keep round-trips byte-stable for everything except the cell that changed.
---
## 24B. Map Diagrams
Geographic concept maps: highlight/shade political subdivisions, drop points of interest, and connect them with routes or edges. Built for "share a concept" business maps (low detail), not cartography. Renders at a fixed position — no pan/zoom (cf. §17 visualization conventions). Basemap and viewport are **inferred from the content you reference** — most maps need no directives beyond the data itself.
### 24B.1 Declaration
```
map [Title]
```
Requires explicit first line — no heuristic detection.
### 24B.2 Basemap & viewport (inferred)
Both are **inferred from the content you reference** — normally there is no directive:
- **Boundaries** — country outlines are the base, always. For any country whose *subdivisions* you reference (value/tag a US state), that country's subdivision boundaries auto-load. Data follows content.
- **Viewport** — auto-fits to the bounding box of everything referenced (valued/tagged subdivisions + POIs) + padding. List Caribbean POIs and you get a Caribbean map; no preset needed. A country fill frames on its **dominant landmass cluster**, not its full territorial bbox: far-detached *minor* territories (overseas DOM such as French Guiana, distant small islands) are excluded from the fit, so a Europe map that names France stays on metropolitan France rather than zooming out across the Atlantic. Large detached parts (Alaska-scale) and near-shore islands stay in frame, and the territories still **draw** — they are just not allowed to balloon the viewport.
- **Resolution is a post-parse pass** — the parser collects referenced names, then a resolver determines the basemap(s), validates names (did-you-mean), lazy-loads geometry, and computes the fit. This is why "Georgia" is never ambiguous mid-parse.
The US-state mesh and US name-scoping are **inferred** from the content: reference any US state (`California heat: 92`, `Texas red`), a US-scoped POI, or `locale US` and the map renders as the conventional **US states map** — the full state mesh on `albers-usa`, **every state outlined even where it carries no data**. A POI-only US map (named US cities, no state) gets the same national US frame; you don't draw three floating dots on a blank country. There is no `region` directive — the world basemap is the default and US subdivisions follow the content.
A map is **"US-oriented"** (gets the full state mesh, and AK/HI insets whenever it frames the whole nation) when it has US content **and all other content stays within North America** — i.e. every other POI/region resolves to the **US, Canada, or Mexico**. A US-oriented map frames the **whole nation on `albers-usa`** when its data is national in spread; a **region/choropleth map whose coloured states are geographically compact auto-zooms to that region** (on `mercator` — North stays straight up, the same projection a sub-national POI cluster uses) instead of stranding the data in a corner of an empty nation. "Compact" is inferred from the raw data bounding box — it must occupy less than ~40% of the contiguous-US area (so the Northeast, or a single state plus its neighbours, zooms, while a coast-to-coast spread or a near-national diagonal stays national). Two cases always stay national regardless of compactness: any data touching **Alaska or Hawaii** (the composite insets are inherently a national device), and an explicit **`locale US`** (country, no subdivision) — the documented escape hatch for a compact map you want framed as the whole country. There is no force-zoom override (the failure being avoided is "too zoomed out"). POI-only US maps are unaffected — they keep their fit-to-cluster behaviour (below). A near-border neighbour is framed alongside the states rather than clipped: a single Canadian-city POI (e.g. the Toronto Blue Jays in an otherwise-US baseball map) no longer drops the map to a world projection, and **no `locale US` is required**. A Canada/Mexico **POI** expands the frame by just its point (the US barely shrinks); a Canada/Mexico **country fill** expands it to that country's full geometry (so the whole neighbour shows). Content reaching **beyond North America** — any non-NA POI, or a non-US/CA/MX country fill — keeps a geographic world/regional projection that frames everything (nothing is clipped). The **US state mesh draws when US states are referenced as data** (`California heat: 92`) **or the map is US-oriented** (all content within North America). It is **suppressed on a global map whose only US content is bare POIs** — a worldwide backbone with `us-east-1` + Tokyo renders **country-only**, since exploding the US into 50 states reads as noise once the frame spans beyond NA. (A US-state data reference always wins: `California heat: 92` + Tokyo still outlines the states, because the states *are* the subject.) Alaska & Hawaii always appear as insets on the national frame — see below; the main frame itself shows the lower-48 + DC, with AK/HI in their lower-left boxes. A country-level `United States` fill on its own is **not** US content — it keeps the US as a single shape (no state mesh).
Projection is **always inferred — there is no `projection` directive**:
- **`projection`** is auto-picked by **extent span**: the US-oriented national case (US content + only US/Canada/Mexico elsewhere, national spread) gets `albers-usa`; **world/multi-continent extents** (span > ~90°, or any frame reaching past ~80° latitude) get **equirectangular** (plate carrée — a clean, fully rectangular conventional wall-map frame), data choropleth and dataless reference alike, for one consistent world look. (Trade-off: equirectangular is not equal-area, so a choropleth's high-latitude regions stretch vertically — accepted for the consistent rectangular frame over Equal Earth's area honesty.) The world projection snaps to a full Greenwich frame (US left, Asia right — the conventional world view; avoids the antimeridian split a tight longitude union produces when a country box like the US wraps via its Aleutians). **Single-continent regional views** (Europe, East Asia) and **non-US clusters** use **`conic-equal-area`** (a per-extent Albers), so a mid-latitude continent reads with its familiar conventional shape and high-latitude land isn't vertically inflated. **Sub-national US** views — a POI cluster fit-zoomed to its home state, or a compact region/choropleth that auto-zooms — use **`mercator`** so North stays straight up (a conic fans its meridians and visibly tilts an off-centre US region); area distortion across a sub-national frame is negligible. The user never picks a projection. **Skew fallback (aspect-aware):** the `albers-usa` choice is provisional — it's the *national-composite* frame, which elides Alaska/Hawaii from the basemap (they live in insets, drawn only when referenced; see below). When the host renders into a canvas whose aspect is **far from CONUS's own** (the desktop app's preview pane, vs. the headless export sized at the CONUS aspect), contain-fitting the lower-48 opens margins where the elided AK/HI landmass projects — bare ocean painted exactly over Alaska, *a lie*. So a **map that references neither Alaska nor Hawaii** falls back at that skew to **`conic-equal-area`** framed on the data extent: every landmass draws in true position, so Alaska is honestly off-frame rather than faked as sea. Near-CONUS aspects keep the national snap; a referenced AK/HI map always keeps `albers-usa` (its insets are the right tool).
- **Alaska & Hawaii insets** under `albers-usa` are **inferred, not a directive**: the national frame is the canonical AlbersUSA composite, so the AK/HI inset boxes **always draw** there — even with no data in them — because a US national map must not silently drop two states (*maps never lie*). The empty insets take the map's own dress (the colorize pastel, or neutral land under `no-colorize`), so they read as the rest of the country, not as data. Only `albers-usa` (the whole-nation frame) shows them; a **compact/sub-national** US map (the auto-zoom or a fit-to-cluster POI map on `mercator`) frames just its region with no insets. There is no flag.
**Basemap dress** — how the land/water themselves are coloured — is **inferred from content**, with one opt-out (`no-colorize`). **Colorize (distinct political fills) is the default**; the only things that turn it off are region *data* or the opt-out:
| Content | Dress |
|---|---|
| **Any region data** (`heat:` or a tag) | Choropleth / categorical hues on a **neutral-gray** basemap — the data owns the only saturation (the choropleth convention). Colorize is auto-suppressed (no error). |
| **No region data** — named regions, POI/route-only, or a bare `map` | **Colorized** — every drawn region gets a distinct pastel "political map" fill (see below). POIs/routes draw on top. |
| **`no-colorize`** | Forces the plain **green land + blue water** dress (the opt-out). |
- **Colorize (inferred political fills).** Unless a region carries data (a `heat:` or a tag), the map is dressed as a *political map*: every region drawn at the resolved extent is filled a **distinct light pastel** such that **no two bordering regions share a hue** — the conventional "colour the countries/states so neighbours separate" look, with zero config. This holds **across the international border too**: a US state never shares a hue with the Canadian/Mexican land it touches (e.g. Maine vs Canada). It is the default for **any** non-data map — named-region maps, POI/route-only maps, and even a bare `map` (the whole world colours as the backdrop; markers and routes render over it). It colours the **whole drawn political set**, not just the named regions: naming one US state colours all 50 (plus the AK/HI insets when shown); naming three European countries colours every visible European country. The fills are **non-semantic** — pure boundary disambiguation — so they add **no legend entry**, and a region's colour is **extent-independent** (the same `.dgmo` colours France identically at any width and in an inset). A direct trailing colour (`Texas red`, §24B.4) paints **on top** of the pastel as a highlight and does **not** suppress colorize. Adding any `heat:` or tag flips the map to the data dress automatically (colorize is auto-suppressed — no error). Use **`no-colorize`** for the plain green-land backdrop (e.g. when many POIs/routes should pop against a calm map). Across-water look-alikes (e.g. the UK and France across the Channel) may share a hue in v1 — see §24B.12.
**Cosmetic basemap features are ON by default.** Every feature below renders with no directive; the only control is a bare `no-*` opt-out (§24B.7). There are no positive opt-in flags.
**Relief** — subtle mountain-range terrain shading (always ON; only `no-relief` turns it off):
- Draws notable mountain ranges (the Rockies, Andes, Alps, Himalayas, …) as distinct horizontal hachure lines (dark-on-light, light-on-dark) clipped to each range, so the land reads as textured terrain rather than flat fill. It's decoration for **visual richness**, not orientation: there are no range labels and no height tiers (single tier in v1).
- **Unconditional** — relief renders on **every** map: data choropleths as well as reference maps (the renderer lays the hachure **atop** the data fills and the hatch tone flips to stay visible over muted land), at **every zoom** (world, continent, US national, regional), and at **every width**. `no-relief` is the single switch that turns it off. The only remaining filter is a per-range quality guard: a range whose projected footprint is sub-pixel (below a min area / min dimension) is skipped so it never draws as a smudge — that drops individual ranges, never the feature.
- Draws **under** rivers, POIs, and labels, and is clipped to land so the lines never touch water. The lines render crisp with a non-scaling stroke, so their thickness and spacing stay uniform — no moire — regardless of render size or DPR.
- Needs the optional `mountain-ranges.json` data asset; if a build predates it, relief simply draws nothing (no error).
**Coastline** — faint nautical-chart water-lines (default ON; `no-coastline` turns it off):
- Draws **5 faint, equal-width coast-parallel water-lines** stepping offshore and **fading seaward** — the antique nautical-chart depth-contour look — so the eye instantly separates a **water border** (ocean/lake shore) from a **land border** (the shared edge between two adjacent regions). It applies to the outer ocean coast, inland lake shores, and the AK/HI inset boxes.
- The lines sit on the **water side only**: each land outline is buffered into a thin ring and revealed only where it falls on water (via an SVG mask of *water = canvas − land + lakes*), so land/land borders self-remove (no separate boundary data needed). They draw **above** the water/land/choropleth fills and **below** rivers, POIs, edges, and labels. Where relief also draws, the two never overlap — water-lines stay on water, relief stays on land. Decorative: no knobs, no labels, no data attributes.
- The offshore distance is a fixed **fraction of the canvas**, so the rings stay proportional at any export size and any geographic extent, and they **carry through to every edge of the visible map**. Small islands (Hawaii, the Caribbean chain, etc.) **are** ringed — the island hashing is part of the nautical look; only sub-threshold specks are dropped to avoid confetti. Polygon holes/enclaves (e.g. Lesotho inside South Africa) are **not** ringed; very narrow inlets/fjords may fill solid at small scale (accepted/artistic).
- Needs no data asset — geometry is derived from the drawn region paths.
**Context labels** — orientation backdrop (default ON; `no-context-labels` turns it off):
- Paints a sparse backdrop — water-body names plus a few notable *unreferenced* countries — so a reader can place the view at a glance. It is **orthogonal to region labels**: region labels name the regions you *referenced*; context labels name the surrounding *context* (oceans, seas, neighbouring countries) and **never double-label** a region already labeled.
- **Water** (oceans, seas, gulfs, bays, straits, channels, sounds — *no rivers*) is named in full, always, in a muted blue-gray italic with letter-spacing; names and prominence come from **Natural Earth** marine data (neutral, citable provenance — the single editorial override is `Gulf of Mexico` → `Gulf of America`).
- **Countries** that you didn't reference are picked by on-screen size and labeled only where the name fits the footprint; they use the ISO code at the compact render width, the full name otherwise (mirrors the region-label abbreviation policy).
- **Density is deliberately low.** A small per-view label budget plus a span-derived priority (oceans → major seas → minor water; biggest countries first) keeps it clean: a **world view shows oceans and major seas only — never bays**; broader-detail views admit smaller features as space allows. Under dense data it degrades to **zero** context labels (data already orients the reader); context labels are placed **dead-last** and never displace a data, region, or POI label. There is no density knob — it should never look cluttered.
- On a **US (`albers-usa`) view** the surrounding oceans (Pacific to the west, Atlantic to the east) and the Gulf of America, plus neighbouring countries (Canada, Mexico, …), are labeled too — oceans whose centre is just off-frame are edge-clamped to the margin, while distant oceans and off-frame AK/HI water are dropped (so nothing is mislabeled). Smaller basins keep the strict on-screen rule.
- Water names sit only over **open water** — a candidate whose footprint would touch land (or doesn't fit the canvas) is excluded rather than placed. Country names sit over their country.
- Needs the optional `water-bodies.json` asset; absent → no water labels (no error).
**City dots** — subtle gazetteer scatter for orientation (default ON; `no-cities` turns it off):
- Scatters a **faint, unlabeled dot** at notable gazetteer cities across the basemap so the eye can place a region by its cities. Decorative: muted ink at low opacity, **no label, no interactivity, no data attributes** — pure context, drawn **above** the basemap (land/water/relief/rivers) and **below** connectors, POIs, and labels.
- Cities are taken **population-first** and **spacing-thinned** (a fixed minimum pixel gap between dots), so density **adapts to zoom for free**: a world view shows only the biggest of any dense cluster; zooming into one country spreads those apart and lets smaller local cities fill in. The only cull is the **visible canvas** — any city that projects on-screen is a candidate (so near-border neighbour cities show too), independent of projection or the declared extent.
- **Explicit POIs always win:** a city dot is never placed under a referenced `poi` marker (it would double-draw the same place).
- Uses the bundled `gazetteer.json` (the same city data that resolves bare city names). International coverage is **major-city only** (a high population floor outside the US), so a zoomed-in *foreign* country shows few dots until the gazetteer densifies.
### 24B.3 Region fill — heat (choropleth)
A subdivision name on its own line, with a `heat:`, fills with a tint ramp:
```
map
region-heat Sales ($M)
California heat: 92
Texas heat: 78
Florida heat: 51
```
- **`heat:`** on a region renders as a **choropleth shade** — the region's colour channel. (Each map element kind carries its own channel key: POIs take `size:` → marker radius, edges take `width:` → line thickness; see §24B.5/§24B.6. A channel key on the wrong element errors, §24B.10.)
- **`region-heat [low] [high]`** labels the ramp in the legend and optionally sets its endpoint colors (§1.5). **No color** → the default red high (chosen to stand out against blue water) over a neutral low. **One color** → that color is the high (full-value) hue over a neutral low: `region-heat Sales ($M) blue` → a blue ramp. **Two colors** → explicit `low high` endpoints in that order: `region-heat Peril green red` → green (low) → red (high). A wide-hue-gap pair routes through a neutral midpoint (so green→red mid values stay clean, not muddy); analogous pairs blend directly. The order is literal — **which end means "good" vs "bad" is your choice** (dgmo encodes only "low color = the low-value end"). Up to two trailing color names peel from the right and never empty the label (`region-heat Red blue` keeps the label `Red`, high `blue`).
- The ramp **auto-fits** — there is no `scale` directive. The low end anchors at the **data minimum** (data-min→data-max for all data, signed or not), maximising within-map contrast and matching the size/width ramps (`poi-size`, `flow-width`), which also floor at their minimum. A ~15% tint floor keeps the minimum reading as "low, present" rather than empty. The legend prints the actual min/max values. (Equal-value data renders at the top of the ramp. Cross-map comparability is not provided — two maps fit their own ranges independently; re-add a `scale`/comparative mode only if small-multiples ever needs it.)
- The numeric **value is shown on-map by default**, drawn as a smaller, dimmer second line **under the region's name** (e.g. `California` over `39.5M`). It uses the same **compact formatting** as the legend's ramp ends (`1.1K`, `39.5M`, `2.3B`; sub-1000 values bare), so a region's printed value lines up with the legend scale. It renders only when the **heat ramp is the active colouring dimension** (a tag-coloured map shows no value line). Where a region is too small to fit the name+value stack, the label degrades to the bare name (the value is then available via hover); `no-region-heat-value` suppresses the value line entirely while keeping the name.
- A subdivision with no `heat:` (and no tag) renders as the neutral base. Heat input is the **`heat:` key only** — no bare-trailing-number or tabular shorthand.
- Value keys are **subdivision names generically** (not assumed state-level), so deferred county/FIPS keys slot in later without a grammar change.
- `heat` + a tag value may **coexist** on a region — see §24B.4.
### 24B.4 Region fill — categorical (tags)
Categorical fill uses the universal tag model (§1.3): declare a `tag` group and apply its alias as a key. The first declared group colours regions by default (§1.3) — `active-tag` is only needed to select a *different* dimension (another group, or the value ramp).
```
map
tag Market as m
HQ indigo
Region teal
Prospect amber
United States m: HQ
Germany m: Region
Japan m: Region
Brazil m: Prospect
```
**`heat:` + a tag value on the same region (bivariate):** both are kept and become **two selectable colouring dimensions**. The legend (below the title) shows the value ramp and each tag group as **mutually-exclusive, collapsible groups**; the active one drives every region's fill. Exactly one is active at a time:
- **Default** active dimension is the **heat ramp** whenever any region has a `heat:`, else the first tag group (the universal precedence — decision #48; treemap §24C.7 follows the same rule).
- **`active-tag `** activates that tag group → categorical fill (the heat value is ignored for fill but still parsed/available).
- **`active-tag `** — the `region-heat` label, or `Value` if none — re-selects the choropleth ramp. (There is no magic `score`/`heat` token; the ramp is the default, and you select it by its legend name like any other dimension.)
- In the app, **clicking a legend group flips** the active dimension (live preview only — it does not edit the source; the authored `active-tag` is the default). Exports render only the active dimension's legend.
Authoring both is fully supported — no warning.
**Direct color (the lightweight escape hatch):** a **trailing color** on a region line (§1.5) paints that one region a flat categorical fill without declaring a tag group — `Texas red`, `California blue heat: 92` (color before metadata). It is a **flat override**: it paints regardless of the active colouring dimension and adds **no legend entry**. Use it to highlight a region or two; use a `tag` group when the colors are a legend-worthy category. A direct color wins over both the value ramp and any tag on the same region.
### 24B.5 Points of interest (`poi`)
```
poi > [as ] [: , …]
```
A POI's **position** is independent of its **label**. Position is either a gazetteer city name or **positional coordinates** — two leading numbers, lat then lon, signed:
```
// label defaults to "Austin"
poi Austin
// anchored at Austin; displays "West HQ"
poi Austin label: West HQ
// coord-positioned (lat lon); label supplies the name
poi 39.74 -104.99 as dcw label: DC-West
// size → marker radius (see poi-size, §24B.7)
poi Tokyo size: 38
// categorical color via a tag alias
poi Chicago m: Office
// direct marker color (§1.5 trailing token)
poi Austin red
```
- **Coordinates are positional** — `poi ` (e.g. `poi 39.74 -104.99`). Two leading numbers occupy the name region, so `as `, trailing color, and metadata peel by the universal rules (§1.4/§1.5). Unambiguous against city names (cities never start with a number). This is the offshore / exact-site case (oil rigs, data centers).
- **`size:`** value-scales the marker radius — a **data channel**, not pixels (see `poi-size`, §24B.7). The sanctioned way to show magnitude per point and to pre-aggregate dense data (`poi Dallas size: 320`).
- POI properties (v1): `label`, `size`, `style`, `clock` (local-time card — bare flag or `clock: `, §24B.14), the applied tag alias, `as`. **No `icon`** in v1 (color-only categories).
- Coord-positioned or relabeled POIs take **`as `** to be referenced by routes/edges; named POIs are referenced by name.
- POIs are **tag-colored from their tag group independently of `active-tag`** (which governs region fill); each tag group emits its own legend section. A POI with a tag renders at the **flat tag color**.
- A **trailing color** (§1.5) on a POI sets its marker fill directly — `poi Austin red` — no tag group needed. It **wins** over a tag color and over the default orange; it is a flat override and adds no legend entry. (A city literally named for a color keeps it via capitalization — `poi Orange` is the place, `poi orange` is the color.)
- **Metadata form:** same-line (`poi Austin t: A, size: 18`) for 1–2 attributes; **indented (one per line, no commas) is preferred for 3+ attributes**. Indented attributes and an indented `-> destination` edge may coexist under a `poi` (per §1.7) — useful for converging hubs (each origin a source with its own `-> venue`). An indented child **naming a place must carry an arrow glyph** (it is a hub edge from the POI); a bare name with no arrow errors as a malformed hub edge rather than silently degrading to a top-level region — declare a standalone place at the top level instead.
### 24B.6 Routes & connectors
**`route `** — an ordered, auto-numbered voyage. The header names the **origin** (the first stop, with a distinct marker) and takes **no shape option**; each indented line is a **` ` leg** that continues from the previous stop, using the **same indented arrow idiom as a sitemap** (`-label-> dest`). A leg **is an edge**: the in-arrow text is the leg label, **`width:`** is its thickness, and the **arrow glyph alone sets its shape** — `-…->` is straight, `~…~>` is an arc, **per leg, mixable**. The **arrow is required and must be directional**: a bare destination with no glyph errors as a malformed leg, and an **undirected glyph** (`--`/`~~`) errors too — a voyage always flows from the previous stop to the next, so only `->`/`~>` (and their labeled forms) are valid legs. A **tag on a leg line colours the LINE** (categorical leg-type colouring — flights vs cruises vs trains; see "tagging the line" below); **`label:`/`as`** still name the **destination** stop. The header `size:` sizes the origin marker, and a tag/`label:` on the header decorates the origin **point** (the header is a stop, not a line). **Repeat the origin as a leg's destination to close a loop** (drawn without a second marker). To size or categorise an intermediate **stop**, declare it as a `poi` and reference it by name.
```
route Miami
// arc leg: label + thickness
~weigh anchor~> Nassau width: 40
~> Grand Turk
~> San Juan
// destination == origin → closed loop
~> Miami
```
There is **no header `style:`** — make a whole voyage arc by using `~>` on each leg (mix `->` and `~>` freely when only some legs should bow).
The indented `-> dest` leg shape is valid **only inside a `route` block** (an ordered voyage). Outside a `route`, every native edge is one full line (` `) — see below.
**Native edges** handle every non-route connection — there is no `link` or `leg` keyword. The connector token follows one rule: **it draws an arrowhead iff it ends in `>`, and an arc iff it starts with `~`.** Drop the `>` for a plain line (no arrowhead) — use it when the relationship is mutual or non-directional and an arrow would mislead. The label, if any, always sits between the delimiters:
| | no label | labeled |
|---|---|---|
| directed straight | `A -> B` | `A -ships-> B` |
| directed arc | `A ~> B` | `A ~trade~> B` |
| undirected straight | `A -- B` | `A -ferry- B` |
| undirected arc | `A ~~ B` | `A ~cable~ B` |
```
// one-off, directed
A -> B
// undirected line; width = thickness (see flow-width, §24B.7)
A -ferry- B width: 12
// undirected arc with a label
A ~cable~ B
// inline chain (ordered, but not a numbered route)
A -> B -> C
// hub/star — ONE edge per line, repeat the origin
// (indented `-> dest` is a parse error outside a route)
JFK ~daily~> LAX
JFK ~daily~> LHR
// the connector label carries the relationship
JFK ~2x daily~> SFO
```
- Edge **label = the relationship** (`-ships->`, `-ferry-`); `width:` (line thickness) rides as edge metadata.
- For a **hub/star** (many edges from one origin — an airport's daily flights, a depot's shipments), repeat the origin on a one-line edge per spoke (`JFK ~daily~> LAX`, `JFK ~daily~> LHR`). There is **no indented / shared-source hub form** for native edges — an indented `-> dest` outside a `route` errors as `Malformed edge`. For an ordered chain use a `route` block or an inline `A -> B -> C`; route legs are inherently directional voyages. Edge endpoints auto-create their POIs, so a connected map needs no separate `poi` lines.
- No geographic path-finding — legs are straight or arced geometry and may cross land.
**Tagging the line (categorical leg colouring).** A **tag value on a connector or route leg colours the LINE itself** — the universal tag model (§1.3) applied to edges. Declare a `tag` group and apply its alias on the edge/leg line; every line wearing that value renders in the group's colour, and the group shows in the legend as a **line-colour key**. This is the "trip-leg type" idiom (flights, cruises, trains) — colour the segments, not the dots:
```
map 7 Night Eastern Caribbean
tag Leg as l
Fly gray
Cruise blue
Train green
poi Denver as den
poi Miami as mia
poi 25.62 -77.56 as ck label: Coco Cay
poi Orlando as orl
den ~> mia l: Fly
mia ~> ck l: Cruise
ck ~> mia l: Cruise
mia ~> orl l: Train
orl ~> den l: Fly
```
- The tag colours the **line**; to categorise a **stop** instead, put the tag on its own `poi` line (`poi mia m: Port`). One alias per line stays unambiguous — line vs. stop is decided by *which line the tag sits on*, not the tag itself.
- An edge/leg tag is a **flat line colour** like a POI tag — it colours independently of `active-tag` (which selects the region-fill dimension). A tag group used **only** on connectors never tints regions or suppresses the political colorize dress (§24B.2); it just adds its line-colour swatches to the legend.
- A connector with no tag renders in the default neutral connector ink. `width:` (thickness) and a tag (colour) compose on the same line.
### 24B.7 Labels, legend & chrome
**The zero-config map is the good-looking map.** Type `map`, name some places, and every label and basemap feature renders by default. The only directives are the six that name irreducible author intent and the seven `no-*` opt-outs.
- **Title** is the declaration line (`map My Map`); **`caption`** is a directive (in-image data-source attribution — the only provenance carrier on an exported PNG). There is no `title` or `subtitle` directive — the title is the first line.
- **`region-heat `** (region colour ramp), **`poi-size `** (POI marker size), and **`flow-width `** (edge line thickness) label their channel's legend with units. Each element carries its own channel key — `heat:` on regions, `size:` on points, `width:` on edges — and the directive names what that channel means (regions by population, points by sales, edges by volume). A trailing color on `region-heat` sets the ramp hue (§24B.3).
- **`active-tag `** picks which tag group leads when several are present (or the heat ramp by its label/`Value`); declaration order otherwise decides.
- **Region labels** are **on by default** — subdivision names for the regions you *referenced*, auto-fitting **full → abbrev → hide**: the full name shows when it fits its footprint, a **US-state two-letter abbreviation** is tried when it doesn't, and the label hides rather than overlap or spill onto the ocean. (A clean abbreviation exists only for US states; other regions degrade full → hide.) At the **compact render width** (< ~480px effective width) the abbreviation is preferred first. **Exception — a sub-national US choropleth zoom** (map-us-subnational-zoom): abbreviations are skipped entirely; a state too small to carry its full name in place instead floats that name + value into a **leader-lined callout** in the ocean-side margin column, so the zoomed map reads with full names throughout.
- **POI labels** are **on by default** (collision-managed auto): a POI's label or value renders **on the map**, not hover-only, so it survives PNG/PDF export (the dominant deliverable). Auto-collision handling is inline + halo, escalating per dense cluster to leader lines, then numbered pins + a legend list. **Markers never move** (geographic truth); only labels move.
- **Legend** auto-composes from the colouring dimensions (tag swatches + heat ramp + region-heat) and is on by default. POI size and edge thickness are self-evident from the marker/line scale and carry no legend key in v1.
- **Cosmetic `no-*` opt-outs** — the only switches; each turns off an on-by-default feature (no positive opt-in form exists):
| Flag | Turns off |
|------|-----------|
| `no-legend` | The whole legend block. |
| `no-coastline` | Coastal water-lines (§24B.2). |
| `no-relief` | Mountain-range relief shading (§24B.2). |
| `no-context-labels` | Orientation backdrop — water + unreferenced countries (§24B.2). |
| `no-region-labels` | On-map subdivision names. |
| `no-region-heat-value` | The metric value drawn under each region's name on a `region-heat` choropleth (§24B.3). The name still renders (that's `no-region-labels`); only the numeric value line is suppressed. |
| `no-poi-labels` | On-map POI labels. |
| `no-colorize` | Inferred political fills (§24B.2) — forces the plain green-land dress. A no-op under data (the basemap is already neutral-gray). |
| `no-cities` | Subtle gazetteer city dots (§24B.2) — the faint orientation scatter across the basemap. |
| `no-cluster-pois` | Coincident-POI clustering (§24B.2) — markers always render fanned out (the static-export look) instead of collapsing into a count badge. A no-op for export (already expanded) and for maps with no overlapping markers. |
Each `no-*` is a bare flag, idempotent (repeating it is harmless, no duplicate warning, like `no-legend`). A **plain / data-journalism look** is the five basemap flags together (`no-coastline no-relief no-context-labels no-region-labels no-cities`) — there is no single "clean" master flag. (`no-colorize` is **not** one of the five — it toggles region *fill style*, not a basemap backdrop layer.)
- **Hover detail** (interactive contexts only) shows fuller per-feature detail (app-controlled contents); it is supplementary — the on-map label/value is the export-safe channel.
### 24B.8 Name resolution
**Standard (not arbitrary).** Admin units use **ISO 3166** — 3166-1 (countries), 3166-2 (subdivisions); geometry is keyed by code, not display string, so "United States" / "USA" / "US" resolve alike. Cities use **GeoNames** (geonameid + population + alternate names) — giving alias/accent/abbreviation matching ("NYC", "São Paulo"), the population ranking behind most-populous, did-you-mean, and the autocomplete dictionary, all from one source.
- **`locale `** scopes bare name resolution to a default country (`locale US`) or subdivision (`locale US-GA`) — one field; length/format disambiguates. The **country** part biases ambiguous bare cities to that nation; the **subdivision** part further prefers that state — `locale US-GA` resolves a bare `Athens` to Georgia rather than the most-populous global match. It is a soft preference: a name with no match in the locale still resolves (most-populous), so the locale never hard-fails. Normally **inferred from the referenced content**; set it explicitly only to steer an ambiguous guess. (Discoverability: it surfaces in editor completion, and a cross-border/ambiguous resolution emits a hint diagnostic naming `locale` at the point of need.)
- A bare ambiguous, **undeclared** name resolves to the **most-populous** match in scope, with an info note.
- **Disambiguate once.** Add a trailing ISO code at first declaration — 3166-1 country (`San Jose CR`) or 3166-2 subdivision (`Portland US-OR`). The parser treats a trailing token matching `/^[A-Z]{2}(-[A-Z0-9]{1,3})?$/` as a scope qualifier (city names aren't all-caps). **You don't repeat it** — once declared, the POI is a named node; later bare references (`Portland -> Seattle`) bind to that node. Two same-named cities in one map → give each an `as ` and reference the aliases.
- **Bare US state codes resolve to states.** A trailing bare two-letter token that is a **US state postal code** (`OR`, `CA`, `TX`, …) resolves to that state — `poi Portland OR` → Oregon, `CA` = California (not Canada) — and itself signals US scope (so `poi Portland OR` works standalone). Any other bare two-letter token (`CR`, `FR`, `JP`) is a 3166-1 country code; the explicit `US-XX` form is always unambiguous. **Trade-off:** a foreign country whose code collides with a state postal code (`DE`, `IN`, `AR`, `CO`, `LA`, `PA`, …) can't be picked by bare code in a US context — use the city name alone (most-populous → `Berlin` = Germany) or coordinates.
- **Region-fill lines disambiguate the country-vs-state collision** (`Georgia` is both 3166-1 country `GE` and US state `US-GA`) two equivalent ways: a **bare ISO code** (`US-GA heat: 5` → state, `GE heat: 5` → country — codes resolve directly, no name needed) or **name + scope** (`Georgia US heat: 5` → state, `Georgia GE heat: 5` → country). Prefer one of these over the redundant `Georgia US-GA` (still accepted, and a mismatched code like `Georgia US-CA` is rejected). Bare codes never warn; an explicit scope on a named region silences the ambiguity warning. An inferred US-scope signal (a `locale US` directive, another bare US-state name, or a US-scoped POI) also resolves the name to the state silently — in a US context the state is the unambiguous intent. The ambiguity warning (with both fixes named) appears only when there is no US-scope signal at all, where the name resolves to the country.
- **IATA airport codes resolve to airport coordinates.** A memorized three-letter airport code — `JFK`, `LAX`, `LHR` — resolves to that airport's location, so `poi JFK` and `route JFK -> LAX` just work with no new syntax. Coverage is **large international hubs + all US scheduled-commercial airports** (the codes a traveler types from muscle memory); resolution is **case-insensitive** (`jfk` / `Jfk` / `JFK` alike). Airport codes are the **lowest-precedence identifier** — consulted only after city names and aliases, so a token that is **both** a city and a code resolves to the **city** (`Ufa` → the Russian city, not UFA airport), with a non-blocking hint naming the airport. Airports resolve by **code only**, never by airport name (`poi "John Wayne"` does not resolve — use `SNA`); the airport name shows in editor completion to disambiguate look-alike codes (ORD vs ORF). A POI's label is the typed code (`poi JFK` renders "JFK"); override with `label:` as usual. The airport's nearest city stays a city (`poi Orlando` → the city centroid; `MCO` → the airport), and a code the bundled set doesn't carry errors with an `as ` coordinates hint.
- **A city name must match the gazetteer's canonical form.** `New York City`, not `New York`; `Washington, D.C.`, not `Washington`. A name with no gazetteer match is rejected with a did-you-mean (§24B.10) and contributes no point — and since the viewport auto-fits the places that *did* resolve, a dropped point leaves a plausible-looking but incomplete map. Use the exact name or coordinates.
- **Discovering the right token** (so authors and AI agents don't guess): `dgmo map search ""` (CLI, `--json` for machine output) and the `lookup_map_location` MCP tool substring-search the gazetteer cities **and** the bundled airports, returning the exact token to use (`york` → `New York City`; `heathrow` → `LHR`). There is deliberately **no exhaustive printed catalog** — thousands of cities + ~1,500 airports — so search/completion is the discovery interface; both reuse the same fold→lookup data the resolver does.
- **Positional coordinates** (`poi `) are the ultimate escape for anything missing or ambiguous — including an airport not in the bundled set, or to force the airport over a colliding city (`poi 54.56 55.87 as UFA`).
### 24B.9 Reserved keys & colon usage
Reserved-key registry (§1.4.3) for `map`: `heat`, `size`, `width`, `label`, `style`, `clock`, plus declared tag aliases. These require colons (same-line `key: value` or indented) — **except `clock`, which is dual**: a bare `clock` flag (zone auto-derived from the place) or a valued `clock: ` (§24B.14). The three numeric channels are **per-element**: `heat:` (region choropleth shade), `size:` (POI marker radius), `width:` (edge line thickness) — a channel key on the wrong element kind errors (§24B.10). The directive set is **17, all colon-free** — eight naming irreducible intent (`region-heat`, `poi-size`, `flow-width`, `locale`, `active-tag`, `caption`, plus the clock-card window pair `hours` + `workweek`, §24B.14) and nine `no-*` cosmetic opt-outs (`no-legend`, `no-coastline`, `no-relief`, `no-context-labels`, `no-region-labels`, `no-poi-labels`, `no-colorize`, `no-cities`, `no-cluster-pois`). There is **no** `projection`, `scale`, `subtitle`, or `surface` directive (and no `surface:` metadata key) — projection/ramp are inferred, and the cosmetics have no positive opt-in form. A removed token used as a directive first-word is not recognized and parses as content (a region-fill that then fails resolution). Coordinates are positional (no `at:` key). A **bare trailing color** (§1.5) is honored on tag values, on a region line (flat override fill, §24B.4), on a `poi` line (marker fill, §24B.5), and on `region-heat` (ramp hue, §24B.3).
### 24B.10 Diagnostics & line classification
**Line classification** (single-pass, deterministic): keyword-first (`region-heat`/`poi-size`/`flow-width`/…/`tag`/`poi`/`route`) → indented child of a `route` block (= leg, `[-label->] dest`) → line containing `->`/`~>` (= edge; endpoints are POIs, implicitly created) → bare line (= region-fill subdivision). The `poi` keyword is load-bearing — a keyword-less, arrow-less line is never a point.
| Condition | Behavior |
| --- | --- |
| Subdivision not in basemap | error + did-you-mean |
| POI name not in gazetteer & no coords | error + did-you-mean |
| Unknown three-letter (airport-shaped) token | error naming it an airport code + `as ` coordinates hint |
| Token resolves to a city but is also an IATA code | city wins; non-blocking info hint names the airport escape (`as `) |
| `heat:` + tag on same region | both kept as selectable colouring dimensions; active one fills (default: the heat ramp). No warning — §24B.4 |
| Ambiguous bare city name | resolves most-populous in scope + info note |
| Duplicate region / POI | last-wins + duplicate warning |
| Empty map (no referenced content) | valid — renders inferred base map |
| Wrong-channel key on an element (`size:`/`width:` on a region, `heat:`/`width:` on a POI, `heat:`/`size:` on an edge) | error naming the element's correct channel key (regions `heat:`, points `size:`, edges `width:`) |
| Bare-coord `clock` pin with no derivable zone | warn `W_MAP_CLOCK_TZ_NEEDED`; pin renders without a card — add `clock: ` (§24B.14) |
| `clock:` zone contradicts a named city's real zone | warn `W_MAP_CLOCK_TZ_OVERRIDE`; the override is honored (§24B.14) |
| Unparseable `clock:` zone | warn `W_MAP_CLOCK_TZ_INVALID`; pin renders without a card (§24B.14) |
### 24B.11 Data & rendering (v1)
- **Boundaries:** world countries (8% simplified ≈ 11 KB gz coarse + 50m for regional views) + US states (15% ≈ 7 KB gz). Lazy-loaded TopoJSON; not bundled into non-map diagrams.
- **Gazetteer:** GeoNames-derived (geonameid + population + alternateNames), world majors + US cities, minified to `{name → [lat, lon], iso, pop}` (~25–30 KB gz). Single asset shared by the renderer and the editor completion source.
- **Airports:** OurAirports-derived (public domain), large international hubs + all US scheduled-commercial airports, in a **separate optional asset** keyed by IATA code (~38 KB gz). Lets `poi JFK` / `route JFK -> LAX` resolve through the same name-lookup path; never mixed into the city gazetteer (so the city-dot scatter and reverse-geocode never see airports).
- **Renderer:** d3-geo projection + path render (already in dgmo's d3 family); reuses the tint-ramp, tag/legend, and label-layout machinery.
- **Layering:** filled/scored regions → edges → POIs → labels → callouts.
- **Deterministic render rules** (author-invisible, no lever — must be predictable): co-located POIs spiderfy/jitter by a fixed rule (collapsing into a count badge in the interactive preview, fanned out in export — `no-cluster-pois` forces the fanned form everywhere); parallel/opposing edges between the same endpoints fan out and each keeps its own `weight`; route legs crossing the antimeridian take the shortest great-circle wrap.
### 24B.12 Reserved grammar seams (future-additive — leave room, do not repurpose)
Parse-accepted or grammar-reserved now so adding them later is non-breaking; **not built in v1**:
> **Cut 2026-06-01:** `date:` and `description:` were removed from the reserved-key registry (no v1 surface — they now raise an unknown-key error rather than silently no-op; re-add at their feature epics). `flow-width` + edge `width:` thickness and undirected `--` are **shipped** (no longer seams). Numeric channels are **per-element** keys mirroring their directive: `heat:` (region, `region-heat`), `size:` (POI, `poi-size`), `width:` (edge, `flow-width`) — see decision #20.
>
> **Cut 2026-06-02 (directive scrutiny, 18 → 13):** removed `region` (basemap is inferred from content; the `world`/`us-states` selector was redundant or inert), `muted` + `natural` (basemap dress is a fixed automatic aesthetic — no override), `no-insets` (AK/HI insets are now **inferred** — shown only when the map references Alaska or Hawaii), and `scale … center ` parse-acceptance (the diverging-ramp seam below stays documented but is no longer parsed until built). Merged `default-country` + `default-state` into one **`locale `** directive — and **wired its subdivision part** (the old `default-state` was parsed but never consumed; `locale US-GA` now genuinely scopes bare-city resolution to Georgia). With `region us-states` gone, the US-state mesh now renders for **any all-US-content map** — including POI-only named-city maps, which snap to the national `albers-usa` frame with every state outlined (replacing the old "POI-only US maps stay on a geographic projection" behavior). The `region-heat`/`poi-size`/`flow-width` trio is **retained** pending richer hover/legend interactions that will lean on the per-channel labels.
>
> **Cut 2026-06-02 (defaults-on review, 16 → 12):** cosmetic directives flipped to **on-by-default** with bare `no-*` opt-outs — `coastline` → `no-coastline`, `relief` → `no-relief`, `context-labels on|off` → `no-context-labels`, `region-labels full|abbrev|off` → `no-region-labels` (now auto **full → abbrev → hide**, US-state abbrev only), `poi-labels off|auto|all` → `no-poi-labels` (auto only; the `all` mode dropped). Removed **`projection`** (replaced by span-based inference: equirectangular for all world maps, albers-usa for US, mercator sub-world), **`scale`** (the ramp always auto-fits to data-min→data-max; *update 2026-06-08:* the low end anchors at the data minimum, no longer at 0 for non-negative data — see decision log), **`subtitle`**, and **`surface`** — both the directive and the `surface:` leg/edge metadata (parser stripped in this cut; the `surface-route.ts` renderer module plus the `ResolvedEdge.surface`/`MapDirectives.surface`/route-leg/edge type fields were fully removed in a 2026-06-02 follow-up). `caption` was reinstated (in-image export attribution). The guiding rule: `no-` is the single uniform off-switch — no positive opt-in cosmetic flag survives. A wide map in a narrow column (< ~480px effective width) auto-degrades as if zoomed out (prefer abbrev). Hard break, no compat shims (pre-1.0). *(Update 2026-06-02: relief is no longer auto-gated — it renders on every map regardless of data/zoom/width; only `no-relief` hides it. See the relief decision entry.)*
>
> **Add 2026-06-03 (colorize, 12 → 13):** added **`no-colorize`** (the only new directive — a seventh `no-*` opt-out, so the "no positive opt-in cosmetic flag" rule holds). **Distinct political pastel fills are now the default dress for any map without region data** (§24B.2) — named-region maps, POI/route-only maps, and even a bare `map` colour the whole drawn political set, neighbours never sharing a hue (graph 4-colouring over shared map boundaries). The only things that suppress colorize: region **data** (any `heat:`/tag → the gray choropleth/categorical dress, no error) or **`no-colorize`** (plain green-land). A direct trailing colour overrides a single region's pastel as a highlight. Colours are non-semantic (no legend) and extent-independent. *(2026-06-03 follow-up: the trigger was widened from "≥1 region referenced" to "no region data at all" — POI-only and bare maps colorize too; `no-colorize` is the calm-backdrop escape hatch.)* Near-distance adjacency is a v2 seam (below).
>
> **Add 2026-06-04 (no-cluster-pois, 13 → 14):** added **`no-cluster-pois`** (an eighth `no-*` opt-out — the "no positive opt-in cosmetic flag" rule still holds). Coincident POI markers **cluster** by default in the interactive preview: an overlapping stack collapses to a single count badge that fans out on hover/click (the spiderfy controller). A **static export was already always fanned out** (no JS to collapse it), so this directive simply makes the **preview** match the export — every marker drawn at its fanned ring position with legs, no badge, nothing to expand. Rationale: a dense reference map (e.g. all 30 MLB ballparks) reads better fully spread when the author *knows* it's dense and wants the paper look on screen too. Scoped to the document (per-map) rather than only the app's global "Expand bunched POIs" view toggle, so the choice travels with the diagram. No-op for export and for maps with no overlapping markers.
>
> **Add 2026-06-07 (no-cities, 14 → 15):** added **`no-cities`** (a ninth `no-*` opt-out — the "no positive opt-in cosmetic flag" rule still holds). A **subtle, unlabeled gazetteer dot scatter** now draws across the basemap by default (§24B.2) for geographic orientation — muted ink, low opacity, over the basemap and under connectors/POIs/labels. Cities are **population-first** and **spacing-thinned** (fixed minimum pixel gap), so density **adapts to zoom for free** without a knob, and the **only cull is the visible canvas** (projection-agnostic, antimeridian-safe — a lon/lat extent box wrongly blanked albers-usa maps that value AK/HI, since their extent wraps the dateline). Explicit `poi` markers always win — a dot never sits under a referenced marker. Reuses the bundled `gazetteer.json`; international coverage is major-city-only until the gazetteer densifies. Applies **universally** across every projection/context (world, regional, albers-usa).
>
> **Change 2026-06-08 (AK/HI insets always-on national — reverses the 2026-06-02 inference):** the AK/HI insets under `albers-usa` no longer gate on a reference. The **national frame now always draws Alaska + Hawaii** (the canonical AlbersUSA composite), even with no data in them — a US national map silently dropping two states reads as a lie. The empty insets take the map's own dress (colorize pastel, or neutral land under `no-colorize`), so they don't masquerade as data. **Compact/sub-national** US maps (the auto-zoom region/choropleth and fit-to-cluster POI maps, both `mercator`) are unchanged — they frame just their region with no insets. Still **no directive** — content/extent decides; the `no-insets` opt-out stays removed. (Supersedes the §24B.7 cut-log note that insets show "only when the map references Alaska or Hawaii".)
>
> **Add 2026-06-07 (airport IATA codes — no new directives/syntax):** three-letter **IATA airport codes** now resolve as place identifiers (§24B.8) — `poi JFK`, `route JFK -> LAX` — through the existing name-lookup path, with **no new syntax** and **no parser change**. Airports ship in a **separate optional `airports.json` asset** (§24B.11) — large international hubs + all US scheduled-commercial airports, ~38 KB gz, OurAirports (public domain). They never enter the city gazetteer, so the city-dot scatter and reverse-geocode are untouched. **Precedence:** airports are the lowest tier — a token that is both a city and a code resolves to the **city** (the rare overlap is just 2 codes — `ABA`, `UFA`), with a non-blocking shadow hint naming the `as ` coordinates escape. Resolution is **case-insensitive**; airports resolve **by code only** (never by airport name — the name is completion-display only, to disambiguate look-alikes like ORD/ORF). An unknown three-letter token errors as an airport code with the `as ` hint. Editor completion lists airport codes as a **second group below cities** (so ~1500 codes don't bury city names), marked "Airport · ", with optional **scope-aware ranking** (in-region codes sort first; out-of-region still appear — rank, never filter). *Coords are 2-decimal (~1km, sub-pixel at map scale); the size was the only budget lever once OurAirports' `large_airport` tag proved ~3× the initial estimate.*
- **Colorize near-distance adjacency** (v2 seam): v1 colorize derives "neighbours" from **shared map boundaries** only, so regions separated by water (UK/France across the Channel, islands near a mainland) can be assigned the same hue. A future pass could also treat regions within a small **projected-px** distance as adjacent to break those across-water look-alikes. Deferred because projected-px adjacency would make a region's colour depend on the render extent (breaking the v1 extent-independence guarantee + inset/main-frame colour match); re-add as opt-in if it proves to matter.
- **Diverging scale** (seam only — `scale` is no longer a directive; the ramp auto-fits): a future signed-data ramp (`min`/`max`/`center`) for anomalies / partisan lean, re-added only if cross-map comparability demand appears.
- **Subdivision-generic value keys** so US counties / non-US subdivisions slot in.
- **`[Group]` blocs** — grouping subdivisions into named sets + group-to-group edges (reuse §1.8 group brackets).
- **`import:` named POI sets** — reuse a shared library of POIs (offices, stadiums, sites) across diagrams via org's existing `import:`/resolver mechanism (§7). Feeds the same completion dictionary as the gazetteer (private places + public cities). v2 — needs the app/web "my places" / persistence layer (no filesystem in the web editor).
### 24B.13 Non-goals (documented; resist)
POI icons/shapes (v2 — color-only categories in v1) · drive-time/radius/catchment rings (approximate magnitude with `size:`, never a geographic ring) · cartograms / value-weighted geography · CSV/bulk import · density/heat/aggregation/label-declutter · **dense-point maps** (pre-aggregate to sized POIs: `poi Dallas size: 320`) · historical/custom/freehand boundaries (renders MODERN borders — unsuitable for historical extents) · time-animated/phased reveal · inset/small-multiple maps (use separate diagrams) · derived attributes (drive times; **timezone is now a first-class derived channel — `clock`, §24B.14**) · inline value shorthand · tabular data block.
### 24B.14 Local-time cards (`clock`)
A **`clock` control on a POI** floats a live **local-time card** above its marker — the wall clock at that place. It is the map's dynamic channel: it **ticks every second** on interactive surfaces, and CLI/PNG bake a correct point-in-time snapshot.
```
// bare flag — zone auto-derived from the gazetteer
poi Denver clock
// position-tolerant, comma-separated from other keys
poi Los Angeles clock, label: El Segundo
// coord pin — clock: names the zone (required)
poi 39.74 -104.98 as Field clock: America/Denver
// fixed offset (DST-blind)
poi Tokyo clock: UTC+9
// header: availability window (evaluated per-pin, local zone)
hours 9-17
workweek mon-fri
```
- **The zone comes from the place.** A named city already carries its GeoNames IANA `timezone` in the gazetteer (§24B.11), so a **bare `clock` flag** needs no zone typed — `poi Denver clock` derives `America/Denver` correct-by-construction (`Austin` → `America/Chicago` Central, **not** a nearest-zone guess).
- **`clock: `** is the **valued form** — the colon value names the zone explicitly. It is **required for a bare-coordinate pin** (no gazetteer entry to derive from) and may **override** a city's real zone. The value is an **IANA id** (`Asia/Tokyo`) or a **fixed UTC offset** (`clock: UTC+9`, DST-blind — honest but frozen, cf. §37 / decision #43). Bare `clock` vs `clock: ` follows the universal metadata rule (§1.4): a flag with no colon, a key with one.
- **Position-tolerant.** `clock` / `clock:` may sit anywhere in the metadata run and be comma-separated from other keys — `poi Los Angeles clock, label: El Segundo`.
- **Multi-word display name** uses **`label:`** — the map's `as` alias is a single ≤12-char routing token and does not render (§24B.5).
- **Availability window** — two header directives, **`hours -`** and **`workweek `** (`hours 9-17`, `workweek mon-fri`; legacy `days` accepted as an alias), define an open/closed schedule **evaluated per pin in its own local zone**. Each card carries a **status dot**: **green** open · **amber** soon (near a window edge) · **hollow** closed (outside hours, or a weekend/non-listed day).
- **Card contents:** the **local time** (large `HH:MM`, with seconds + am/pm bracketing it), the **status dot**, and the **weekday** — the weekday shows **only when the pin's day differs from the viewer's** (dateline safety, so a card never redundantly restates today).
- **Zero runtime deps** — `Intl.DateTimeFormat` supplies time/offset/weekday and the gazetteer supplies the zone (the same solar math used elsewhere on the map is reused for daylight cues).
- **Warnings** (non-blocking — the pin still renders, just without a card):
| Warning | Cause |
|---|---|
| `W_MAP_CLOCK_TZ_NEEDED` | a bare-coordinate `clock` pin with no derivable zone — add `clock: ` |
| `W_MAP_CLOCK_TZ_OVERRIDE` | a named city's `clock:` contradicts its real gazetteer zone — honored, but flagged |
| `W_MAP_CLOCK_TZ_INVALID` | the `clock:` value is an unparseable zone |
---
## 25. Colon Usage Summary
### Constructs Where Colons Are REQUIRED
| Construct | Diagram Type | Example |
|-----------|-------------|---------|
| Same-line metadata | all metadata-bearing types (wireframe uses a trailing-keyword flag list; flowchart has none; state takes tag-value keys only) | `Foo key: value, key2: value2` |
| Indented metadata (reserved-key) | all metadata-bearing types (same carve-outs as above) | `description: Main gateway` (indented) |
| Class field types | class | `+ name: string` |
| Class method returns | class | `+ sail(): void` |
| Function expressions | function | `f(x): x^2 + 1` |
| Hide tag values | boxes-and-lines | `hide phase:Planning` |
| Sort/lane tag selector | gantt, timeline | `sort tag:Team` |
| Infra node properties | infra | `latency-ms: 50` |
### Colons OPTIONAL
| Construct | Diagram Type | Example |
|-----------|-------------|---------|
| Class relationship label | class | `--|> Vessel : extends` or `--|> Vessel extends` |
| RACI role assignment | raci | `Cap: A` or `Cap A` (role marker lines) |
### Colons NOT USED
| Construct | Diagram Type | Example |
|-----------|-------------|---------|
| Chart type declaration | all | `bar Title` |
| Tag declarations | all | `tag Name as x` |
| Boolean options | all | `no-activations`, `fill-solid`, `no-title` |
| Key-value options | all | `start-date 2026-03-15`, `active-tag Team` |
| Series declarations | data charts | `series A B C` |
| Data rows | bar/line/pie/etc | `Label 100` |
| ER columns | er | `id int pk` |
| Sequence messages | sequence | `A -msg-> B` |
| Groups/containers | all | `[Group Name]` |
| Section dividers | sequence | `== Phase ==` |
| Comments | all | `// comment` |
| Wordcloud data | wordcloud | `swordsmanship 95` |
| Slope data rows | slope | `Blackbeard 40 4` |
| Slope period directive | slope | `period 1715 1725` |
| Venn intersections | venn | `sw + nav Sea Raiders` |
| Wireframe flags | wireframe | `(Submit) primary destructive` |
### Preferred Metadata Form Per Chart Type
Both same-line metadata (§1.4.1) and indented metadata (§1.4.2) are valid wherever metadata is supported, but the spec names ONE canonical form per chart type so generators have a deterministic default. Deviate from the preferred form when a specific situation calls for the alternative.
| Chart type | Preferred form | Rationale |
| --- | --- | --- |
| sequence | Same-line | Participants are declared once with all metadata; no rich indented-child grammar |
| infra | Indented for node properties (`latency-ms: 50`, etc.); same-line for tags | Node properties use `key: value` like standard metadata |
| org | Indented | Org hierarchies are deep and same-line metadata gets long |
| c4 | Indented | Multi-key metadata (description, tech, …) is long |
| er | Same-line | Tables have indented columns; attributes belong on the table declaration line |
| kanban | Same-line | Cards are short; indented slot is for card detail prose |
| sitemap | Same-line | Indented slot is for sub-pages |
| gantt | Same-line | Indented slot is for dependencies (`-> Next`) |
| boxes-and-lines | Same-line | Indented slot is for edges and descriptions |
| timeline | Same-line | Events are flat; no indented child concept |
| pert | Same-line | Indented slot is for dependencies (`-> Next`); reserved-key/tag-alias metadata (`confidence: low`, tag aliases) may also appear there, intermixed with the arrows |
| mindmap | Same-line | Mindmaps are visual trees; indented attributes break scan-readability |
| tech-radar | Same-line | Indented slot is for blips (under quadrants) |
| cycle | Same-line for nodes; indented for descriptions | Description prose is naturally multi-line |
| journey-map | Same-line for steps; indented for annotations (`pain:`/`opportunity:`/etc.) | Existing convention |
| pyramid / ring | Same-line for color; indented body for description | Existing trailing-token color shortcut applies |
| raci | Same-line for phase headers and roles; indented for tasks/role-assignments | Chart-type-specific |
| wireframe | **Trailing-keyword-list** (not key-value metadata) | Flags are positional; indented form is the fallback for collision disambiguation |
| map | Same-line for 1–2 attributes; indented (one per line) for 3+ | §24B's own form rule; directives are colon-free |
| treemap | Same-line | Size is a bare trailing number; `heat:` and tag keys ride the node line |
| swimlane | Same-line | Tag metadata rides node lines/references; indented slot is the lane's nodes and edges |
| event-line | Same-line | Tag values trail the event title; indented slot is description prose |
| version-control | Same-line | `id:`/`tag:`/`type:` ride the commit line; indented slot is commits under a branch |
| block | Same-line, OUTSIDE the bracket | `[Backend] l: Service, span: 2`; indentation is a container's sub-grid |
| sketch | Same-line | `shape:`/`at:`/tags on the shape line; indented slot is edges |
| family | Same-line | GEDCOM keys on the person line; indented slot is children |
| body | Same-line | Tag value trails the part name; indented slot is notes |
| goal | — (no element metadata) | Values are space-separated `key value` directives (`now`, `target`) |
| bracket | Same-line | Tag values on `seed`/roster lines; indented slot is commentary |
| countdown | — (no element metadata) | Space-separated `key value` directives and rule-slot lines only |
| clock | — (no element metadata) | Flat entries — anchor + `as ` + trailing color; directives space-separated |
### The Rule
A colon binds a value, and it appears in exactly **four syntactic positions** — disambiguated by
position, not by spelling:
1. **Metadata assignment** — `key: value` in same-line or indented metadata (per §1.4), registry-gated. The general case.
2. **Type / expression separation** — where both sides may contain spaces so a delimiter is required: class field types (`+ name: string`), class method returns (`+ sail(): void`, colon optional), function expressions (`f(x): x^2 + 1`).
3. **Tag-value selector** in a directive — `hide phase:Planning` (boxes-and-lines) and `sort tag:Team` (gantt/timeline, the back-compat spelling of `lane-by`): a filter/axis predicate, not assignment.
4. **Role assignment** — `Cap: A` (raci), colon optional.
**The one true carve-out: ER columns** (`id int pk`) are an indented typed-property list like
infra node properties, yet ER is space-separated while infra requires the colon (`latency-ms: 50`).
This is deliberate — ER follows SQL DDL muscle memory; infra follows metadata convention. ER is
the single exception to memorize.
**Colons never appear in:**
- Directives and options — space-separated (`start-date 2026-03-15`, `x-label Low, High`, `workweek mon-fri`)
- Tag declarations and chart type declarations
- Series declarations and data rows for simple/data charts (space-delimited — incl. sankey/arc links `Source -> Target value` and quadrant data). Space-separated is canonical, but commas in data-row values are **accepted** — thousands grouping (`Revenue 1,000`) and separator commas (`Q1 400, 700`) both parse; decision #28 reversed the short-lived comma freeze
- Structural syntax (groups, sections, arrows, comments)
- Wireframe flag lists (space-separated keywords from the closed flag enum)
- Flowchart node labels — colons are literal label text (`Idle: Waiting` is a node named "Idle: Waiting"); flowchart has no metadata. State node labels read the same way, but state does take tag-value keys as metadata (decision #48)
---
## 26. Authoring Rules (Generators Read This First)
A consolidated checklist of constraints that span chart types. Generated DGMO that violates any of these will emit warnings or errors. **If you are an LLM generating DGMO, read this section before writing.**
### 26.1 Declare Before Reference
Every entity referenced by an edge, group-edge, or arrow target must be declared *before* the reference, either on a prior line or by being declared inline at the reference site (in chart types that support inline forward declarations, e.g. pert's `-> name 1 2 4`).
- ❌ `Home` → `-login-> Login` (with no `Login` page declared anywhere) → `Arrow target "Login" not found` (sitemap)
- ❌ `[Backend] -> [Frontend]` with no `[Frontend]` group declared → `Group '[Frontend]' not found` (boxes-and-lines)
- ❌ `lp` → `-> celebrate` with `celebrate` never declared → `Unknown activity 'celebrate'` (pert)
- ✅ Declare the target first, then reference it; OR use a chart-type-specific inline forward declaration that includes the required schema (pert durations, etc.).
### 26.2 Don't Declare Twice — Combine Metadata + Edges
When a node has both metadata and outgoing edges, put same-line metadata on the declaration line and indent the edges below it. Splitting into two declarations triggers a `Duplicate node` warning.
```
// ❌ Duplicate-node warning
API description: Main gateway
…
API
-routes-> UserService
// ✅ Combined
API description: Main gateway
-routes-> UserService
```
### 26.3 Chart-Type Scope of Universal-Looking Features
Some constructs *look* universal but are scoped to specific chart types. Don't transplant them across charts.
| Construct | Scope |
| --- | --- |
| Bare `collapsed` flag (legacy `collapsed: true`) | see §1.8's authoritative list |
| Same-line / indented metadata on element declarations | all chart types except **flowchart and data charts** (state takes tag-value keys only, decision #48); see §1.4 reserved-key registry |
| Trailing-keyword flag list | wireframe only (§18.5) |
| `progress: ` key | gantt only (§13.8) |
| `score: ` + `emotion: ` keys | journey-map only (§22) |
| `description: ` shorthand for layers | pyramid, ring (§23, §24) |
| `milestone ` keyword | **removed** — use ` 0` (§13A.6) |
| `columns` | Two different constructs sharing a word — heatmap: an indented column-label block (§15 Rule B); block: a `columns N` grid-count directive (§30.2). Don't conflate them |
| `tag: ` commit key | version-control only (§29.3) — a git ref pill, NOT a §1.3 tag group |
| `|` operator as metadata delimiter | **removed** — see §1.4. Surviving uses of `|`: wireframe dropdown options `{A | B}`, in-arrow label characters `A -file|name-> B`, quoted name characters `"Order | Items"`. |
### 26.4 Quoted-Name Combination Limits
Quoted names cannot combine with `as ` on the same line (§2.2a). If the only reason for quoting is multiple words, drop the quotes — bare names already accept spaces. Reserve quoting for names that contain genuinely reserved characters (`|`, `:`).
### 26.5 Sequence Participants Without `is a TYPE`
Standalone sequence participant declarations (no `is a `) accept only the bare-name-plus-optional-metadata form. If you need an alias or quoted name on a sequence participant, declare it with `is a ` (e.g. `is a person`). See §2.2b.
### 26.6 Spec / Parser Drift Watch
The following keywords or forms are documented elsewhere in the spec or in older content but are **not currently supported** by parsers. Do not emit them:
- `milestone ` (PERT) — replaced by ` 0`
- Inline forward-declaration of pert edge targets (`-> name 1 2 4`) — declare the target on its own source line first, then reference by name
- `"Quoted Name" as alias` (any chart type) — drop the quotes or drop the alias
- Standalone sequence participant `Name as a` (with metadata) without `is a TYPE` — use the typed form
- The `|` operator as metadata delimiter — removed in 0.18.0; its `E_PIPE_OPERATOR_REMOVED` guard was dropped in 0.43.0 (#28), so a stray `|` now falls through as a literal rather than erroring. Use same-line or indented metadata per §1.4.
### 26.7 Diagnostic-Free Checklist
Before considering DGMO output complete, mentally verify:
1. Every edge target appears as a declaration on a prior line (or as an inline forward declaration with required schema).
2. No entity is declared more than once with conflicting metadata.
3. Metadata uses the canonical form per §1.4 — same-line `key: value, ...` after the name region, or indented `key: value` for reserved keys. No `|` delimiter anywhere except wireframe dropdowns, in-arrow label characters, and quoted name characters.
4. Wireframe flags are written as space-separated lowercase trailing keywords from the closed 16-flag enum, or indented one level under the element.
5. Journey-map steps use `score: N, emotion: Word`; gantt tasks use `progress: N`; pyramid/ring layers use `description: ` (quote when the value contains commas).
6. Groups start folded via a bare trailing `collapsed` flag (§1.8; legacy `collapsed: true` accepted).
7. Quoted names don't carry `as ` on the same line.
8. Sequence participants with alias or quoted names use `is a `.
9. No `milestone` keyword in pert — use ` 0`.
10. Tag declarations appear before the first non-tag content line (§1.3).
### 26.8 Enumerated Options Document Their Defaults
Every enumerated option documents its default. If a chart section lists an option with enum values, the default value is stated inline — spec PRs that add an enum without a default are incomplete (decision #48).
---
## 27. Swimlane Diagrams
Cross-functional / BPMN-flavored process diagrams: actors or systems are **lanes**, the process flows along the flow axis with branches, gateways, terminals, and loops, and optional `[Phase]` columns group steps into stages.
### 27.1 Chart Type Declaration
```
swimlane [Title]
```
Space-separated, no colon; title optional. Detection is by the first-line token (`swimlane`).
### 27.2 Direction
`direction-lr` / `direction-tb` (booleans, §1.9; default is LR — lanes as horizontal bands with the flow running left-to-right). `direction-tb` transposes: lanes become vertical columns and phases horizontal bands. The key+value forms `direction LR` / `direction TB` are accepted legacy.
### 27.3 Lanes (blocks that own their edges)
```
lane [color]
```
A `lane` line declares a row (occupant-neutral — person, system, or org) **and opens that lane's block**: the indented lines beneath it are the lane's nodes _and their outgoing edges, written inline_. There is **no separate flow block** — a node and its wiring are written once, together, under the lane that owns it. The trailing color follows the §1.5 trailing-token rule (one of the 11 recognized names); it tints the lane band and is the default fill for the lane's nodes. A lane with no color is auto-assigned a distinct categorical color. Lane names may contain spaces (`lane Claims Agent blue`). Lane order is first-appearance order; re-opening a lane later (or under another phase) resumes it.
### 27.4 Structure & ownership
- **A node is owned by the lane where it appears as a line-head** — the leftmost token of a bare node line or of an inline edge (`File Expense -> …`). Everywhere else the same name is only a **reference**.
- **Edges are inline.** An arrow line's head declares its node in the current lane; its targets are references, resolved after the whole diagram is read, so an edge may **forward-reference** a node declared later (or in another lane).
- **Unresolved target — delimited auto-creates, bare errors.** A target that resolves nowhere is created as a leaf **only if it is delimited** — a terminal `(…)`, gateway `<…>`, or subprocess `[[…]]` (an intentional endpoint), and it takes the lane + phase of the edge that referenced it. An unresolved **bare task** is an `E_SWIMLANE_UNKNOWN_NODE` error, so a typo'd target name is caught rather than silently spawning a phantom node. To place a task in a specific lane, give it a line-head there (a bare node line or an outgoing edge).
- **Lane-scoped names.** Two lanes may each hold a node of the same name. A bare reference resolves **in its own lane first**, then across all lanes (a unique match resolves; more than one is ambiguous — `E_SWIMLANE_AMBIGUOUS_NODE`). A **`Lane.Node` qualifier** targets one lane explicitly (`Harbor.Inspect`) and is how a cross-lane edge disambiguates a shared name.
- `[Phase]` (bracketed, at indent 0) opens a **phase column**; `lane` blocks indent under it (3-deep: phase ▸ lane ▸ nodes). The `[...]`-heading-over-children idiom matches flowchart `[Group]` / journey-map `[Phase]`; a bracketed leaf is still a process node.
- Phase membership can only push a node **right** (`finalRank = max(baseRank, phaseFloor[phase])`), never pull it left of its column; a backward edge across phases becomes a routed back-edge.
### 27.5 Node Tokens (v1, closed vocabulary)
| Token | Shape / meaning |
| ----- | --------------- |
| `Submit Claim` | task (bare) |
| `` | exclusive (XOR) gateway |
| `<+ Fork>` | parallel (AND) gateway |
| `(Start)` | terminal (neutral) |
| `(!Rejected)` | error terminal (`!` prefix → red) |
| `(Paid) success` | typed terminal — trailing word `success` (→ green) or `terminate` |
| `[[Inspect Property]]` | subprocess (double border) |
Delimiters are optional sugar in a flow reference — `Validate` ≡ `` — but echoing them keeps the source self-documenting. A terminal is implied by position: a node with no inbound edge is the start, no outbound is the end; `(…)` only forces the event circle.
### 27.6 Edges (inline, inside lane blocks)
In-arrow labels per §1.10 — `A -invalid-> B`, never a trailing word. Chains inline (`A -> B -> C`). When a source has several branches, write the source on its own line and **indent** the arrow lines beneath it; every indented arrow fans out from that header (not from the previous line's tail). A back-edge to an earlier-ranked node draws a routed loop (dashed). A cross-lane edge is authored in the **source** lane; its target resolves by name (§27.4). Same-line tag metadata (`Maroon r: high`, §1.4.1) may ride on either a node's declaration or a reference — on a reference it applies to the resolved node.
### 27.7 Color Cascade
First match wins: active **tag** value (`r: high` → the tag's color) → **event / symbol** type (`(!x)`→red, `(x) success`→green, gateways neutral) → **lane** shade. A `tag` group is an optional second dimension that deliberately overrides the lane fill.
### 27.8 Reserved Seams (fast-follow — rejected, not silently dropped)
The following each emit an `E_SWIMLANE_UNSUPPORTED` diagnostic in v1, so promoting one later is one glyph + one key:
| Token | Diagnostic |
| ----- | ---------- |
| `` inclusive gateway | inclusive gateway is fast-follow |
| `<* …>` event-based gateway | event-based gateway is fast-follow |
| `timer: …` (and `message:` / `signal:`) | timer/message/signal events are fast-follow |
| `note: …` | notes are deferred past v1 |
| `data: …` | data objects are deferred past v1 |
| `A ~> B` (message flow) | message flow is fast-follow |
### 27.9 Diagnostics
| Code | When |
| ---- | ---- |
| `E_SWIMLANE_DUPLICATE_NODE` | a lane declares the same node name on two bare lines |
| `E_SWIMLANE_UNKNOWN_NODE` | an edge references an undeclared **bare task** (only delimited endpoints auto-create), or a `Lane.Node` qualifier resolves nowhere |
| `E_SWIMLANE_AMBIGUOUS_NODE` | warning — a bare reference matches nodes in more than one lane; qualify it `Lane.Node` (resolved to the first match otherwise) |
| `E_SWIMLANE_UNKNOWN_LANE` | a node appears outside any lane block |
| `E_SWIMLANE_NO_LANES` | a `[Phase]` is declared but no lanes exist |
| `E_SWIMLANE_UNSUPPORTED` | a fast-follow token (§27.8) |
### 27.10 Complete Example
```
swimlane Weekly Publishing
direction-lr
lane Writer gray
Draft Post -> Review
Revise -> Review
lane Editor blue
-changes-> Revise
-ok-> Schedule
Schedule -> Publish -> Promote
lane Social green
Promote
```
Each lane block declares its nodes and their outgoing edges inline; `Review` (Editor) and `Promote` (Social) are forward references from the Writer/Editor blocks, and the `` gateway fans its two branches with in-arrow labels. No separate flow block.
## 28. Event Line Diagrams
> Event dates follow the liberal grammar + year ladder in [§2B Universal Date Handling](#universal-date-handling).
An annotated **event line**: a horizontal spine of events, each a dot with a vertical leader line to an org-style card (title, a muted date subtitle, divider, description) that auto-alternates above and below; a faint year/period tick ruler on the spine gives global orientation. The narrative/infographic timeline ("Super Bowl halftime shows", "a history of the web"). Distinct from `timeline` (§15): `timeline` is a to-scale date axis with eras, markers, and range bars; `event-line` is point events with rich prose, optionally not to scale. Every construct reuses an existing idiom — there is no net-new syntax.
### 28.1 Chart Type Declaration
```
event-line [Title]
```
Space-separated, no colon; title optional. Detection is by the first-line token (`event-line`).
### 28.2 Events
```
2012-02-05 XLVI g: Pop
```
A bare line that isn't a directive or `tag` block is an **event**, in source order. An event at **indent 0** sits outside any era; **indented beneath a `[Name]` bracket** it belongs to that era (§28.6a). The line is, in order: an **optional leading ISO date** (line-prefix, exactly like `timeline` §15.2 — `1718-05 Blockades Charleston`), the **title** (spaces allowed), then **optional trailing same-line metadata** (`§1.4.1`) — tag values and nothing else in v1. There is no `event` keyword and no `date:` key (a date key exists in no chart type; date-primary charts carry the date as a line-prefix).
### 28.3 Dates and scale
- **Formats:** the liberal grammar + year ladder of [§2B Universal Date Handling](#universal-date-handling) (`YYYY`, `YYYY-MM`, `YYYY-MM-DD`, slash/locale forms, optionally `… HH:MM` or `… HH:MM:SS`), plus BCE/BC/CE/AD year suffixes (`753 BCE`). The date renders as a muted subtitle **inside the event's card**, directly adjacent to the title on the side away from the spine — so the date travels with its event instead of sitting on the axis (which produced confusing opposite-side captions and diagonal connectors in dense runs). It is **formatted for reading** (`2008-09-02` → `Sep 2, 2008`; `753 BCE` → `753 BCE`), matching `timeline`'s axis labels.
- **To scale (default):** dots are positioned along the spine by date (linear scale). Two events with the same date share an x — the dots stay coincident (they are simultaneous) and the cards fan into stacked lanes. The spine itself is bare except for a **faint year/period tick ruler** that restores global orientation now that exact dates live in the cards; ticks are computed per to-scale run (a collapsed-era capsule breaks the axis, §28.6) and de-cluttered so labels never overlap. **Crowding moves the cards, never the dots.**
- **`no-scale`:** position events evenly in source order; the date subtitles still print but the ruler is omitted (no meaningful axis). If `scale` is on (default) but some/all dates are absent or unparseable, the renderer falls back to even spacing rather than erroring.
- **`TBD` (future events):** a `TBD` date line-prefix (case-insensitive — `TBD First Crewed Landing`) marks a **not-yet-scheduled future event**. Its caption reads `TBD` and it draws a **hollow dot + a faded leader** (in `no-box`, a faded shelf edge) to distinguish it from events that have happened. To scale, its position is **inferred from its source-order dated neighbors**: a `TBD` with a dated event after it is **interpolated into that gap**; a **trailing** `TBD` (nothing dated after it) parks just past the latest date and the spine **trails off dashed** — the open horizon. Multiple `TBD`s sharing a gap fan evenly across it. It does **not** trip the bad-date or out-of-order warnings. With no real dates to anchor against, `TBD` events simply ride along in source order under even spacing.
### 28.4 Description
The **body indented one level deeper than its event** is the event's description — bare lines, no keyword (the `pyramid` §23.3 / `ring` §24.3 / journey-map-persona idiom). The only structural nesting in an event-line is the era → event → description ladder (§28.6a), so role is unambiguous from indent depth alone: a line deeper than its event is prose, never a child node, and the disambiguating `description:` key that branching charts require is unnecessary. A line beginning `- ` is a **bullet**; inline markdown (`**bold**`, `*italic*`, `` `code` ``, `[links](url)`) renders per §1 inline-markdown. Lines wrap to the card width; very long descriptions truncate with an ellipsis.
### 28.5 Tags and Color
A `tag as ` block (§1.3) declared before the first event; each event applies a value as trailing metadata (`… XLVI g: Pop`), exactly like `timeline`'s `1718-05 Blockades p: Blackbeard`. The tag color tints the **dot, leader line, and card top-rule**. Topology is flat, so there is **no color cascade** — each event colors only itself. Named colors only (§1.5); a value with no explicit color auto-assigns a categorical color. The legend uses the shared legend framework.
**Default value.** A group's **first value is its implicit default** — an event that specifies no value for the group belongs to that default category, exactly as in `org`/`boxes-and-lines`/`pert`. The default is materialized into the event's metadata, so it drives color *and* legend focus identically: hovering the default legend entry lights every untagged event. Declaring a value that no event uses is legal (it just shows in the legend); hovering it is a no-op (nothing to focus).
**Hover (preview only).** The renderer wires self-contained hover affordances that are inert in the static export. Hovering focuses a subject and **dims everything else**, while the spine, the year ruler, the title, and the legend always stay lit: hovering any part of an event (dot, leader, or card) glows that event and fades the rest; hovering an era bracket keeps that era's bracket + member events lit and fades the rest — and hovering a **collapsed era's summary card** focuses it the same way, so its `⊓` bracket and the axis-break squiggle stay lit with the card rather than fading; hovering a legend entry focuses that tag value (dimming events that lack it) — a value with **no matching events is a no-op** rather than an all-dim void. All of it is renderer-owned (one delegated handler on the SVG root) so it works identically in the app, web editor, and Obsidian, and re-binds on every render.
**Legend click — collapse a category to dots (preview only).** Clicking a legend entry **mutes that tag value**: its event cards (and their leaders) collapse, leaving the **bare dots on the spine in their tag color** — the events stay on the timeline (and the to-scale ruler is untouched), they just stop occupying card space, so a busy chart quiets down to the categories you care about. The dots remain fully interactive: **hovering a collapsed event's dot transiently re-reveals its card** (the same glow-the-event focus), so nothing is hidden, only quieted. A muted value is also **dropped from a collapsed era's summary bullets** (§28.6a) — so de-selecting a category quiets it whether its events are expanded or folded inside a collapsed era, and the summary card shrinks to its surviving members. The muted legend entry renders **struck-through with a hollow swatch** so the state is self-documenting; click it again to restore. The muted set is **per-diagram view state that persists across the live era collapse/expand toggles** (and resize / palette changes) — it is reset only when the source itself changes, the same contract as the live era toggles. This is **preview-only** — like the era live-collapse and the map's `active-tag` click it never edits the source, and the static SVG/PNG export always carries the full set. (Persisting or exporting this view state is a reserved fast-follow.)
**Code ↔ diagram sync (app).** In the desktop app, the renderer exposes `focusEventLine(container, target)` so the editor can *pin* a focus: moving the cursor onto an event's line (its date line or any of its description lines), an era header line, or a tag-block entry line focuses the matching subject in the diagram (same dim-the-rest visual). Hovering temporarily overrides the pin; moving the mouse off reverts to it. The pin lives on the SVG (a `data-evt-pin` attribute) so hover and cursor focus share one consistent state.
### 28.6 Directives
Bare flag lines (indent 0), the `no-*` convention (cf. map `no-colorize`, treemap `no-percent`):
| Directive | Effect |
| --------- | ------ |
| `no-scale` | space events evenly instead of by date (date subtitles still print; the spine ruler is omitted) |
| `side above` / `side below` | place all cards on one side instead of alternating; `side alternate` is the default (omit it). Keyword+value form |
| `no-box` | card-less style for slides — a tag-colored label + muted date on a soft tag-tinted "shelf" (no full box/border), with the description below. The shelf's colored spine-side edge is the leader's landing pad, keeping the dot→block link solid in dense charts; the title sits nearest the spine |
| `no-legend` | hide the tag legend |
| `now` / `now ` | plant a "now" pin on the spine (§28.6b) — at today (bare `now`, resolved at render time) or a pinned ISO date; optional trailing custom label. To-scale only |
### 28.6a Eras
Group a run of events into a labeled section of the spine with a `[Name]` bracket — the universal `[…]` container sigil (§1.8) — and **indent its member events beneath it**, the same indentation nesting used by org (§7) and version-control (§29). An event belongs to the era it is indented under; its description sits one level deeper still (§28.4). An event at **indent 0 sits outside any era** (fully supported — it simply has no era), and dedenting back to indent 0 ends the open era. (Indent-nested rather than run-delimited *because* every other collapsible `[…]` container in the language indents its children — consistency and the collapse affordance win over the marginal redundancy of indenting an already-ordered run; reverses the original §28.6a run-delimiter decision, decision #16.)
```
event-line A History of the Web
no-scale
[The Early Web]
1991 WorldWideWeb t: Protocol
Tim Berners-Lee publishes the first site at CERN.
1993 Mosaic t: Browser
[The App Era] collapsed
2005 Ajax t: Platform
```
- **Visual:** an era renders as a horizontal `]` bracket on the side **opposite** the cards — under `side above`/`side below` it takes the open side; alternating places it below. The era name is the label, rendered **verbatim** (casing is the author's, never forced). Tags still color the dots independently — an era is positional, not categorical.
- **Trailing metadata** after `]`: an optional bare `collapsed` flag (§1.8; legacy `collapsed: true` accepted) and/or a named color (`[The 1960s] collapsed blue`). Named colors only; a hex value is rejected (§1.5) and leaves the era neutral.
- **`collapsed`** folds the run into a single event-like **summary card** — the era name as the title and a bulleted list of **all** its member events (date + title) as the body — while a `⊓` bracket stays on the spine so the era never leaves the timeline. This is the **authored / export / CLI** default; in the app a reader toggles it live (preview only — it does not edit the source), the same interactive-vs-export split as map/treemap (§24C)/sequence (§2.2). There are **no expand/collapse chevrons or triangles** — the summary card and its bracket are the affordance.
- **Collapse under scale is a broken axis**: a collapsed run is **compressed to a fixed-width capsule** on the spine (it does *not* consume its real date range), and an **axis-break glyph** (a 90°-rotated `≈`, the spine blanked between its two waves) bisects the capsule to signal "lots of timeline folded here, not to scale." The width this frees is handed to the **expanded** events, which draw to scale among themselves — so collapsing the dense eras lets the era you're focused on take over the line. Dead time (gaps with no expanded events) costs only a small fixed gap, never width. Even spacing applies only to explicit `no-scale` or when a slot genuinely lacks a date.
- **A collapsed era's summary card centers on its own capsule**, so several folded eras **cascade** left-to-right with their capsule spacing (a later era's card sits visibly right of an earlier one) rather than all pinning to the canvas edge and reading as simultaneous — spatial order tracks chronological order. (Only the leftmost card clamps to the edge when its capsule sits too near it to center.)
### 28.6b The `now` marker
A **"grounded pin"** at "today": a small diamond planted on the spine, a short stem, and a labeled tab (default label `now`). It answers "where are we on this timeline right now" without the author hand-placing an event.
```
event-line Product Roadmap
now ← computed: resolves to the render-time date
now 2023-06-01 ← pinned: an explicit ISO date (deterministic)
now 2023-06-01 Today ← pinned + a custom pill label
2022-01 Kickoff
2023-03 GA
2024-01 v2
```
- **Two modes.** Bare `now` is **computed** — resolved to the current date each time the diagram renders (it moves as time passes). `now ` **pins** the mark to an explicit ISO date; deterministic and snapshot-safe. A trailing token after the pinned date overrides the `now` caption (`now 2023-06-01 Today`).
- **Grounded pin + collision avoidance.** The diamond marks the exact spine point; the labeled tab is centered in the spine→event-row gap and auto-slots into a **card-free lane** — the gap above or below, else just past the nearest overlapping card — so it never overlaps a card. Colored palette-native **red** (`palette.destructive`) — the Gantt "today-line" convention, on-palette in every palette and distinct from the tag hues — and it honors the §1.9 fill family on the tab (`fill-solid`/`fill-outline`/tint) like the event cards. Drawn last, so nothing hides it.
- **Dotted "today line".** A thin **dotted** vertical rule in the same red is grounded at the spine. At rest it **dies quickly** — faded to nothing within the spine→card leader gap (a vertical gradient stroke), so it never paints over an event card. resvg-safe (`linearGradient` + per-stop `stop-opacity`), so the resting rule survives PNG/SVG export. **Hover** (preview only) reveals a full-height dotted rule that rides above the cards while the pointer is on the marker and vanishes when it leaves; static exports skip it.
- **To-scale only.** The rule rides the date axis, so it is drawn only when the timeline is to scale (every event dated). Under `no-scale` there is no date position to anchor it — the directive is ignored and emits an `E_EVENT_LINE_UNSUPPORTED` warning.
- **Position** is interpolated over the placed **dated** events, so it honors the broken axis (collapsed-era capsules, per-run scaling) for free. A `now` earlier than the first event clamps to it. A `now` later than the last dated event clamps to that last dot — **unless** the spine trails off into a dashed open horizon (a trailing `TBD`, §28.4), in which case the rule **rides out onto that tail** ("now, in the unscheduled-future era"), anchored at the nearest trailing-`TBD` dot.
- **TBD interaction**: `now` interpolates over real dated events only — `TBD` positions are inferred (§28.4), so they never anchor the rule. A *bracketed* `TBD` (dated event after it) is simply interpolated across; a *trailing* `TBD` opens the horizon the rule can ride onto (above).
- **Last one wins** if `now` is repeated. A `now` value that isn't a valid date emits `E_EVENT_LINE_BAD_DATE` and no rule is drawn.
### 28.7 Reserved Seams (fast-follow — rejected, not silently dropped)
Each emits `E_EVENT_LINE_UNSUPPORTED` in v1:
| Token | Diagnostic |
| ----- | ---------- |
| `direction-tb` | vertical orientation is a fast-follow; `direction-lr` (boolean, §1.9) is the only supported mode — horizontal — and the default. `direction-tb` is a reserved seam and emits the unsupported-direction diagnostic. Key+value `direction LR` accepted legacy |
| `section ` | superseded by the `[Name]` era delimiter (§28.6a); the diagnostic points there |
| a trailing `^` / `v` on an event | per-event side override is fast-follow |
### 28.8 Diagnostics
| Code | When |
| ---- | ---- |
| `E_EVENT_LINE_NO_EVENTS` | declaration with no event lines |
| `E_EVENT_LINE_BAD_DATE` | a date-looking prefix that isn't valid ISO |
| `E_EVENT_LINE_UNSUPPORTED` | a fast-follow token (§28.7) |
| `E_EVENT_LINE_ERA_DATE_ORDER` | warning — an event is dated outside its era's chronological run (dated after a later era begins, or before an earlier era ends), which makes the era brackets overlap (§28.6a). Only checked when the date scale is active. |
### 28.9 Complete Example
```
event-line Super Bowl Halftime Shows
tag Genre as g
Pop blue
Rock red
R&B teal
2011-02-06 XLV g: Pop
**The Black Eyed Peas**, Usher, and Slash. High-octane EDM staging.
2012-02-05 XLVI g: Pop
**Madonna** with LMFAO, Nicki Minaj, M.I.A, and Cee Lo Green.
- Greek-temple set, gladiators
- Marching-band finale
2013-02-03 XLVII g: R&B
**Beyoncé** reunites Destiny's Child under a stadium light rig.
```
## 29. Version-Control Diagrams
A **version-control** graph: a commit DAG drawn as parallel branch lanes (the git / Mercurial / SVN branch-and-merge picture). Deliberately **VCS-agnostic** — not `git-graph` — because commits, branches, and merges are universal. The grammar is **keyword-less**: a bare top-level line is a branch, a bare indented line is a commit (the org / `event-line` indentation idiom). Only `merge` and `cherry-pick` are required verbs. At parity with Mermaid `gitGraph` and beyond it (HEAD / remote-tracking / ahead-behind, the `rebase`/`reset`/`revert`/squash operations, step notes). Visual language is "metro map" — thick rounded colored lanes, big dots, ref-pill badges, diagonal commit labels.
### 29.1 Chart Type Declaration
```
version-control [Title]
```
Space-separated, no colon; title optional. Detection is by the first-line token (`version-control`).
### 29.2 Branches (bare top-level line — no `branch` keyword)
A bare line at **indent 0** (that isn't a directive or an operation verb §29.7/§29.8) is a **branch**:
```
develop from main
```
- **Name** — the leading token (slashes allowed: `feature/login`). The **first** top-level branch (or any commit that appears before one) is the **trunk**.
- **`from `** — sets the branch point: the parent branch's **current tip**, or a specific commit (by message or `id:`). Omitted on the trunk.
- **Re-naming an existing branch resumes it** (appends following commits) — this is the *only* mechanism for what git calls `checkout`/`switch`; there is no checkout keyword. Re-opening a branch later in the file places its new commits **later in time** (temporal interleave).
- **Trailing color token** — `hotfix red` (§1.5 named colors only; the venn/wardley trailing-token idiom). Omitted → auto-assigned by lane order. The branch name is labeled **in the gutter, aligned to its lane** (no legend).
- **`order: N`** — optional same-line metadata fixing the branch's lane order (default = declaration order).
### 29.3 Commits (bare indented line — no `commit` keyword)
A bare **indented** line is a **commit**; its text *is* the message, in source order on the active branch:
```
develop from main
Set up CI
Add test suite tag: ci-green
```
- **No `commit` keyword.** It survives **only** as the opt-in form for an empty/dotless commit (`commit` alone → a node with no message).
- **Same-line metadata (§1.4.1):**
- `id: ` — shows that short SHA on the commit (e.g., a real hash). **No `id:` → no SHA is drawn**, so the source maps 1:1 to the render (no invented hashes). An `auto-id` mode that hashes every commit is a reserved seam (§29.11).
- `tag: ` — a release/ref marker, rendered as a **colored ref-pill badge** on the commit (also valid on a `merge`). Version-control charts do NOT take §1.3 tag groups — `tag:` here is a git ref pill, a different concept (§26.3).
- `type: normal | highlight | reverse` — `normal` = filled dot (default); `highlight` = filled square; `reverse` = hollow crossed dot (a backed-out commit).
### 29.4 Merge and Cherry-Pick (the two required verbs)
Indented, inside the active (target) branch:
```
develop
merge feature/login tag: v1.0.0
cherry-pick e90d5f3
```
- **`merge [id:] [tag:] [type:] [squash|ff|no-ff]`** — joins ``'s tip into the active lane (a ringed merge node + a rounded-elbow connector). Strategy flag (§29.8): `squash` = one absorbing commit (source commits ghost); `ff` = fast-forward (no merge node); `no-ff` = force a merge node.
- **`cherry-pick [parent:]`** — a dashed copy-edge from the referenced commit onto the active lane. `parent: [` is required when the source is a merge commit (Mermaid parity).
### 29.5 Color
Branch lanes are palette-colored (auto by lane order, or the §29.2 trailing token). Named colors only (§1.5); hex/CSS colors are rejected (§1.5 enforcement). A commit, its leader, and its message take the branch color.
### 29.6 Orientation and Display Directives
Bare flag lines (indent 0):
| Directive | Effect |
| --------- | ------ |
| `direction-lr` | horizontal lanes, newest right (**default**, the iconic look); boolean per §1.9 (key+value `direction LR` accepted legacy) |
| `direction-tb` | column lanes, newest down (the `git log` view); boolean per §1.9 (key+value `direction TB` accepted legacy) |
| `no-labels` | hide commit messages (Mermaid `showCommitLabel: false`) |
| `no-lanes` | hide branch lane lines (Mermaid `showBranches: false`) |
| `no-head` | suppress the auto HEAD marker (§29.7) |
### 29.7 HEAD, Remote-Tracking & Ahead/Behind (one verb: `ref`)
```
ref origin/main at Add README
ref HEAD at Login form
```
- **`ref ] at [remote]`** — drops a labeled pointer pill at an existing commit (by message or `id:`). One mechanism covers remote-tracking branches, extra tags, and detached HEAD.
- **Remote-tracking** — a ref whose name is `origin/…` / `upstream/…` (or carries the trailing `remote` flag) renders **ghosted** (dashed pill). When a local branch `X` and a `origin/X` ref both exist, the renderer auto-labels **ahead/behind** (`↑2 ↓1`) from DAG distance. Opt-out reserved (`no-ahead-behind`).
- **HEAD** auto-sits as a badge on the **active** (last-named) branch's tip; `ref HEAD at ` forces a **detached** HEAD; `no-head` suppresses it.
### 29.8 Operation Verbs (`rebase` / `reset` / `revert` / squash)
The operations Mermaid cannot represent. `rebase`/`reset` are **top-level** (they move a whole branch); `revert` and the merge `squash`/`ff`/`no-ff` flags are **inside the active branch**.
```
rebase feature onto main
reset main to Add README
main
revert Login form
merge feature squash
```
- **`rebase onto `** — replays ``'s post-divergence commits onto ``'s tip; the originals render as faded **ghost** commits and the solid copies are linked by dashed move-arrows.
- **`reset to `** — moves the branch pointer back; commits after the target become faded ghosts (orphaned).
- **`revert `** — adds an inverse commit (`type: reverse` styling, message `Revert `) with a dashed link to the reverted commit.
- A faded/dashed **ghost commit** is the single shared primitive behind rebase, reset, and squash.
### 29.9 Step Notes
```
main
Bootstrap
note Trunk starts here
```
- **`note `** — an indented line anchored to the active tip; renders a small **auto-numbered** ① badge on that commit plus a caption legend. Auto-numbered in source order. Purely additive — it never affects the graph topology.
### 29.10 Reference Resolution & Caveats
- A **commit reference** (in `from` / `merge` / `cherry-pick` / `ref` / `reset` / `revert`) is a commit **message or `id:`**, resolved to the **most recent** match; duplicate messages require an explicit `id:` (else `E_VERSION_CONTROL_AMBIGUOUS_REF`).
- **Caveat:** a commit message that *literally begins* with a verb word (`merge `, `cherry-pick `, `revert `, `note `, `commit `) or a metadata key (`id:`, `tag:`, `type:`) is parsed as that construct. Use the explicit `commit ` prefix or quote it.
### 29.11 Reserved Seams (fast-follow — rejected, not silently dropped)
Each emits `E_VERSION_CONTROL_UNSUPPORTED` in v1:
| Token | Diagnostic |
| ----- | ---------- |
| `auto-id` | hash + display every commit (the dense `git log` look) is fast-follow |
| `stash` / conflict / PR-overlay / diffstat | tier-3 git features are fast-follow |
| a stateful `checkout` / `switch` keyword | the linear Mermaid-mirror mode is fast-follow |
**Repo import** (`git log` → `version-control` source) is a tooling fast-follow, not syntax.
### 29.12 Diagnostics
| Code | When |
| ---- | ---- |
| `E_VERSION_CONTROL_NO_COMMITS` | declaration with no commits |
| `E_VERSION_CONTROL_UNKNOWN_BRANCH` | `from`/`merge`/`rebase`/`reset` names a branch with no tip |
| `E_VERSION_CONTROL_AMBIGUOUS_REF` | a commit ref matches more than one commit (require `id:`) |
| `E_VERSION_CONTROL_CYCLE` | a cycle in the commit DAG (self-merge / bad `from`) |
| `E_VERSION_CONTROL_UNSUPPORTED` | a fast-follow token (§29.11) |
### 29.13 Complete Example
```
version-control Feature Branch Workflow
main
Initial commit
Add README
develop from main
Set up CI
Add test suite
feature/login from develop
Login form
Form validation
develop
merge feature/login
Address review notes
main
merge develop tag: v1.0.0
Hotfix typo type: highlight
```
---
## 30. Block Diagrams
An author-controlled **grid** of rectangular blocks — for diagrams where the **2-D arrangement itself is the meaning** (system block diagrams, hardware/signal chains, layered stacks, deployment topologies). The deliberate exception to dgmo's auto-layout default: you place the blocks (columns, spans, nesting); the renderer still derives every pixel. No net-new grammar — every construct reuses an existing idiom (`[brackets]` + tail metadata from boxes-and-lines §14, indentation nesting + tag cascade from treemap §24C). Distinct from `boxes-and-lines` (§14, topology auto-laid-out) and `flowchart` (§5, decision flow): pick `block` when you want a specific grid, not a connected graph.
### 30.1 Chart Type Declaration
```
block [Title]
```
Space-separated, no colon; title optional. Detection is by the first-line token (`block`).
### 30.2 The Grid
One **source line = one row**; a `[Label]` is a block, filling the row left-to-right.
- **Columns are inferred** from the widest row — `columns N` is optional. A **lone block** on a row fills the width; a short row whose block count **evenly divides** the column count **spreads to fill** (two blocks in a 4-column grid → two half-width blocks); otherwise blocks stay span-1, left-aligned.
- `columns N` (a directive; may sit in the header before the `tag` block, or as a line inside a container) **overrides** the inferred count for that grid.
- `_` is a deliberate **empty cell**; repeat `_ _` for a wider gap.
- **Span** — `[Wide] span: 2` widens a block across columns (metadata, §1.4.1, OUTSIDE the bracket). A span larger than the column count **clamps** to it.
### 30.3 Containers & Collapse
A block becomes a **container** by indenting a sub-grid under it (the org / treemap indentation idiom). Sibling containers **stack vertically** — each needs its own indented body — so the horizontal grid is for the leaf blocks inside a container; side-by-side containers are a reserved seam.
A bare `collapsed` flag on a container line starts it **folded**: it renders as a header band with the shared **collapse-bar** (the org §7 / sitemap / mindmap signal — no count). Collapse/expand is interactive in the desktop app (interactive-vs-export split, like map §24B / treemap §24C); static export renders the authored state.
### 30.4 Tags
Metadata goes **OUTSIDE** the bracket — `[Backend] l: Service` — never inside (the boxes-and-lines group precedent, §14.4; this keeps a colon inside a label, `[API: v2]`, as label text). A **group** tag **cascades** to its children; an **individual box** tag **overrides** the cascade. Declare the `tag` group before content; named palette colors only (§1.3, §1.5).
### 30.5 Node Metadata & Directives
| Key / token | Effect |
| ----------- | ------ |
| `span: N` | Column span (integer ≥ 1; clamps to the column count) |
| `collapsed` | Bare flag — start this container folded |
| `: V` | Tag value via the group's alias or name (cascades / overrides) |
| `columns N` | Directive — override a grid's inferred column count |
| `no-legend` | Directive — hide the tag legend |
### 30.6 Diagnostics
| Code | When |
| ---- | ---- |
| `E_TAG_DECLARED_AFTER_CONTENT` | a `tag` group appears after the first block row |
Over-indented orphan lines and unclosed `[` brackets warn and are skipped; an empty diagram (no blocks) errors.
## 31. Sketch Diagrams
A **GUI-first constrained canvas**: free placement of uniformly-sized shapes on a snap grid, arrows between them, meaning through tags. The markup is **generated** by the canvas editor — it looks like dgmo and diffs like dgmo, but human writability is a non-goal (the parser still tolerates hand-authored files; see the leniency rules below). The renderer owns every pixel of styling; the author owns only placement, connection, naming, and tags. Pick `sketch` when the drawing IS the content ("get it out of your head"); pick `boxes-and-lines` (§14) when you want topology auto-laid-out.
### 31.1 Chart Type Declaration
```
sketch [Title]
```
Space-separated, no colon; title optional. Detection is by the first-line token (`sketch`).
### 31.2 Shapes
One **top-level source line = one shape**. Shapes are **bare names** with same-line `key: value` metadata (the infra §4.2/§4.3 idiom — multi-word names parse; quotes for reserved characters per §2.2a):
```
Spyglass Feed shape: database, at: 0 0, crew: Deck
Captain's Console as con at: 2 0, crew: Deck
```
- **Org-style card** — every shape renders as a uniform rounded card: a **header** with the name (in the tag color), a **rule**, and one **metadata row** per tag the shape carries (`Group: value`). A shape with no tags centers its name.
- **`shape:`** — closed set, full words: `database`, `queue`, `person`, `document`, `note`. Rectangle is the default and is **never written**. Type appears as a small **badge** in the card header (not a silhouette). `note` is the one non-card shape: a sticky-style card with smaller, regular-weight, left-aligned multiline text. An unknown value warns (`W_SKETCH_UNKNOWN_SHAPE`) and falls back to rectangle. (`cloud` was retired — it did not take the card treatment cleanly.)
- **One universal footprint** — every card is the same fixed size (208×150px at the default cell), renderer-owned, tall enough to host the header plus a few rows. No shape is larger, smaller, or a different aspect than another. There is NO `size:`; the name fits on one line (shrink a step → `…` cutoff).
- **Aliases** (`as con`, §2A) exist for **diff stability**, not typing: edges reference aliases, so renaming a shape is a one-line diff. The GUI mints an alias lazily — only when something references the shape — with one exception: **same-label shapes always get distinct aliases**, referenced or not (bare duplicate lines would merge on parse). Hand-authored **unaliased** duplicates follow standard dgmo semantics: they merge with `I_NAME_MERGED`.
- **Quoted names** (reserved characters, §2.2a) cannot take `as`; they may still be edge targets by exact quoted-label reference when unambiguous. Apostrophes are legal in bare names (`Captain's Console`).
### 31.3 Coordinates — `at: C R`
The **only** coordinate construct. Integers, counting **half-slot steps** (a slot = one footprint + the mandatory gap between shapes; the half-step permits brick-like offsets). Origin-normalized on emit: the content bounding box starts at `0 0`, and box children are relative to their box's origin — so panning the whole sketch, or moving a box, is a tiny or zero diff.
- **`at:` is optional.** Un-positioned shapes auto-place via a deliberately dumb flow layout — appended in rows below existing content, reading order, nothing smarter. Lazy hand-authoring is legal; the GUI writes real coordinates on next save.
- **Overlap is unrepresentable via the GUI** (enforced spacing + make-room). Hand-authored overlap is legal input: it auto-resolves at render with `W_SKETCH_OVERLAP_RESOLVED`. Never reject, never render broken.
### 31.4 Lines
Edges are **indented under their source** shape (§1.7), targeting an alias — or a bare/quoted label when unambiguous (infra §4.4 precedent; ambiguous → `E_SKETCH_AMBIGUOUS_TARGET`):
```
Spyglass Feed shape: database, at: 0 0
-sightings-> con
~haul~> dvy crew: Hold
-- ledger
```
Six grammar forms — three head-configs × solid/dashed:
| Form | Meaning |
| ---- | ------- |
| `-label->` | one head, solid |
| `<-label->` | both heads, solid |
| `-label-` | no heads, solid |
| `~label~>` / `<~label~>` / `~label~` | dashed twins |
- **Unlabeled** headless lines are written `--` (solid) and `~~` (dashed) — never a bare `-`/`~`. Unlabeled arrow forms (`->`, `<->`, `~>`, `<~>`) are legal.
- **Dashed means "secondary"** in sketch — a softer stroke, NOT async. This deliberately diverges from infra's `~>` (async); generators must not carry the async reading over.
- There is **no source-head-only form** — left-pointing arrows are banned language-wide; the GUI normalizes "head at source only" by swapping (`A <- B` → `B -> A` convention) and re-parenting the edge.
- **Both ends must attach** — dangling lines are unrepresentable.
- Edge **tail metadata** (infra §4.4): a tag value (`crew: Hold`) colors the LINE full-stroke (map §24B edge precedent). Targets may be shapes or boxes (by alias or unambiguous label).
### 31.5 Boxes — `[Brackets]`
`[Brackets]` mean exactly **one** thing in sketch: a **box** (group) — consistent with group references everywhere else (§1.8). No `group` keyword. A box line with indented bare-name children draws a labeled frame around them:
```
[Below Decks] at: 2 2, crew: Hold
Booty Queue as bq shape: queue, at: 0 0
Ship Ledger as ledger shape: database, at: 2 0
[Armory] as armory at: 0 2, collapsed
Powder Store at: 0 0
```
- **Label mandatory**; the box reserves a **top band** for it (big, thick, faded, centered) — shapes can never occupy the band.
- **One level only** — a nested box degrades gracefully (children render flat) with `E_SKETCH_NESTED_BOX`.
- **Taggable**: the frame tints and the tag **cascades** to children; an individual child tag **overrides** (block §30.4 rule).
- Bare **`collapsed`** flag (block §30.3): the box folds to a virtual node card wearing the collapse-bar; edges re-target the card. Interactive fold/unfold in the app; static export renders the authored state.
- Children's `at:` coordinates are **relative to the box origin**.
### 31.6 Tags
Tag groups are §1.3/§1.5 **verbatim** — declared before content, values optionally `[color]`-hinted from the named palette, applied via `: value` metadata on shapes, boxes, and edge tails. Untagged shapes render **neutral gray** (fill `mix(palette.surface, palette.bg, 40)`, stroke `textMuted`) — meaning is literally absent until assigned. The GUI's first tag creates an implicit group named `Tags` (`tag Tags`), renamable later; there is no color picker anywhere — authors define semantics that HAVE color via the palette.
### 31.7 Metadata & Directives
| Key / token | Applies to | Effect |
| ----------- | ---------- | ------ |
| `shape: ` | shape | Morph from the default rectangle (closed shape set; type shows as a header badge) |
| `at: C R` | shape, box | Half-slot position (optional; omit → flow auto-place) |
| `as ` | shape, box | Reference handle for edges (§2A) |
| `collapsed` | box | Bare flag — start folded to a virtual node card |
| `: V` | shape, box, edge tail | Tag value (cascades from box; individual overrides; colors edge lines) |
| `no-legend` | directive | Hide the tag legend |
### 31.8 Diagnostics
| Code | When |
| ---- | ---- |
| `E_SKETCH_NESTED_BOX` | a `[Box]` inside a `[Box]` — inner box degrades to flat children |
| `E_SKETCH_AMBIGUOUS_TARGET` | an edge targets a bare label shared by multiple shapes |
| `W_SKETCH_UNKNOWN_SHAPE` | unknown `shape:` value — falls back to rectangle |
| `W_SKETCH_OVERLAP_RESOLVED` | hand-authored overlapping `at:` positions were auto-resolved |
### 31.9 Generator Guidance
Sketch is a **GUI-authored format**. If generating it anyway: use integer `at:` on the half-slot lattice with shapes ≥ 2 half-slots apart on at least one axis (or omit `at:` entirely and let flow placement work); alias any shape an edge references; never invent `size:`, colors, or shapes outside the closed set; `~` means secondary, not async. Malformed coordinates never break a render — they resolve with warnings — but tidy input diffs better.
---
## 32. Family Diagrams
A **genealogy / family tree**: people, the couples (unions) they form, and the children those unions produce. The whole grammar is two line shapes — a **person line** (a bare name, optionally with metadata) and a **union line** (`NameA + NameB`) — plus **indentation** for parent→child descent (§1.6). There are no arrow edges: kinship is expressed by the `+` join and by nesting, the way org charts (§7) express reporting lines by nesting alone. Pick `family` for ancestry, lineage, and dynasty diagrams; pick `org` (§7) for pure single-parent hierarchies with no unions.
### 32.1 Declaration
```
family [Title]
```
Space-separated, no colon; title optional. Detection is by the first-line token (`family`).
```
family House of Blackwater
```
### 32.2 People
A **person** is a bare name on its own line, optionally carrying same-line metadata (§1.4.1). Names may be quoted (§2.2), `as`-aliased (§2A), and carry a bare trailing color token per §1.5.
```
Anne b: 1665, d: 1714, sex: f
"Long John Silver" as ljs sex: m
Redbeard purple
```
- **Person metadata lives on a standalone person declaration only** — the first place the name appears on its own line. Restating the name later (e.g. as one side of a union) re-references the *same* person; it does not accept a fresh metadata set. Declare the person standalone (with all their metadata) before or after the union that uses them.
- `b`/`d` render as a **year range** under the name (`1665–1714`).
### 32.3 Unions (`NameA + NameB`)
A **union** is a couple, written with ` + ` between two names:
```
Anne + Jack m: 1701
```
- Each side is a **bare name** — quoting (§2.2), `as`-aliasing (§2A), and a bare trailing color token per §1.5 are all allowed on a side. **Per-side `key: value` metadata is NOT supported** — a side cannot carry its own `b:`/`sex:`/etc. To give a partner metadata, declare that partner as a standalone person line (§32.2); the union then re-references them by name.
- **Union-level metadata is only `m`** (marriage year), e.g. `m: 1701`. It attaches to the couple, not to either person, and renders on the marriage bar.
#### Union-line parse order (important)
The union line is decomposed in a **pinned order** so that a ` + ` inside a name or a value is never mistaken for the couple join:
1. **Union metadata cut first (§1.4.1).** The trailing `m: ` is peeled off the **end** of the line.
2. **`+` split second, within the remaining name region only.** The name region left after the metadata cut is split on ` + ` into the two sides.
Because the metadata is removed before the split, a `+` living inside a quoted name (`"Anne + Jack"` is **one** person) or inside a metadata value is protected — it never triggers a couple split. Each side is then run through the universal name pipeline (quote → color → alias) exactly as §1.5's cut order specifies.
### 32.4 Children (Indentation = Descent)
Lines **indented under a union line** are that couple's children. Children are themselves **person lines** (§32.2) — so a child can carry `sex:`, `b:`, a color, an alias, everything a top-level person can:
```
Anne + Jack m: 1701
Mary b: 1702, sex: f
Tom b: 1704, sex: m
```
A child can later **head their own union line**, extending the tree another generation. Reusing the child's name as a union side re-references the same person:
```
Anne + Jack m: 1701
Mary b: 1702, sex: f
Tom b: 1704, sex: m
Mary + Silas m: 1725 // Mary from above, now a parent
Grace b: 1726, sex: f
```
**Single parent** — a **lone person line** (no `+`) with indented children is a single-parent family. No extra syntax:
```
Bess sex: f
Nan b: 1690, sex: f
Will b: 1692, sex: m
```
### 32.5 Adoption
A child line ending in a **bare `adopted` token** marks an adopted child; the edge from parents to that child renders **dashed**:
```
Anne + Jack m: 1701
Mary b: 1702, sex: f
Ned b: 1705, sex: m adopted
```
A literal person actually **named** "Adopted" must be **quoted** (`"Adopted"`) so the token is read as a name rather than the flag.
### 32.6 Metadata Keys (GEDCOM-Flavored, Fixed Set)
The reserved metadata keys are a **closed, GEDCOM-flavored set**. Colons are required (`key: value`, §1.4). Unknown keys emit a **warning** (not an error) and are otherwise ignored.
| Level | Key | Value | Renders as |
| --- | --- | --- | --- |
| person | `sex` | `m` \| `f` | drives the default card color (§32.7) |
| person | `b` | birth year | left end of the year range |
| person | `d` | death year | right end of the year range |
| person | `bp` | birth place | detail line |
| person | `dp` | death place | detail line |
| person | `occupation` | free text | detail line |
| person | `military` | free text | detail line |
| person | `education` | free text | detail line |
| person | `religion` | free text | detail line |
| person | `burial` | free text | detail line |
| union | `m` | marriage year | marriage-bar label |
`b` and `d` together render as a **year range** under the name (e.g. `1665–1714`); either alone renders as a partial range (`1665–` / `–1714`).
### 32.7 Sex → Color (Legend Is the Guaranteed Channel)
When a person carries `sex:`, their card takes a **default color** (a named-palette color at 25% tint, §1.5):
| `sex:` | Default color |
| --- | --- |
| `m` | blue |
| `f` | purple |
| unset | gray |
An explicit **tag** (§1.3) or a bare **trailing color token** on the person **overrides** the sex-derived color.
> **Caveat — treat the legend, not the hue, as the sex channel.** Blue and purple sit close in hue and desaturate further under the 25% tint, so they separate weakly under deuteranopia. Where color-vision accessibility matters, distinguish the sexes with **tags** (whose legend entries carry text labels) rather than relying on the fill. The legend is the guaranteed, readable sex channel; the fill is a convenience.
### 32.8 Remarriage
A person who appears in **two union lines** renders as **one card with two marriage bars** — reuse the same name in a second union to express a remarriage:
```
Redbeard sex: m
Redbeard + Anne m: 1701
Mary b: 1702, sex: f
Redbeard + Grace m: 1716 // same Redbeard — one card, second marriage bar
Ned b: 1717, sex: m
```
### 32.9 Same-Sex Unions
Same-sex couples need **zero extra syntax** — both sides are just names, and each partner's `sex:` (declared standalone) is honored for color:
```
Anne sex: f
Bess sex: f
Anne + Bess m: 1720
Nan b: 1721, sex: f adopted
```
### 32.10 Tags & Colors
Tag declarations (§1.3) and trailing-color suffixes (§1.5) work as the **universal constructs** describe — nothing family-specific. Declare a tag group before content, apply it via `: value` metadata on a person line, and the person's card tints accordingly (overriding the sex color, §32.7). A bare trailing color token on a person or a union side also works per §1.5.
### 32.11 Worked Example
```
family House of Blackwater
tag Line as line
Founders green
Cadet blue
Redbeard b: 1660, d: 1718, sex: m, occupation: Captain, line: Founders
Anne b: 1665, d: 1714, sex: f, line: Founders
Redbeard + Anne m: 1685
Mary b: 1687, sex: f
Tom b: 1689, sex: m
Ned b: 1693, sex: m adopted
Mary + "Long John Silver" m: 1710
Grace b: 1711, sex: f, line: Cadet
Redbeard + Grace m: 1716 // remarriage — one Redbeard card, second bar
Bess b: 1717, sex: f
Bess sex: f // single parent
Nan b: 1740, sex: f
```
This exercises the declaration, a union with children, an adopted child, a child who becomes a parent, remarriage (one card / two bars), a single parent, sex-driven color, a tag override, and a quoted partner name.
### 32.12 Diagnostics
| Code | Severity | Fires when |
| --- | --- | --- |
| `W_FAMILY_UNKNOWN_KEY` | warning | A metadata key outside the §32.6 fixed set (and not a declared tag alias) is used — the pair is ignored. |
| `W_FAMILY_SIDE_METADATA` | warning | A `key: value` pair appears on one **side** of a union (per-side metadata unsupported) — declare the partner standalone (§32.3). |
| `W_FAMILY_HIGHLIGHT_UNKNOWN` | warning | A `highlight ` directive (§32.18) names a person not present in the tree — nothing is dimmed. |
### 32.13 Divorce (Dissolved Unions)
A union line ending in a **bare `divorced` token** marks a dissolved marriage; its marriage bar renders **dashed**. The token is **union-level** and sits after any `m:` year. Children still attach normally:
```
Alice + Bob m: 1980 divorced
Carol b: 1982
Dave b: 1985
```
`divorced` is only meaningful on a line that joins two people (has a spaced `+`); a lone person named "Divorced" is unaffected.
### 32.14 Deceased Marker
A person carrying a **death year (`d:`)** is automatically prefixed with a muted **dagger (†)** on their card — the standard genealogy convention, derived (no syntax). Living people (no `d:`) and `?` placeholders (§32.17) never get a dagger. Add the top-level option **`no-daggers`** to suppress the marker entirely (the death year still renders in the year row).
### 32.15 Children Ordered by Birth Year
Within a union, children are automatically ordered **eldest → left** by their birth year (`b:`). Children **without** a `b:` keep their declaration order and follow the dated ones. No option — this is always on.
### 32.16 Generation Labels (`generations`)
The opt-in top-level option `generations` draws a **left gutter** with **Roman-numeral** row labels (`Gen I`, `Gen II`, …), one per generation row, plus **subtle zebra shading** behind alternating generations so it's easy to see who shares a generation:
```
family
generations
Jack + Grace
Ned + Anne
Tom
```
### 32.17 Unknown / Private Person (`?`)
A bare **`?`** where a name is expected renders an **anonymous placeholder** — a faint, solid-bordered, name-only card — for a gap in the tree (an unknown parent, a private individual). **Each `?` is a distinct person**; two `?` never merge into one card:
```
? + Mary // Mary's spouse is unknown
Kid b: 1690
```
### 32.18 Lineage Highlight (`highlight `)
Family is one of the two chart types that support the **emphasis family** (§1.11); the bloodline is its closure rule. The `dim` dual is not offered here — a family tree's background is always "everyone off this line", which `highlight` already names.
The top-level directive `highlight ` **dims** everyone **outside** the named person's bloodline — their **ancestors**, **descendants**, and the **spouses** of that line stay fully lit; collateral relatives (siblings, cousins, in-laws' families) fade. Useful for tracing one person's line through a large tree. An unknown name emits `W_FAMILY_HIGHLIGHT_UNKNOWN` and dims nothing:
```
family
highlight Anne
Jack + Grace
Anne + Ned
Tom
```
## 33. Body Diagrams
A **human-figure annotation**: a fixed anatomical illustration on which you highlight muscles by name, color them by a facet, and note the exercise or finding beneath each. Use it for **workout splits**, **PT / rehab and injury reports**, and **anatomy teaching**. It is a labeling surface over fixed art — there are no axes and no values. The whole grammar reuses existing idioms: bare catalog-name lines, same-line tag metadata (§1.4.1), indented bare-line notes (as in `pyramid`/`ring`), and `tag … as …` groups (§1.3). There is **no net-new syntax**.
### 33.1 Declaration
```
body [Title]
```
Space-separated, no colon; title optional. Detection is by the first-line token (`body`) — there is no content sniffer.
### 33.2 Figure: form, sex, view (bare directives)
Four figures ship — **male** / **female** × **front** / **back**. The figure is set by **bare directives** placed before the parts; each is optional with a default:
| Facet | Tokens | Default |
|-------|--------|---------|
| form | `muscle` \| `skin` | `muscle` |
| sex | `male` \| `female` | `male` |
| view | `front` \| `back` | `front` |
```
body Back & Legs
muscle
female
back
```
- `muscle` renders the segmented muscle map; `skin` renders the same figure as a plain silhouette (head + hair, no segmentation). `skeletal` is **reserved**.
- **View is a set.** Naming **both** `front` and `back` renders the two views **side by side** in one diagram; a part is drawn on whichever view(s) contain it.
### 33.3 Parts (catalog muscle names)
A **bare line** is a catalog muscle name for the active figure, in any order:
```
chest
deltoids
quadriceps
```
- **Front** parts: `neck`, `trapezius`, `chest`, `deltoids`, `biceps`, `triceps`, `forearm`, `abs`, `obliques`, `quadriceps`, `adductors`, `calves`, `tibialis`. **Back** parts: `trapezius`, `rear-delts`, `triceps`, `lats`, `lower-back`, `forearm`, `glutes`, `hamstring`, `calves`. **Fine heads** (`vastus-lateralis`, `rectus-femoris`, `biceps-femoris`, `serratus-anterior`, …) also resolve.
- **Aliases** — gym shorthand and formal anatomy both resolve to the canonical part: `pecs`/`pectoralis-major` → chest, `quads` → quadriceps, `glutes` → gluteal, `lats` → latissimus-dorsi, `delts` → deltoids, `traps` → trapezius. The label shows what the author typed; resolution is normalized.
- An **unknown** name emits `W_BODY_UNKNOWN_PART` and is skipped; the rest of the figure still renders. When both views are requested, a name is unknown only if it resolves on **neither** view.
### 33.4 Tags & Color
A part carries **same-line tag metadata** to color it. Declare a tag group (§1.3) and apply it with `: `; the tag colors the muscle, its leader line, and its label, and adds a legend entry. Untagged muscles stay a neutral gray.
```
body Push Day
muscle
front
tag Effort as e
Primary red
Secondary orange
chest e: Primary
deltoids e: Primary
triceps e: Secondary
```
### 33.5 Notes
A **bare indented line** beneath a part is a note (same shape as a `pyramid`/`ring` note). The **first** note prints beneath that muscle's gutter label — use it for the set/rep scheme or the clinical finding:
```
chest e: Primary
Barbell bench press — 4×8
```
### 33.6 Options
- `no-legend` — hide the tag legend.
## 34. Goal Charts
A single **progress-toward-a-target** reading: one `now` value against one `target`, rendered in one of three static faces. It answers "how close am I?" — a KPI tile, a fundraising thermometer, a quarterly quota, a completion percentage. There is no time axis, no series, and no milestones (for a running clock use `countdown`; for a value over time use `line`). No net-new syntax: the mode is a bare-flag directive (like treemap's `radial` / pyramid's `inverted`) and the values are space-separated `key value` directives (like gantt's `start-date`).
### 34.1 Declaration
```
goal [Title with unit]
```
Space-separated, no colon; title optional but recommended (the **unit lives in the title** — there is no `format`/`currency` directive, per the treemap rule §24C). Detection is by the first-line token (`goal`) — there is no content sniffer.
### 34.2 Values (`now` / `target`)
```
goal Doubloons Plundered ($)
now 6400
target 10000
```
- `now ` — current value (required; ≥ 0). Missing → treated as `0` with a warning.
- `target ` — goal value (required; **> 0**). Missing or ≤ 0 → error diagnostic, but the chart still renders a 0% shell.
Both are **space-separated `key value`** — no colon (`now: 3` is not recognized). Values accept `_` separators (`10_000`) but **not** thousands commas. The percent is `now / target`; values auto-compact for display (`6.4k`, `1.2M`), matching treemap.
### 34.3 Faces (bare-flag mode)
A bare flag on its own line selects the render face; omit it for the default bar.
| Flag | Face |
|---------------|---------------------------------------------------|
| *(none)* | Progress bar — horizontal rounded fill (default). |
| `thermometer` | Vertical tube + bulb; the column rises with progress. |
| `gauge` | Semicircular dial with a needle and value arc. |
All three faces consume the **same** `now`/`target` pair — the flag only changes the drawing.
### 34.4 Over-target & edge cases
- **Over target** (`now > target`): the fill **clamps at 100%** but the % label stays truthful (e.g. `120%`); the gauge needle pins at max.
- **Negative `now`**: fill clamps to 0%, label stays honest.
- Single value only — this type has no children/rows. Indented content is ignored with a warning, **except** an indented `note` block body (§34.7).
- `aria-label` = `":