DGMO Language Reference

Complete syntax documentation for every chart type, directive, and option.

Reference for dgmo v0.55.0
New to Diagrammo? Start with the chart-type guides →
The complete grammar. This is the exhaustive, canonical DGMO syntax reference — every chart type, directive, and option. For AI assistants, the leanest path is the MCP server (per-type syntax on demand) or the plain-text /llms-full.txt for whole-spec context.

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
  2. Universal Name Handling
  3. Sequence Diagrams
  4. Infrastructure Diagrams
  5. Flowchart Diagrams
  6. State Diagrams
  7. Org Charts
  8. C4 Architecture Diagrams
  9. Entity-Relationship Diagrams
  10. Class Diagrams
  11. Kanban Boards
  12. Sitemap Diagrams
  13. Gantt Charts
  14. Boxes and Lines Diagrams
  15. Timeline Diagrams
  16. Data Charts
  17. Visualizations
  18. Mindmap Diagrams
  19. Wireframe Diagrams
  20. Tech Radar Diagrams
  21. Cycle Diagrams
  22. Journey Map Diagrams
  23. Pyramid Diagrams
  24. Ring Diagrams
  25. Colon Usage Summary
  26. Authoring Rules (Generators Read This First)
  27. Swimlane Diagrams
  28. Event Line Diagrams
  29. Version-Control Diagrams
  30. Block Diagrams
  31. Sketch Diagrams
  32. Family Diagrams

1. Universal Constructs

These patterns are shared across all or most diagram types.

1.1 Chart Type Declaration (First Line)

<chart-type> [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 <alias>
  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 Zonetrust-zone) used for assignment, matching, and active-tag. Always pair a quoted name with an as <alias> 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 <alias> 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 <GroupName> 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 <key>: (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 typeReserved keys (in addition to declared tag aliases)
sequencecolor, description, role, collapsed, position
infracolor, description, collapsed, icon
flowchart(no metadata)
state(tag-group aliases — tags newly granted, decision #48)
orgcolor, description, role, location, email, phone, plus per-chart custom keys (free-form indented attributes)
c4color, description, tech, type, collapsed
ercolor, description, domain
classcolor, description
kanbancolor, description, wip, assignee, due, priority
sitemapcolor, description, status
ganttcolor, description, duration, offset, progress, start
pertcolor, description, confidence, collapsed
boxes-and-linescolor, description, heat
timelinecolor, description, duration
mindmapcolor, description, collapsed, plus per-chart priorities/status keys declared as tag aliases
tech-radarcolor, description, quadrant, ring, trend
cyclecolor, span, description (width is edge-only)
journey-mapscore, emotion, description, pain, opportunity, thought, color (decision #13, persona long form)
pyramidcolor, description
ringcolor, description
racicolor, description, plus declared roles (which dispatch as role assignments, not attributes)
wireframe(uses trailing-keyword flag list — see §19.5)
treemapheat, plus tag-group aliases
swimlane(tag-group aliases; note: deferred)
event-line(tag aliases; collapsed via era lines)
version-controlid, tag, type, order
blockspan
sketchshape, at
familyclosed GEDCOM set — see §32.6
body(tag aliases)
bracket(tag aliases; seed/score keys per §35)
mapregistry 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.

CodeSeverityFires when
E_PIPE_OPERATOR_REMOVEDremoved 0.43.0(Historical) A `
W_EMPTY_METADATA_VALUEwarningA metadata pair has an empty value (Foo c:). The pair is dropped from the entity’s metadata.
W_ATTRIBUTE_AT_PARENT_INDENTwarningAn 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_REMOVEDremoved 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_REMOVEDremoved 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 <alias> 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 <alias> postfix, and (c) a trailing-token color, the parser strips them in this order:

  1. Metadata cut (§1.4.1) — the first <key>: 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 <alias> 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 (<label> as <alias> <color>); 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 <alias> sits between the label and the trailing color token — <label> as <alias> <color>. 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 <color> directive is a legacy alias). Other chart types currently ignore a title-line trailing color.

Long form color: <name> is reserved for multi-key metadata. Use color: <name> 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: <name> 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 <name> color: <name> 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.
  • 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 typeExample
sequence (§2.2)[Auth System] collapsed
infra (§4.6)[Backend] collapsed
mindmap, pertResearch 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):

DirectiveEffect
fill-tintThe default, spelled explicitly — shapes get the canonical 25% tint fill with a solid intent-color outline
fill-solidRender nodes/bars at full intent saturation instead of the canonical 25% tint
fill-outlineNo fill — shapes take the theme background fill; the intent color is carried entirely by the outline
no-titleHide the diagram banner title in the rendered output (does not mutate the parsed model)
no-legendSuppress the legend on any chart type that renders one (universal — includes data charts; added by decision #48)
legend-inlineRender 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. <script>alert(1)</script> 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

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 <Name>[, <Name>…]     // the named elements are the figure; everything else recedes
dim <Name>[, <Name>…]           // 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 typeFormsClosure
sankeyhighlight, dimUpstream + downstream flow closure (§15.5)
familyhighlight onlyThe 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 → 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:

CharacterWhy 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 whitespaceStripped 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 <alias> (§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.

CombinationBare nameQuoted 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 <type>) accept only the bare-name-plus-optional-metadata form. Anything else needs the typed declaration form.

FormAcceptedNotes
Alicebare implicit
Alice role: xbare + metadata
Alice as astandalone alias without is a is rejected
Alice as a role: xsame — needs typed form
"Auth Service"quoted standalone is rejected
"Auth Service" role: xsame
Alice is a persontyped
Alice is a person as a role: xtyped accepts alias + metadata
"Auth Service" is a database as a role: xtyped 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 <type> (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

SourceWhereRenders asNotes
Auth Service is a databasesequenceAuth Servicebare multi-word, no quoting needed
Cache then cacheflowchartCachefirst-seen wins; warning emitted
"first name" varcharer columnfirst namequoting required to escape : type separator if you wanted it in name
"Order | Items"infraOrder | Itemsquote the pipe
class "Customer Service"classCustomer Servicebare multi-word also accepts
Auth Server is a database then auth server -hi-> DBsequenceone participant Auth Servermessage resolves via normalization
[Order Items]infra groupOrder Itemsbrackets are the group sigil; name inside accepts spaces
Customer Orders { ... }erCustomer Ordersmulti-word table
webapp api gatewayinfrawebapp api gatewayfull 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

CodeSeverityWhenMessage template
I_NAME_MERGEDwarningTwo source-distinct names normalize to the same key AND their displayed forms differmerged '{incoming}' (line N) into '{existing}' (line M) — names differ only in case/whitespace
E_NAME_RESERVED_CHARerrorA bare name contains a reserved character without "..." quotingname contains reserved character '{char}' — wrap in "..." to use literally
E_AKA_REMOVEDerrorThe removed aka keyword appears in a sequence participant declaration'aka' is no longer supported — use the participant name directly
E_PARTICIPANT_TYPE_REMOVEDerrorA 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
  • 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 <alias> — 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: <name> [is a type] [as <alias>] [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 <label…> 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. AlicealiceALICEAlice (trailing space). Author intent is the same string with cosmetic variation.
  • Aliases = different-token short codes. pmProduct 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:

WasNow
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

CodeSeverityMessage
E_ALIAS_BEFORE_DECLerrorAlias '<x>' used before declaration. Declare '<canonical> as <x>' on or above this line.
E_ALIAS_COLLISIONerrorAlias '<x>' is already bound to '<canonical-1>' (line <n>). Cannot rebind to '<canonical-2>'.
E_ALIAS_SHADOWS_NAMEerrorAlias '<x>' would shadow an existing canonical name. Choose a different alias.
E_ALIAS_REBINDINGerror'<canonical>' is already aliased as '<x-1>' (line <n>). Cannot also alias as '<x-2>'.
E_ALIAS_OF_ALIASerror'<x>' is itself an alias for '<canonical>'. Cannot alias an alias — alias the canonical instead.
E_ALIAS_RESERVED_KEYWORDerror'<x>' is a reserved keyword and cannot be used as an alias.
E_ALIAS_INVALID_FORMATerrorAlias '<x>' must match [A-Za-z][A-Za-z0-9_]{0,11} (letter start, letters/digits/underscore, max 12 chars).
E_ALIAS_AFTER_CANONICALerror'<canonical>' was already used as a canonical name (line <n>). Aliases must be declared on or before first use.
E_TAG_SHORTHAND_REMOVEDerrorBare tag shorthand 'tag <name> <x>' was removed. Use 'tag <name> as <x>' instead.
E_VENN_ALIAS_KEYWORD_REMOVEDerrorVenn 'alias' keyword was removed. Use 'as' instead — '<name>(color) as <x>'.
W_ALIAS_CASE_NEAR_MATCHwarning'<x>' differs only in case from declared alias '<X>'. Did you mean '<X>'?
W_ALIAS_UNDERUSEDwarningAlias '<x>' is declared but referenced ≤1 time. Aliases earn their keep on names that repeat 3+ times.

See also


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 writeReads asNote
2026-07-042026-07-04ISO — canonical, always unambiguous
2026-07-04 14:30 / …T14:30[:SS]datetimewhere the type supports a time
2026-07month grainday omitted
2026 / 1716year grainbare year
07-04{year}-07-04bare MM-DD → resolved year (§2B.3)
7/4{year}-07-04slash form, US month-first default (§2B.2)
Jul 4 / July 4{year}-07-04month name — never order-ambiguous
July 4, 2026 / 4 Jul 20262026-07-04full natural form (explicit year)
753 BCE / 44 BC / 14 CEera-signed yeartimeline 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):

DirectiveEffectDefault
year 20XXbase year for bare month-days belowderive / current year
date-order dmynumeric slash/dash dates read day-firstmdy (US)
no-current-yeara fully-bare chart is a hard error, not current-yearoff

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 <type> [as <alias>] [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 TypeShapeName Patterns (examples)
actorStick figureUser, Customer, Admin, Agent, Person, Buyer, Seller, Guest, Visitor, Operator, Developer, Alice, Bob, Charlie, Fan, Purchaser, Reviewer, *User, *Actor, *Analyst, *Staff
databaseCylinder (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
cacheDashed cylinder*Cache, Redis, Memcache, KeyDB, Dragonfly, Hazelcast, Valkey
queueHorizontal cylinder (pipe)*Queue, *MQ, SQS, Kafka, RabbitMQ, EventBus, MessageBus, *Bus, Topic, *Stream, SNS, PubSub, *Broker, NATS, Pulsar, Kinesis, EventBridge, CloudEvents, Celery, Sidekiq, EventHub, *Channel
defaultRectangleEverything 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-onlyis 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 <alias>, 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)

TypeSyntaxExample
Sync (labeled)A -label-> BClient -login-> API
Sync (bare)A -> BClient -> API
Async (labeled)A ~label~> BAPI ~notify~> Queue
Async (bare)A ~> BAPI ~> 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 <Name> as <alias> group and assign <Participant> <alias>: <Value>.

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 <alias> 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:

PropertyCapabilityEffect
cache-hitCache% requests served from cache, reduces downstream RPS
firewall-blockFirewall/WAF% requests blocked, reduces downstream RPS
ratelimit-rpsRate limiterMax RPS passed through
latency-msLatencyAdds to path latency
uptimeAvailabilityMultiplied along path for SLO
instancesHorizontal scalingMultiplies capacity (number or min-max range)
max-rpsCapacity ceilingMax RPS node handles
cb-error-thresholdCircuit breakerError rate trip threshold
cb-latency-threshold-msCircuit breakerLatency trip threshold
concurrencyConcurrency limitMax concurrent requests (serverless)
duration-msProcessing timeTime spent processing (serverless)
cold-start-msServerlessCold start penalty
bufferQueueBuffer size
drain-rateQueueConsumption rate
retention-hoursQueueMessage retention
partitionsQueuePartition count
descriptionDisplayDescription text

Mutually exclusive: concurrencyinstancesmax-rps; buffermax-rps. A node is serverless, traditional, or a queue — not two at once.

4.4 Connections

TypeSyntax
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 <alias> 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

ShapeSyntaxExample
Terminal(Label)(Start)
Process[Label][Do Task]
Decision<Label><Check?>
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

TypeSyntax
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] -> <Valid?>
  -yes-> [Process Data] -> (Done)
  -no-> [Show Error] -> [Validate Input]

Nested decisions:

<Authenticated?>
  -yes-> <Authorized?>
    -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 <NodeId> 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 <NodeId> 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

TypeSyntax
UnlabeledIdle -> Active
LabeledIdle -submit-> Processing

5.4 Groups

[Group Name]

5.5 Notes (States)

State diagrams support the same note <StateName> 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 <Group> as <alias> 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 <type>
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).

TypeSyntax
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 <Table> 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 <ClassName> 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).

RelationshipArrow
Inheritance`—
Implementation..|>
Composition*--
Aggregationo--
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.

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 <number><unit>.

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 +<number><unit> 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 defaultDatabase 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 KickoffKickoff start: 2024-01-15.
  4. Legacy timeline-duration — 2024-01-15 -> 30d BuildBuild 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):

OptionDefaultDescription
sprint-length <dur>2wDuration of one sprint. Only d and w units allowed.
sprint-number <n>1Which sprint the chart starts at (positive integer).
sprint-start <date>chart start-date or todayYYYY-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. 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<N+offset> (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 <alias>: <Value> (or <GroupName>: <Value>) 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 <GroupName> 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 <name> [<durations>] [as <id>] [metadata]. Durations follow the name, separated by spaces or commas. Three forms:

FormMeaning
recruit crew 1 2 4Three-point estimate: O M P (in time-unit weeks)
recruit crew 2M-only: parser fills O and P from default-confidence factors
celebrateTBD: 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.

TypeConstraintUse case
FSB.ES ≥ A.EF + lagDefault; sequential work
SSB.ES ≥ A.ES + lagParallel start (SS+2d = B starts 2 days after A starts)
FFB.EF ≥ A.EF + lagSynchronized finish (ship-together activities)
SFB.EF ≥ A.ES + lagRare; 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 <id>)

The universal as <ident> 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 — <name> 0 (M-only zero) or <name> 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 <name> 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

KeyWhereMeaning
confidenceactivityPer-activity override of default-confidence (high / medium / low / O/P)
collapsedgroupbare trailing flag (§1.8) — start the group collapsed
tag aliases (e.g. c: Captain)activity, groupResolves 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

ChannelWhen applied
Project subtitlealways (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: forwardExpected finish: <date> · ≈ <μ> <unit> of work (± <σ>); backwardExpected start: <date> · ≈ <μ> <unit> lead time (± <σ>); unanchored≈ <μ> <unit> (± <σ>). 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 captionalways (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
Diamondmilestones only
Dashed borderTBD 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 bandscriticality index when MC is on — red ≥ 0.80, orange ≥ 0.50, yellow ≥ 0.25, green ≥ 0.10, blue ≥ 0.02
Slack baronly when node-detail full
σ-as-border-thicknessonly 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 anchorBackward
x-axisfinish 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 labelP(finish ≤ x)P(hit deadline | start by x)
FormulaCDF_duration(x − projectStart)CDF_duration(end-date − x)
DirectionRising 0 → 1Falling 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.

CodeSeverityTriggered when
E_PERT_BOTH_ANCHORSerrorstart-date AND end-date both present (clears any partial anchor state)
E_PERT_INVALID_DATEerrorYYYY-MM-DD malformed or fails round-trip date check
E_PERT_END_DATE_NOWerrorend-date now (only start-date now is valid)
W_PERT_BD_WITH_ANCHORwarningtime-unit bd combined with an anchor
W_PERT_SUBDAY_WITH_ANCHORwarningtime-unit min/h combined with an anchor

14. Boxes and Lines Diagrams

Notes. Annotate any box with note <Box> 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 <Label> [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 <heat-label> — 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: <number> 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 <Label> [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 <tag-group> switches to a tag group; active-tag none suppresses all tinting; active-tag <heat-label> 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 <Facet> as <alias> with one color per value — values indented under the tag header, color as a trailing token — then reference the alias in box metadata (<alias>: <Value>). The <alias> <Value> 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 (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 — 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:

FamilyChartsno-nameno-valueno-percent
share-of-totalpie, polar-areayesyesyes
funnelfunnelyesyesyes (conversion %)
cartesianbar, line, radar— (axis ticks are separate)yes
point-cloudscatteryes
densityheatmap— (axis ticks are separate)yes
flowsankey, arcname suppression deferred (Phase 2c)— (Phase 2c)
spatialslope, quadrant, vennname suppression deferred (Phase 2d)— (Phase 2d)
single-labelwordcloud
functionalfunction

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 <alias>] [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

OptionEffect
active-tag GroupNameSet default active tag group
color-by-depthBare 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:

SyntaxElementNotes
[text]Text input (leaf) or group (if has indented children)See §18.8 for disambiguation
(Button)Button
{Option A | Option B}Dropdown / selectOptions separated by |
<x>Checkbox (checked)
< >Checkbox (unchecked)
(*) LabelRadio button (selected)
( ) LabelRadio button (unselected)
# HeadingHeading
## SubheadingSubheading
---DividerHorizontal rule
- itemList item
Bare textText / paragraphAny line not matching other syntax

18.4 Keyword Elements

Nine special keyword elements render common UI patterns:

KeywordDescriptionHints / Options
navNavigation bar
tabsTab bar
tableData tabletable RxC for skeleton shorthand (e.g. table 5x3)
imageImage placeholderround, wide
modalModal dialogHas indented children for modal content
skeletonLoading skeleton
alertAlert / notification
progressProgress barValue 0–100 (e.g. progress 65)
chartChart placeholderline, 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):

FlagEffect
disabledGreyed-out, non-interactive appearance
activeActive / current state
selectedSelected state
emptyEmpty / placeholder state
ghostGhost / outline-only button style
destructiveDestructive action (red-tinted)
successSuccess state (green-tinted)
warningWarning state (yellow-tinted)
infoInfo state (blue-tinted)
scrollableScrollable container
collapsedCollapsed container
toggleToggle switch appearance
passwordPassword input (masked)
textareaMulti-line text input
horizontalHorizontal layout for group children
primaryPrimary / 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.

DirectiveDescription
no-blip-legendSuppress 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

KeyTypeDefaultDescription
colorpalette namefirst palette color (uniform)Node fill color
spanpositive number1Relative arc distance to next node. Decimals allowed (e.g., 1.5). Zero or negative → parse error
descriptionstringOne-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

KeyTypeDefaultDescription
colorpalette nameinherits source node colorEdge stroke color
widthnumber (px)3Edge 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

DirectiveDescription
direction-counterclockwiseReverse cycle direction (default: clockwise)
circle-nodesRender 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 <name> | <N> (or | <N> <Word>) emits E_JOURNEY_BARE_SCORE_REMOVED with hint to use score: <N>, emotion: <Word>.

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

DirectiveEffect
active-tag GroupNameSet 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:

Quick FeedbackFrustratedRelievedOpened appSearched for featureHit errorNo helpful error messageContacted supportGot resolution
Quick FeedbackFrustratedRelievedOpened appSearched for featureHit errorNo helpful error messageContacted supportGot resolution
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

Buying a LaptopTech-Savvy Shopper28yo developer, price-sensitive, doesextensive researchChannelWebMobileEmailIn-PersonEngagedFrustratedDelightedResearchCompare specsChecked 12 laptops across 4 reviewsitesWebWatch reviewsMobileAsk friendsIn-PersonPurchaseAdd to cartWebForced account creationWants guest checkoutPassword requirements too strictWebComplete paymentWebDeliveryTrack packageMobileUnboxingInclude setup guideExcited to try it outIn-Person
Buying a LaptopTech-Savvy Shopper28yo developer, price-sensitive, doesextensive researchChannelWebMobileEmailIn-PersonEngagedFrustratedDelightedResearchCompare specsChecked 12 laptops across 4 reviewsitesWebWatch reviewsMobileAsk friendsIn-PersonPurchaseAdd to cartWebForced account creationWants guest checkoutPassword requirements too strictWebComplete paymentWebDeliveryTrack packageMobileUnboxingInclude setup guideExcited to try it outIn-Person
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

Coffee Shop VisitOrderWalk inWait in lineOrder drinkWaitFind a seatWait for drinkEnjoyGet drinkDrink coffee
Coffee Shop VisitOrderWalk inWait in lineOrder drinkWaitFind a seatWait for drinkEnjoyGet drinkDrink coffee
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 <layer-name> | <text> (no key: prefix) emits E_PYRAMID_BARE_DESCRIPTION_REMOVED with hint to use description: <text> (quoted when the value contains commas) or the indented body form.

23.4 Metadata Keys

Keys are comma-separated, per §1.4.1.

KeyValueExample
colorpalette color nameSafety color: blue, description: "Security, health"
descriptiondescription (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: <name> long form is reserved for cases where color rides alongside another key.

Love & Belonging orange
  Friendship, intimacy, family.

23.5 Directives

DirectiveEffect
invertedFlip 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

Maslow's Hierarchy of NeedsSelf-ActualizationMorality, creativity, acceptance of facts,meaning, inner potential.EsteemRespect, recognition, confidenceLove & BelongingFriendship, intimacy, family, a senseof connection.SafetySecurity, employment, health, propertyPhysiologicalFood, water, warmth, rest
Maslow's Hierarchy of NeedsSelf-ActualizationMorality, creativity, acceptance of facts,meaning, inner potential.EsteemRespect, recognition, confidenceLove & BelongingFriendship, intimacy, family, a senseof connection.SafetySecurity, employment, health, propertyPhysiologicalFood, water, warmth, rest
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

Funnel of LearningHear about itPassive awarenessRead about itShallow familiarityTry it onceFirst-hand attemptPractice itBuilding fluencyTeach itDeep mastery
Funnel of LearningHear about itPassive awarenessRead about itShallow familiarityTry it onceFirst-hand attemptPractice itBuilding fluencyTeach itDeep mastery
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 <layer-name> | <text> (no key: prefix) emits E_RING_BARE_DESCRIPTION_REMOVED with hint to use description: <text> or the indented body form.

24.4 Metadata Keys

Keys are comma-separated, per §1.4.1.

KeyValueExample
colorpalette color nameCrew color: blue, description: Deckhands and gunners
descriptiondescription (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: <name> 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

DirectiveEffect
fill-solid / fill-outlineRender 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

Captain's Sphere of InfluenceCaptainQuartermasterCrewAllied CrewsThe Open SeaCaptainFinal word on heading and plunder,keeper of the ship's charter.QuartermasterSecond-in-command, arbitratesdisputes, divvies the booty.CrewDeckhands, gunners, and powdermonkeys aboard the ship.Allied CrewsLoose alliances kept by oathor shared bounty.The Open SeaWeather, currents, and rivalflags beyond reach.
Captain's Sphere of InfluenceCaptainQuartermasterCrewAllied CrewsThe Open SeaCaptainFinal word on heading and plunder,keeper of the ship's charter.QuartermasterSecond-in-command, arbitratesdisputes, divvies the booty.CrewDeckhands, gunners, and powdermonkeys aboard the ship.Allied CrewsLoose alliances kept by oathor shared bounty.The Open SeaWeather, currents, and rivalflags beyond reach.
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 <Label> [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:

InputRamp
no colors, data crosses 0diverging red · neutral · green, midpoint pinned at 0
no colors, one-sign datasequential neutral · accent (palette primary)
one color cneutral → c (map’s one-color rule, §24B)
two colors lo hilo → 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

KeyValueExample
heatnumber (signed/float)AAPL 180 heat: -2.4
<tag>a declared tag valueEngineering t: Eng

value is not a metadata key — size is the bare trailing number.

24C.6 Directives

DirectiveEffect
heat <Label> [low] [high]Name (and optionally color) the value ramp; pairs with the heat: key.
active-tag <GroupName | HeatLabel>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 NRender 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-valueHide value labels (plural no-values accepted as silent alias).
no-percentHide percentage labels.
no-headersHide parent header bars.
no-legendHide 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.

Sailing & Rigging500 · 69%Cannon Battery220 · 31%Rigging320 · 44%Helm180 · 25%Powder90 · 13%Shot130 · 18%Plunder Spend ($k)720BranchSailing & RiggingCannon Battery
Sailing & Rigging500 · 69%Cannon Battery220 · 31%Rigging320 · 44%Helm180 · 25%Powder90 · 13%Shot130 · 18%Plunder Spend ($k)720BranchSailing & RiggingCannon Battery
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

Q3 BudgetEngineering640 · 62%Go-to-Market220 · 21%Operations180 · 17%Platform320 · 31%Mobile180 · 17%Data140 · 13%Field Sales130 · 13%Ads90 · 9%Cloud110 · 11%Support70 · 7%TeamEngSalesOps
Q3 BudgetEngineering640 · 62%Go-to-Market220 · 21%Operations180 · 17%Platform320 · 31%Mobile180 · 17%Data140 · 13%Field Sales130 · 13%Ads90 · 9%Cloud110 · 11%Support70 · 7%TeamEngSalesOps
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.

VariantMarker alphabetConstraint
RACIR A C IExactly one Accountable per task
RASCIR A S C IExactly one Accountable per task
DACID A C IExactly 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: <markers>

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 <GroupName> — 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

CodeSeverityFires when
E_RACI_MULTI_ACCOUNTABLEerrorA RACI/RASCI task has more than one A
E_DACI_MULTI_DRIVERerrorA DACI task has more than one D
E_DACI_MULTI_ACCOUNTABLEerrorA DACI task has more than one A
E_RACI_INVALID_MARKERerrorA marker isn’t in the resolved variant’s alphabet
E_RACI_UNEXPECTED_LINEerrorFree-text line appears after the first role assignment
E_RACI_MIXED_VARIANTSerrorBoth D and S markers appear in the same chart
W_RACI_MISSING_ACCOUNTABLEwarningRACI/RASCI task has no A
W_RACI_MISSING_RESPONSIBLEwarningRACI/RASCI task has no R
W_DACI_MISSING_DRIVERwarningDACI task has no D
W_DACI_MISSING_ACCOUNTABLEwarningDACI task has no A
W_RACI_UNKNOWN_ROLEwarningRole 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:

ContentDress
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 mapColorized — every drawn region gets a distinct pastel “political map” fill (see below). POIs/routes draw on top.
no-colorizeForces 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 MexicoGulf 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 <label> [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 <GroupName> activates that tag group → categorical fill (the heat value is ignored for fill but still parsed/available).
  • active-tag <HeatLabel> — 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 <name | <lat> <lon>> [as <alias>] [<key>: <value>, …]

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 positionalpoi <lat> <lon> (e.g. poi 39.74 -104.99). Two leading numbers occupy the name region, so as <alias>, 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: <zone>, §24B.14), the applied tag alias, as. No icon in v1 (color-only categories).
  • Coord-positioned or relabeled POIs take as <alias> 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 <origin> — 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 <arrow> <destination> 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 (<origin> <connector> <destination>) — 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 labellabeled
directed straightA -> BA -ships-> B
directed arcA ~> BA ~trade~> B
undirected straightA -- BA -ferry- B
undirected arcA ~~ BA ~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 <label> (region colour ramp), poi-size <label> (POI marker size), and flow-width <label> (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 <group> 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):

    FlagTurns off
    no-legendThe whole legend block.
    no-coastlineCoastal water-lines (§24B.2).
    no-reliefMountain-range relief shading (§24B.2).
    no-context-labelsOrientation backdrop — water + unreferenced countries (§24B.2).
    no-region-labelsOn-map subdivision names.
    no-region-heat-valueThe 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-labelsOn-map POI labels.
    no-colorizeInferred political fills (§24B.2) — forces the plain green-land dress. A no-op under data (the basemap is already neutral-gray).
    no-citiesSubtle gazetteer city dots (§24B.2) — the faint orientation scatter across the basemap.
    no-cluster-poisCoincident-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 <ISO> 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 <alias> 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 <CODE> 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 "<place>" (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 (yorkNew York City; heathrowLHR). 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 <lat> <lon>) 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: <zone> (§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.

ConditionBehavior
Subdivision not in basemaperror + did-you-mean
POI name not in gazetteer & no coordserror + did-you-mean
Unknown three-letter (airport-shaped) tokenerror naming it an airport code + as <CODE> coordinates hint
Token resolves to a city but is also an IATA codecity wins; non-blocking info hint names the airport escape (as <CODE>)
heat: + tag on same regionboth kept as selectable colouring dimensions; active one fills (default: the heat ramp). No warning — §24B.4
Ambiguous bare city nameresolves most-populous in scope + info note
Duplicate region / POIlast-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 zonewarn W_MAP_CLOCK_TZ_NEEDED; pin renders without a card — add clock: <zone> (§24B.14)
clock: zone contradicts a named city’s real zonewarn W_MAP_CLOCK_TZ_OVERRIDE; the override is honored (§24B.14)
Unparseable clock: zonewarn 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 <n> parse-acceptance (the diverging-ramp seam below stays documented but is no longer parsed until built). Merged default-country + default-state into one locale <ISO> 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 — coastlineno-coastline, reliefno-relief, context-labels on|offno-context-labels, region-labels full|abbrev|offno-region-labels (now auto full → abbrev → hide, US-state abbrev only), poi-labels off|auto|allno-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-<feature> 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 <CODE> 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 <CODE> 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 (AustinAmerica/Chicago Central, not a nearest-zone guess).

  • clock: <zone> 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: <value> 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 <start>-<end> and workweek <range> (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 depsIntl.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):

    WarningCause
    W_MAP_CLOCK_TZ_NEEDEDa bare-coordinate clock pin with no derivable zone — add clock: <zone>
    W_MAP_CLOCK_TZ_OVERRIDEa named city’s clock: contradicts its real gazetteer zone — honored, but flagged
    W_MAP_CLOCK_TZ_INVALIDthe clock: value is an unparseable zone

25. Colon Usage Summary

Constructs Where Colons Are REQUIRED

ConstructDiagram TypeExample
Same-line metadataall 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 typesclass+ name: string
Class method returnsclass+ sail(): void
Function expressionsfunctionf(x): x^2 + 1
Hide tag valuesboxes-and-lineshide phase:Planning
Sort/lane tag selectorgantt, timelinesort tag:Team
Infra node propertiesinfralatency-ms: 50

Colons OPTIONAL

ConstructDiagram TypeExample
Class relationship labelclass`—
RACI role assignmentraciCap: A or Cap A (role marker lines)

Colons NOT USED

ConstructDiagram TypeExample
Chart type declarationallbar Title
Tag declarationsalltag Name as x
Boolean optionsallno-activations, fill-solid, no-title
Key-value optionsallstart-date 2026-03-15, active-tag Team
Series declarationsdata chartsseries A B C
Data rowsbar/line/pie/etcLabel 100
ER columnserid int pk
Sequence messagessequenceA -msg-> B
Groups/containersall[Group Name]
Section dividerssequence== Phase ==
Commentsall// comment
Wordcloud datawordcloudswordsmanship 95
Slope data rowsslopeBlackbeard 40 4
Slope period directiveslopeperiod 1715 1725
Venn intersectionsvennsw + nav Sea Raiders
Wireframe flagswireframe(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 typePreferred formRationale
sequenceSame-lineParticipants are declared once with all metadata; no rich indented-child grammar
infraIndented for node properties (latency-ms: 50, etc.); same-line for tagsNode properties use key: value like standard metadata
orgIndentedOrg hierarchies are deep and same-line metadata gets long
c4IndentedMulti-key metadata (description, tech, …) is long
erSame-lineTables have indented columns; attributes belong on the table declaration line
kanbanSame-lineCards are short; indented slot is for card detail prose
sitemapSame-lineIndented slot is for sub-pages
ganttSame-lineIndented slot is for dependencies (-> Next)
boxes-and-linesSame-lineIndented slot is for edges and descriptions
timelineSame-lineEvents are flat; no indented child concept
pertSame-lineIndented slot is for dependencies (-> Next); reserved-key/tag-alias metadata (confidence: low, tag aliases) may also appear there, intermixed with the arrows
mindmapSame-lineMindmaps are visual trees; indented attributes break scan-readability
tech-radarSame-lineIndented slot is for blips (under quadrants)
cycleSame-line for nodes; indented for descriptionsDescription prose is naturally multi-line
journey-mapSame-line for steps; indented for annotations (pain:/opportunity:/etc.)Existing convention
pyramid / ringSame-line for color; indented body for descriptionExisting trailing-token color shortcut applies
raciSame-line for phase headers and roles; indented for tasks/role-assignmentsChart-type-specific
wireframeTrailing-keyword-list (not key-value metadata)Flags are positional; indented form is the fallback for collision disambiguation
mapSame-line for 1–2 attributes; indented (one per line) for 3+§24B’s own form rule; directives are colon-free
treemapSame-lineSize is a bare trailing number; heat: and tag keys ride the node line
swimlaneSame-lineTag metadata rides node lines/references; indented slot is the lane’s nodes and edges
event-lineSame-lineTag values trail the event title; indented slot is description prose
version-controlSame-lineid:/tag:/type: ride the commit line; indented slot is commits under a branch
blockSame-line, OUTSIDE the bracket[Backend] l: Service, span: 2; indentation is a container’s sub-grid
sketchSame-lineshape:/at:/tags on the shape line; indented slot is edges
familySame-lineGEDCOM keys on the person line; indented slot is children
bodySame-lineTag value trails the part name; indented slot is notes
goal— (no element metadata)Values are space-separated key value directives (now, target)
bracketSame-lineTag 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 <label> + 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 assignmentkey: 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 assignmentCap: 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.

ConstructScope
Bare collapsed flag (legacy collapsed: true)see §1.8’s authoritative list
Same-line / indented metadata on element declarationsall chart types except flowchart and data charts (state takes tag-value keys only, decision #48); see §1.4 reserved-key registry
Trailing-keyword flag listwireframe only (§18.5)
progress: <N> keygantt only (§13.8)
score: <N> + emotion: <Word> keysjourney-map only (§22)
description: <text> shorthand for layerspyramid, ring (§23, §24)
milestone <name> keywordremoved — use <name> 0 (§13A.6)
columnsTwo 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: <label> commit keyversion-control only (§29.3) — a git ref pill, NOT a §1.3 tag group
`` operator as metadata delimiter

26.4 Quoted-Name Combination Limits

Quoted names cannot combine with as <alias> 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 <type>) 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 <type> (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 <name> (PERT) — replaced by <name> 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: <text> (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 <alias> on the same line.
  8. Sequence participants with alias or quoted names use is a <type>.
  9. No milestone keyword in pert — use <name> 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 <Name> [color]
  <node / edge lines…>

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)

TokenShape / meaning
Submit Claimtask (bare)
<Validate>exclusive (XOR) gateway
<+ Fork>parallel (AND) gateway
(Start)terminal (neutral)
(!Rejected)error terminal (! prefix → red)
(Paid) successtyped terminal — trailing word success (→ green) or terminate
[[Inspect Property]]subprocess (double border)

Delimiters are optional sugar in a flow reference — Validate<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:

TokenDiagnostic
<o …> inclusive gatewayinclusive gateway is fast-follow
<* …> event-based gatewayevent-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

CodeWhen
E_SWIMLANE_DUPLICATE_NODEa lane declares the same node name on two bare lines
E_SWIMLANE_UNKNOWN_NODEan edge references an undeclared bare task (only delimited endpoints auto-create), or a Lane.Node qualifier resolves nowhere
E_SWIMLANE_AMBIGUOUS_NODEwarning — a bare reference matches nodes in more than one lane; qualify it Lane.Node (resolved to the first match otherwise)
E_SWIMLANE_UNKNOWN_LANEa node appears outside any lane block
E_SWIMLANE_NO_LANESa [Phase] is declared but no lanes exist
E_SWIMLANE_UNSUPPORTEDa 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
  <Review>
    -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 <Review> 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.

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 (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-02Sep 2, 2008; 753 BCE753 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 TBDs 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 <Group> as <alias> 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):

DirectiveEffect
no-scalespace events evenly instead of by date (date subtitles still print; the spine ruler is omitted)
side above / side belowplace all cards on one side instead of alternating; side alternate is the default (omit it). Keyword+value form
no-boxcard-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-legendhide the tag legend
now / now <date>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 <date> 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:

TokenDiagnostic
direction-tbvertical 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 <Name>superseded by the [Name] era delimiter (§28.6a); the diagnostic points there
a trailing ^ / v on an eventper-event side override is fast-follow

28.8 Diagnostics

CodeWhen
E_EVENT_LINE_NO_EVENTSdeclaration with no event lines
E_EVENT_LINE_BAD_DATEa date-looking prefix that isn’t valid ISO
E_EVENT_LINE_UNSUPPORTEDa fast-follow token (§28.7)
E_EVENT_LINE_ERA_DATE_ORDERwarning — 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 <parent> — 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 tokenhotfix 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: <sha> — 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: <label> — 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 | reversenormal = 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 <source> [id:] [tag:] [type:] [squash|ff|no-ff] — joins <source>’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 <commit-ref> [parent:] — a dashed copy-edge from the referenced commit onto the active lane. parent: <ref> 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):

DirectiveEffect
direction-lrhorizontal lanes, newest right (default, the iconic look); boolean per §1.9 (key+value direction LR accepted legacy)
direction-tbcolumn lanes, newest down (the git log view); boolean per §1.9 (key+value direction TB accepted legacy)
no-labelshide commit messages (Mermaid showCommitLabel: false)
no-laneshide branch lane lines (Mermaid showBranches: false)
no-headsuppress 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 <name> at <commit-ref> [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 <commit> 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 <branch> onto <target> — replays <branch>’s post-divergence commits onto <target>’s tip; the originals render as faded ghost commits and the solid copies are linked by dashed move-arrows.
  • reset <branch> to <commit-ref> — moves the branch pointer back; commits after the target become faded ghosts (orphaned).
  • revert <commit-ref> — adds an inverse commit (type: reverse styling, message Revert <msg>) 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 <text> — 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:

TokenDiagnostic
auto-idhash + display every commit (the dense git log look) is fast-follow
stash / conflict / PR-overlay / diffstattier-3 git features are fast-follow
a stateful checkout / switch keywordthe linear Mermaid-mirror mode is fast-follow

Repo import (git logversion-control source) is a tooling fast-follow, not syntax.

29.12 Diagnostics

CodeWhen
E_VERSION_CONTROL_NO_COMMITSdeclaration with no commits
E_VERSION_CONTROL_UNKNOWN_BRANCHfrom/merge/rebase/reset names a branch with no tip
E_VERSION_CONTROL_AMBIGUOUS_REFa commit ref matches more than one commit (require id:)
E_VERSION_CONTROL_CYCLEa cycle in the commit DAG (self-merge / bad from)
E_VERSION_CONTROL_UNSUPPORTEDa 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 / tokenEffect
span: NColumn span (integer ≥ 1; clamps to the column count)
collapsedBare flag — start this container folded
<tag>: VTag value via the group’s alias or name (cascades / overrides)
columns NDirective — override a grid’s inferred column count
no-legendDirective — hide the tag legend

30.6 Diagnostics

CodeWhen
E_TAG_DECLARED_AFTER_CONTENTa 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:

FormMeaning
-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 <- BB -> 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 <group>: 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 / tokenApplies toEffect
shape: <kind>shapeMorph from the default rectangle (closed shape set; type shows as a header badge)
at: C Rshape, boxHalf-slot position (optional; omit → flow auto-place)
as <alias>shape, boxReference handle for edges (§2A)
collapsedboxBare flag — start folded to a virtual node card
<tag>: Vshape, box, edge tailTag value (cascades from box; individual overrides; colors edge lines)
no-legenddirectiveHide the tag legend

31.8 Diagnostics

CodeWhen
E_SKETCH_NESTED_BOXa [Box] inside a [Box] — inner box degrades to flat children
E_SKETCH_AMBIGUOUS_TARGETan edge targets a bare label shared by multiple shapes
W_SKETCH_UNKNOWN_SHAPEunknown shape: value — falls back to rectangle
W_SKETCH_OVERLAP_RESOLVEDhand-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: <year> 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.

LevelKeyValueRenders as
personsexm | fdrives the default card color (§32.7)
personbbirth yearleft end of the year range
personddeath yearright end of the year range
personbpbirth placedetail line
persondpdeath placedetail line
personoccupationfree textdetail line
personmilitaryfree textdetail line
personeducationfree textdetail line
personreligionfree textdetail line
personburialfree textdetail line
unionmmarriage yearmarriage-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
mblue
fpurple
unsetgray

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 <group>: 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

CodeSeverityFires when
W_FAMILY_UNKNOWN_KEYwarningA metadata key outside the §32.6 fixed set (and not a declared tag alias) is used — the pair is ignored.
W_FAMILY_SIDE_METADATAwarningA key: value pair appears on one side of a union (per-side metadata unsupported) — declare the partner standalone (§32.3).
W_FAMILY_HIGHLIGHT_UNKNOWNwarningA highlight <name> 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 <name>)

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 <name> 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:

FacetTokensDefault
formmuscle | skinmuscle
sexmale | femalemale
viewfront | backfront
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 <alias>: <Value>; 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 <n> — current value (required; ≥ 0). Missing → treated as 0 with a warning.
  • target <n> — 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.

FlagFace
(none)Progress bar — horizontal rounded fill (default).
thermometerVertical tube + bulb; the column rises with progress.
gaugeSemicircular 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 = "<Title>: <now> of <target> (<pct>%)".

34.5 Color

By default the fill is auto traffic-light by completion: < 50% red, 50–80% orange, ≥ 80% green (over-target stays green). Precedence:

  1. An explicit trailing color token on the title line (goal Marathon Fund ($) green, §1.5) always wins.
  2. Otherwise the auto band color (needs a target).
  3. no-auto-color disables the bands and falls back to the palette series color.

The fill is a 25% tint of the resolved color; fill-solid opts into full saturation; fill-outline hollows the meter — the band color moves to the region’s rim and advancement reads from the outlined extent against the gray track (decision #47).

34.6 Note (description block)

An optional note adds a free-text caption. Two forms — inline (note <text>) or a block header followed by an indented body (multi-line, may be indented):

goal Grog Barrel Fill (L)
thermometer
now 34
target 50
note
  **Great job, crew!** Still waiting on tallies from:
  - Seattle — first mate says "soon"
  - Columbus — *almost there!*
  Top us off to reach the goal.

The body supports simple inline markdown (**bold**, *italic*, `code`), - /* bullets, and blank-line gaps. It renders beside/below the face (left column for thermometer & gauge, under the bar). no-notes suppresses it even when authored (singular no-note accepted as a silent alias).

34.7 Options

DirectiveEffect
no-percentHide the % label.
no-valueHide the raw now / target label.
fill-solidFull-saturation fill instead of the 25% tint (fill-outline hollows the meter — color on the rim, §1.9).
no-titleHide the banner title.
no-notesSuppress the note block even if authored (singular no-note accepted as a silent alias).
no-auto-colorDisable traffic-light bands; use palette color.

35. Bracket Charts

A single-elimination tournament bracket. Winners auto-advance up a tree that builds itself from the results — a one-sided ladder for a simple pool, or two [Side] columns that mirror inward and meet at a championship (MLB / NBA / NCAA-style). Game night, a fantasy league, “best taco in town.” Detection is by the first-line token (bracket) — there is no content sniffer (looksLike*). Two authoring modes: casual (results-only, structure inferred) and seeded (declare the field → the full day-0 skeleton renders before a single game is played).

35.1 Declaration

bracket [Title]

Space-separated, no colon; title optional. A trailing color token on the title line sets the winner accent (§1.5 title-line accent slot).

35.2 Matches (beats / vs)

The body is a list of match lines. Names are split on the beats / vs keyword (surrounded by spaces), so competitor names may contain spaces.

// decided — WINNER on the left, score cosmetic
Yankees beats Guardians 2-1
// pending — no winner yet
Astros vs Rangers
  • <Winner> beats <Loser> [score] — a decided match. The winner is always the left operand, regardless of the score. The optional trailing \d+-\d+ (ASCII hyphen or en-dash) is a cosmetic annotation; a score that shows the winner lower emits a warning but keeps the declared winner.
  • <A> vs <B> — a pending, undecided match: both boxes render with no winner emphasis and no downstream advancement.

Competitor names are identifiers — the same string is the same competitor climbing a round. Duplicate distinct entrants (e.g. two seed lines with the same name) are an error with a rename hint. A competitor appearing as a winner in two later matches is ambiguous → warning, first-in-source-order wins.

35.3 Seeded mode (seed, rounds)

Any seed line switches on seeded mode: the standard single-elimination skeleton is generated from the seed count and every slot renders on day 0, unplayed ones as dashed TBD boxes that fill as beats lines arrive.

bracket World Series
rounds Wild Card, Division, Championship

[American League]
  // top seeds get a first-round bye
  seed 1 Blue Jays
  seed 2 Mariners
  seed 3 Yankees
  seed 4 Red Sox
  seed 5 Tigers
  seed 6 Guardians
  Yankees beats Guardians 2-1
  ...

[National League]
  seed 1 Brewers
  ...

// indent-0, outside any side = the championship
Blue Jays beats Brewers 4-3
  • seed N [Name] — declare a seeded entrant. Seeding follows the standard recursive 1-vs-N bracket (a six-team side pairs 3v6 / 4v5 in round 1; seeds 1–2 bye to round 2 — the fixed MLB Division-Series positions, no reseed).
  • rounds A, B, C — comma-separated column names, entry round → inner. Absent → generic Round N / Final. In a two-sided bracket the center column is labeled with the title (the deciding game).

35.4 Sides & championship ([Side])

[Side] [color] is a bracket-column header (the kanban [Column] idiom, §11), with an optional trailing color token that tints that half. Indented match/seed lines belong to the side above them.

  • 0 or 1 side → a single ladder growing one way; the last match is the final.
  • 2 sides → the two halves mirror left/right and meet at a center championship — an indent-0 match (outside any [Side]) linking the two side winners.

35.5 Round inference (casual mode)

With no seeds, the structure is inferred: a match feeds a later match when its winner reappears there. A match’s round = 1 + max(round of the ≤2 feeding matches); a participant with no feeding match is an entrant at that round (round > 1 ⇒ a bye — a competitor whose first appearance is a later round simply never played round 1). Round-1 matches are placed top→bottom in source order; each parent sits at the mean y of its feeders (the classic bracket midpoint).

35.6 Tags, commentary, home & seeds

Two visual channels stay independent: fill intensity encodes advancement (winner = faded tint, loser = empty, TBD = dashed — never authored) and the box outline encodes an author attribute via the tag-group idiom.

  • Tag groups (tag Group [as g] + indented Value color, exactly as block/kanban §30/§11). A competitor carries a tag value via §1.4 same-line metadata on its seed line (seed 1 Blue Jays tk: MLB Ballpark) or, in casual mode, a standalone roster line (Blue Jays tk: MLB Ballpark — a name + metadata, no beats/vs). The tag colors that competitor’s outline; a legend renders and no-legend hides it (the coloring stays).
  • Commentary — prose lines indented deeper than a match become a caption under it (event-line §28 idiom): a leading - becomes a bullet, and **bold** / *italic* / [links](url) render inline.
  • Home/away<W> beats <L> [score] @ <Home> marks the host; in seeded mode the higher seed (lower number) is home automatically, shown with a small house glyph.
  • Seed badges — seeded competitors show their seed number beside the name.
  • Upset — in seeded mode, a win by a higher seed-number over a lower one is auto-flagged. Champion gets a star + heavier outline.
bracket AL Playoffs
rounds Wild Card, Division, Championship

tag Ticketing as tk
  MLB Ballpark blue
  Ticketmaster orange

[American League]
  seed 1 Blue Jays tk: MLB Ballpark
  seed 6 Guardians tk: MLB Ballpark
  Guardians beats Yankees 2-1
    Walk-off in the 11th — **Ramírez** off the foul pole.
    - Series MVP: Ramírez
  Red Sox beats Tigers 2-1 @ Red Sox

35.7 Format & out of scope

  • single-elim (default) / double-elim / seeded — bare-flag format directives (like treemap’s radial). double-elim is reserved: it parses but emits a “not yet supported” diagnostic and renders the winners’ side only. A losers’-bracket grammar is deferred (the positional A beats B form can’t express a team continuing after a loss).
  • accent <color> — legacy alias for the title-line trailing color (decision #48); the title-line token wins on conflict. A [Side] color or a competitor tag still wins per-box.
  • no-round — suppress the round/column labels. no-legend — hide the tag legend (outlines stay colored).
  • Keywords: beats / vs (infix match operators), rounds, seed, tag, accent, as (tag alias), @ (home marker).
  • 4-region (NCAA) four-column layouts, round-robin / group stages, and best-of-N series detail are out of scope for v1.

36. Countdown Charts

The only dynamic dgmo chart: “N until X” recomputed against the viewer’s clock on every load and ticking every second on any live surface. Distinct from goal (static, §34) — a countdown counts down to a future instant (and, for a timed one-shot, keeps ticking up past it — §36.7); static faces measure now against target. A block has either a one-shot target or a recurring every rule, never both. Below the header it draws a calendar band that tightens resolution as the target nears (§36.6). See docs/spikes/countdown-live-rendering-spike.md for how it renders live on all surfaces.

The target/from date follows the liberal grammar + year ladder in §2B Universal Date Handling; now and tz-pinned instants (§36.2) are unaffected.

36.1 Declaration

countdown Trip to Japan
target 2026-08-21

Requires the explicit first line countdown — no content inference (house rule for new chart types). The title carries the event label; a trailing color token (§1.5) tints the figure.

36.2 One-shot target (space-separated key value, no colon)

  • target <iso> — the future instant. Accepts a bare date (2026-08-21), a datetime (2026-08-21T18:00), or a datetime with a tz offset. The literal now resolves to render-time (→ immediately expired; for testing). A bare date counts to midnight; a datetime with no offset is interpreted at that wall-clock time — both in the tz zone if one is set, otherwise viewer-local. An explicit offset is always honored as an absolute instant.
  • tz <IANA>pin the authored wall-clock times to a zone (tz America/New_York, tz Asia/Kolkata, tz UTC). Space-separated (no colon), an IANA identifier like the clock chart’s zones. With tz set the target (bare date, offset-free datetime, or recurring at) resolves to an absolute instant, so a shared page shows every viewer the same remaining time regardless of their OS clock — the count does not drift when you move the laptop to another zone. The footer then prints the resolved instant in that zone with a UTC-offset tag (Fri Aug 21 2026 · 18:00 · UTC−4). Omitted → viewer-local (the default), where the same source shows each viewer their own local time. An unknown zone warns and falls back to viewer-local. A target carrying its own ISO offset is already absolute, so tz doesn’t move it (it still labels the footer).
  • expired <text> — shown once the target passes; the tick stops (freezes this message instead of counting up — §36.7). Without it, a passed timed one-shot counts up. One-shot only — recurring blocks roll forward instead.

36.3 Recurring rule (fixed-slot grammar — NOT free prose)

A recurring block resolves to the next matching instant and rolls forward automatically when it passes — birthdays, anniversaries, standing meetings never go stale. The rule is a single line: every <cadence> [on <instant>] [at <time>] [from <anchor>] (each slot may also sit on its own line).

countdown Denver JS meetup
every month on 3rd Tuesday at 18:00
on-day Tonight! 🍕
SlotValuesNotes
everyyear | month | week | N days|weeks|monthsthe cadence
onAug 21 (month+day, with every year) | 3rd Tuesday / last Friday (nth/last weekday, with every month) | Friday (weekday, with every week)which instant within the cadence
at18:00 — also accepts am/pm forms (6pm, 6:30pm), normalized to 24h (decision #48)default midnight
from2026-07-03 — anchor dateinterval cadences (every N …) only
  • Weekday & month names are a closed vocabulary (full or 3-letter) → editor autocomplete; a typo is a named error, not a silent wrong date.
  • Single nth/last weekday only. No 2nd and 4th Wednesday / multi-weekday selectors (the iCal RRULE tar pit — deferred, §36.7).
  • Timezone is viewer-local unless pinned. By default 18:00 is 18:00 on the viewer’s clock (a shared cross-tz page shows each viewer their own local time). Add tz <IANA> (§36.2) to pin at/on to a fixed zone so every viewer counts to the same instant.
  • Free prose is rejected with a fix suggestion — every Friday 6pm✕ bare weekday needs a cadence, ↳ every week on Friday at 18:00.
  • on-day <text> — label shown on the occurrence day; roll-forward resumes after.
  • Editor assist. The every line is cursor-position aware: completion offers the cadence after every, month names after every year on, ordinal+weekday after every month on, a weekday after every week on, the on/at/from connectors after a cadence, and time/date hints after at/from; the sub-keywords and closed instant vocab highlight distinctly (context-gated to countdown, so on/at/from never light up as keywords elsewhere).

36.4 Ordinal (since) — recurring blocks

since numbers the countdown (a 7th anniversary). Math is always resolvedYear − since.

  • since <year> — anchors the ordinal.
  • since-label <template> — free-form eyebrow template where Nth → the ordinal word (7th) and N → the bare number (7): since-label Nth Anniversary → “7TH ANNIVERSARY”, since-label Year N → “YEAR 7”. Any phrasing works (“Nth Time Around the Sun”). Defaults to Nth <title>. Tokens are case-sensitive, so ordinary words containing “n” are untouched.

36.5 Count units — the hero

  • units <human|days|full|clock|weeks|words>human (default) the natural, calendar-aware phrase: the coarse top two units including years (“1 year, 2 months”) as the hero, with the finer remainder (“3 days”) in a small muted sub-line beneath. Leading zero units are dropped (“2 months, 4 days”, never “0 years, …”) and the hero auto-shrinks so a long phrase never collides with the title. All-day (no-time) targets floor to whole days — the human hero reads a flat “8 days”, never false sub-day precision (“7 days, 14 hours”), since a bare date counts to midnight; only a timed target shows hours/minutes in the sub-line. days whole days (“400 days”) for people who want the raw number; full ticks Nd HH:MM:SS; clock total HH:MM:SS (may exceed 24h); weeks; words loose English. full/clock bake the day count and only tick sub-day on live surfaces.
  • round <up|down|nearest> — ceil (default) / floor / round for day & week modes. up keeps a target later today at “1”.
  • fields <d,h,m,s> — which full-mode segments show (drop s for a calm widget).
  • lang <en> — locale for phrasing + month names (English at launch).

36.6 The calendar band (default-on; no-visual to suppress)

Below the header, every date-bearing countdown draws a calendar band — a fixed-height strip whose resolution auto-tightens as the target nears, so the same “you-are-here → event” reading holds from years out to the final week. The tier is chosen from the span to the target:

Span to targetBand
> 3 yearsyears-strip — decade-aligned rows, one cell per year (not per month). Years outside the now→target span appear dimmed so each row is a full, gridded decade. Scales from a handful of years to centuries: for very long spans the cells shrink (and year labels drop) rather than stacking a fresh row per year — so a 70-year (or 500-year) target reads as one bounded gradient field, not a wall of empty month-rows
1–3 yearsyear blocks — one row of 12 month rectangles per year
~3–12 monthsmonth rectangles — one rectangle per month (the year-block idiom, un-grouped), muted/role fill, only the now- and target-months dated
~3 weeks – 3 monthsmonth calendars — real side-by-side month grids at full height, padded with dimmed context months on either side so each column lands at a natural calendar width (never stretched or row-cropped)
≤ 18 daysday-strip — one stretchy linear row, a cell per day from today (leftmost) to the event (rightmost); weekday labels drop and cells compact as the span grows
  • Color encodes distance, not decoration. Two live anchors sit on a bed of inert gray: now is a fixed cold blue (you-are-here; shifted to teal if the accent is itself blue, so the two never merge) and the target is a hot accentred by default, or the trailing color token (§1.5) if set. Every cell strictly between them is a per-cell now→target blend (cold → hot), so a cell’s color reads as how far along the journey it sits. There is no separate traffic-light ramp — the gradient itself is the urgency signal.
  • The target pops. The event cell carries a bright halo ring split from it by a background gap, so it reads as the destination even when the ramp’s approach runs warm.
  • Everything outside the span is one inert gray (elapsed days and empty/context days alike). The since/anniversary/tenure ordinal wears a calm greentime banked, the positive mirror of the red countdown.
  • On the day-strip: past the event is never shown; the row is exactly today→event, the gradient is the path. On the calendars: only the now→target run is colored; padding months are dimmed context that give the grid natural proportions.
  • The band is a fixed vertical reservation (a constant multiple of the header height), identical across tiers; each tier fills it — widening cells or adding/dropping detail — rather than floating in dead space.
  • Cells default to the canonical 25% tint (dark text, the hue kept saturated only on the now/target anchor borders and the target halo) like every other dgmo chart type — the now→target hue gradient still reads at low ink. The bare-flag fill-solid directive (§1) opts into full-saturation chips with white text; fill-outline renders outline-only chips. The final-day ring gauges (§36.7) are unaffected: their swept arc is a meter, not a fill, so it stays saturated in either mode.
  • no-visual — bare-flag directive that suppresses the band; the chart collapses to the header (title · resolution line · hero). An all-day target on its own day likewise shows no band — the day itself is the message.

36.7 Timed targets — the pivot

When the target carries a time (2026-08-21T18:00, or a recurring at 18:00), the event instant is a pivot, not a stop:

  • days out — the human hero keeps its calendar phrase; a live HH:MM:SS clock rides the sub-line.
  • final day — the hero becomes the ticking clock and the band becomes three ring gauges (hours · minutes · seconds): a track plus a swept arc and a big numeral each. On the final day everything is imminent, so all three arcs sweep the hot accent; the seconds hand reads as live by its motion, not a borrowed hue. It fills the band where a 24-hour cell strip would sit mostly empty.
  • after the instant — the same clock and rings keep ticking up; the caption flips to goago and the now-marker crosses to the far side of the event. One continuous life: days → hours → through the instant → hours-ago → days-ago.

Count-up is thus a first-class part of a one-shot’s life. expired <text> still wins when set — it freezes the tick with a fixed message instead of counting up (§36.2).

The renderer bakes a <text data-dgmo-countdown="…"> hero (the no-JS floor) carrying structured data-dgmo-* (-units/-round/-fields/-expired, -recur-* for roll-forward, -since, -asof). It never emits a <script> (every sanitizer strips it). Two self-dating guarantees ride in the SVG on every surface, including images:

  • an in-chart footer resolution line→ Tue Jul 21 2026 · 18:00 · in 8 days — so a wrong rule shows a visibly wrong date;
  • an “as of” freshness stamp the ticker erases on its first tick — on a live surface it vanishes instantly; on an image no tick fires, so the picture stays honestly dated.

The calendar band and ring gauges are baked at render time (recomputed on load) and share the ticker: the now-marker advances and the day/gauge values recompute each second alongside the hero. A page-level ticker (single-sourced in dgmo/src/countdown/ticker.ts + resolve.ts, wired into the auto/element drop-ins, remark’s bindDgmo, the desktop app, and the Obsidian plugin) scans [data-dgmo-countdown], recomputes each second, and rolls recurring nodes forward — accurate on every load with zero persisted state. CLI export of a countdown to a raster/.svg file prints ⚠ countdown is dynamic — this file is a snapshot as of <date>.

36.9 Out of scope

Full iCal RRULE (multi-weekday, BYSETPOS, exceptions); locales beyond en; multiple targets in one chart; real-time sync or persistence — everything derives from the source target/rule. (The tz slot — §36.2 — pins the count to a zone; a per-viewer “in your zone” relabel of a pinned instant is still out of scope.)

37. Clock Charts

The second dynamic dgmo chart (after §36 countdown): a set of world clocks showing the current wall-clock time for people and places across time zones, ticking every second on any live surface and correct on every load. Where a countdown counts down toward one future instant, a clock reads now simultaneously across many zones — “what time is it for the London team right now, and are they at their desks?”. Category life. Zero runtime dependencies: time, UTC offset and weekday come from the built-in Intl.DateTimeFormat (IANA-aware, so DST is correct for free), the optional sundown line from SunCalc-style solar math, and the zone→coordinate lookup from a bundled, trimmed IANA table.

37.1 Declaration

clock Crew standups
analog
hours 9-17
workweek mon-fri

London as UK team
New York as Dani (NY)
Los Angeles as West coast

Requires the explicit first line clock — no content inference (house rule for new chart types); title optional. The title, when present, renders as the block heading; there is no icon before it. The syntax is flat — nothing is indented under clock, and no directive or entry uses a colon. Directives and entries are order-independent: any non-blank line whose first token is an option keyword (analog, hours, workweek, no-sun, time-24, no-title, direction-tb/direction-lr, color-by; the legacy days and direction too) is a directive; every other line is an entry. A single-entry clock (one zone, no directives) is the common case.

Each entry carries one anchor — the thing that names its zone — followed by an optional as <label>. The anchor is one of three forms (§37.3): a city name you type the way you’d say it (London), an explicit IANA id (Europe/London), or a raw UTC offset (UTC+5:30). There is no separate place-then-zone pair; you name the zone once.

37.2 Directives (global; space-separated, no colon)

  • analog — a bare flag that draws each row’s clock as a round analog dial with hour/minute/second hands. The default face is digital (a numeric readout). It is strictly either/or — a row never shows both faces — and the clock always sits on the left of the row.
  • hours <start>-<end> — an optional working window (9-17). 24-hour by default, it also accepts HH:MM and am/pm (8:30-17:15, 9am-6pm). Applied in each row’s own zone, it drives the status chip (see §37.4). Reads as a generic “availability window”, reusable for office hours, a shift, or quiet hours.
  • workweek <start>-<end> — an optional working-days window (mon-fri; days is accepted as a legacy alias). Weekday names are a closed 3-letter vocabulary. Outside these days a row’s chip is off (grey).
  • no-sun — a bare flag that hides the secondary solar line. The line is on by default: each row shows a sundown/sunrise line (“27m to sundown”) beneath its offset unless no-sun is set.
  • time-24 — a bare flag switching digital readouts and printed times to 24-hour. The default is 12-hour with am/pm.
  • no-title — a bare flag that suppresses the board title.
  • direction-tb (rows — the default vertical stack) / direction-lr (columns — the panels laid out in a row as a horizontal strip). Booleans per §1.9; the old direction <tb|lr> key+value and the columns alias are accepted legacy.
  • color-by <place|work|daylight|time|none> — which dimension colors the zones (see §37.5). Default is place; color-by none goes neutral.

37.3 Entries (the zones)

Each entry is <anchor…> [as <label…>] [color]. The anchor is resolved in this fixed order, and the first form that matches wins:

  1. Starts UTC or GMT → a fixed offset (§37.3a). UTC, UTC+1, UTC-7, UTC+5:30, GMT+2. GMT is an exact synonym of UTC.
  2. Contains a / → an explicit IANA id (Europe/London, America/Argentina/Buenos_Aires). The escape hatch for ambiguous or obscure places.
  3. Otherwise → a city name, resolved through the bundled gazetteer (§37.3b): exact city → curated alias → an ambiguous error listing the candidates → an unknown warning with a did-you-mean.
  • as <label…> is optional and gives the row an alias (the person or team — as Dani (NY)). When omitted the alias defaults to the resolved city (for a city/IANA anchor) or the offset label (for a fixed offset).
  • A trailing palette color token (§1.5) pins the row’s shade — London as UK team purple, or London purple with no alias.

You type the zone once. London as UK team replaces the old London Europe/London as UK team; the city resolves to Europe/London and the label leads.

37.3a Fixed UTC offsets (no DST)

A raw UTC±HH:MM anchor is a fixed offset: it is computed straight off UTC and never observes daylight saving. Use it for UTC itself, for regions that don’t shift (e.g. India UTC+5:30), or for a quick throwaway. Accepted forms: bare UTC/GMT (→ UTC+0), UTC+1, UTC-7, UTC+5:30, and the unpunctuated UTC+0530. The range is clamped to the real span (−12:00 … +14:00); anything outside errors.

Because a fixed offset has no location, a fixed row has no sundown/sunrise line and cannot be colored by daylight. It renders with a small “no DST” marker so the reading is honest: UTC-5 is right for New York in January and wrong in July — the marker says so rather than the parser blocking it. A hours/workweek window still applies, evaluated against the frozen offset.

37.3b City gazetteer & discoverability

A bare place name is matched against a bundled, trimmed gazetteer (reverse-indexed from the same IANA table that backs the sun line, plus curated colloquial aliases — NYC, LA, Bombay, Kyiv/Kiev, London, ON). Matching is accent- and case-insensitive.

  • Exact / alias hit → resolves; the row displays the canonical city (nyc → “New York”, Bombay → “Kolkata”).
  • Ambiguous (a name that maps to more than one zone, e.g. San Jose) → an error listing the candidate IANA zones so you can disambiguate with form 2.
  • Unknown → the row is skipped with a warning, including a did-you-mean (nearest city by edit distance) and a pointer to the IANA / UTC escape hatches.

This gazetteer is also the autocomplete index: in the app and web editor, the anchor gets a live combobox (city → resolved IANA id → current offset), so you discover Europe/London by typing Lon, not by memorizing it.

37.4 Layout

Each row is three columns:

  • Left — the clock face: a digital readout (with live :SS, the default) or an analog dial (with a live second-hand) when the analog flag is set.
  • Middle — the alias, the place, and the current UTC offset (e.g. UTC−7), resolved live from Intl so DST is always right.
  • Right — a working-hours status chip plus the optional sundown line. The chip is green when the row is inside its hours/workweek window, amber when the window is starting soon, and grey when the row is off-hours or on a non-working day.

37.5 Coloring (color-by)

Clock boards are colorized by default — colorful out of the box. color-by <dimension> chooses which dimension the zone colors encode; omitting the directive entirely is the same as color-by place, and color-by none goes neutral. The five forms are color-by place | color-by work | color-by daylight | color-by time | color-by none.

  • place (default) — each place gets the next palette accent: an identity color with no meaning, like a legend’s series colors. Use it when you just want the rows visually distinct.
  • worksemantic, availability: green while a zone is inside its hours/workweek window, amber in the last hour before it closes, grey when off. Needs hours to be set — without it the board stays neutral.
  • daylightsemantic, light: warm where the sun is currently up, cool where it is down, using the same sunrise/sundown solar math as the sun line (§37.2). Renamed from day to avoid clashing with the (now-legacy) days alias of workweek.
  • timesemantic, local hour: a continuous dawn → midday → dusk → night hue ramp keyed to each zone’s local hour. Order the zones west-to-east to watch the day sweep across the board.
  • none — neutral greyscale.

Where the color lands. The zone’s time and label render in the resolved solid color; its lane — the row, or the column under direction-lr — gets a soft tint of that same color. This holds on both faces and both layouts: on the analog dial the face is tinted and the ring + second-hand take the color while the hour and minute hands stay ink.

Precedence. A hand-set per-zone defined shade always wins over the dimension for that zone — color-by only fills the zones you did not color yourself. The defined shade is the ordinary trailing color token (§1.5) on the entry line — London Europe/London as UK team purple — so you can set color-by daylight for the board and still pin one zone to purple.

37.6 Live rendering & runtime

Clocks tick live through the same bake + ticker machinery as countdown: the renderer bakes the current time, offset, weekday and status into the SVG (structured data-dgmo-* attributes, never a <script> — every sanitizer strips one), and a single-sourced page-level ticker recomputes each row every second on all live surfaces (the auto/element drop-ins, remark’s bindDgmo, the desktop app, and the Obsidian plugin). Images and CLI raster/.svg exports keep the baked snapshot. All computation is zero-dependency: Intl.DateTimeFormat for time/offset/weekday and solar math for the sundown line. When a zone has no known coordinates in the bundled table, the clock still renders normally — it simply omits the sun line for that row rather than erroring.

Because every row formats through its own IANA zone (not the host’s), a clock is viewer-independent: moving the laptop to another zone does not change any displayed time. (Contrast the default countdown, which is viewer-local and only becomes viewer-independent once its tz slot pins it — §36.2.)

37.7 Out of scope

Per-row working-hours overrides (the hours/workweek windows are global for now — a per-entry override is a deferred follow-up); locales beyond en; a shared “meeting overlap” band across rows. (Raw UTC±HH:MM offsets, previously out of scope, are now a first-class anchor — §37.3a.)