DGMO Language Reference
Complete syntax documentation for every chart type, directive, and option.
/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,
chordfolded intoarc+layout chordin 0.44). Retired legacy/ambiguous forms include: baredescription, PERTanalysis, C4 bare same-line tail, timeline positional duration (→duration:), gantt legacy scheduling, and the standalonechord/ 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 thedescription:fix and the diagram does not render). Seedgmo-language-spec-decisions.md§ “1.0 Freeze Decisions”.Diagnostic codes (0.43.0, decision #28): the dedicated
E_*_REMOVEDfamily was deleted for minimal surface — removed forms fail however they naturally fail, not via a named code. Treat everyE_*_REMOVEDreference 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
- Universal Constructs
- Universal Name Handling
- Sequence Diagrams
- Infrastructure Diagrams
- Flowchart Diagrams
- State Diagrams
- Org Charts
- C4 Architecture Diagrams
- Entity-Relationship Diagrams
- Class Diagrams
- Kanban Boards
- Sitemap Diagrams
- Gantt Charts
- Boxes and Lines Diagrams
- Timeline Diagrams
- Data Charts
- Visualizations
- Mindmap Diagrams
- Wireframe Diagrams
- Tech Radar Diagrams
- Cycle Diagrams
- Journey Map Diagrams
- Pyramid Diagrams
- Ring Diagrams
- Colon Usage Summary
- Authoring Rules (Generators Read This First)
- Swimlane Diagrams
- Event Line Diagrams
- Version-Control Diagrams
- Block Diagrams
- Sketch Diagrams
- 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
tagkeyword, NO colon- Name: a single identifier (
[A-Za-z_][A-Za-z0-9_-]*— letters, digits, underscore, hyphen), e.g.Team,Trust-Zone. For a multi-word display name with spaces, quote it:tag "Trust Zone" as tz. The quoted text becomes the legend label; internally it slugs to a DOM-safe key (Trust Zone→trust-zone) used for assignment, matching, andactive-tag. Always pair a quoted name with anas <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, notHigh 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), soHigh/Low redgivesHigha non-red color. An explicitValue coloralways 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
noneis 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-tagis 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 nonesuppresses all tag colouring — every node renders neutral and all groups stay collapsed. This is the opposite of omittingactive-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 explicitaliaskeyword (tag Priority alias p) emitE_TAG_SHORTHAND_REMOVEDper 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_TYPESindgmo/src/completion.ts(derived fromSTRUCTURAL_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 type | Reserved keys (in addition to declared tag aliases) |
|---|---|
| sequence | color, description, role, collapsed, position |
| infra | color, description, collapsed, icon |
| flowchart | (no metadata) |
| state | (tag-group aliases — tags newly granted, decision #48) |
| org | color, description, role, location, email, phone, plus per-chart custom keys (free-form indented attributes) |
| c4 | color, description, tech, type, collapsed |
| er | color, description, domain |
| class | color, description |
| kanban | color, description, wip, assignee, due, priority |
| sitemap | color, description, status |
| gantt | color, description, duration, offset, progress, start |
| pert | color, description, confidence, collapsed |
| boxes-and-lines | color, description, heat |
| timeline | color, description, duration |
| mindmap | color, description, collapsed, plus per-chart priorities/status keys declared as tag aliases |
| tech-radar | color, description, quadrant, ring, trend |
| cycle | color, span, description (width is edge-only) |
| journey-map | score, emotion, description, pain, opportunity, thought, color (decision #13, persona long form) |
| pyramid | color, description |
| ring | color, description |
| raci | color, description, plus declared roles (which dispatch as role assignments, not attributes) |
| wireframe | (uses trailing-keyword flag list — see §19.5) |
| treemap | heat, plus tag-group aliases |
| swimlane | (tag-group aliases; note: deferred) |
| event-line | (tag aliases; collapsed via era lines) |
| version-control | id, tag, type, order |
| block | span |
| sketch | shape, at |
| family | closed GEDCOM set — see §32.6 |
| body | (tag aliases) |
| bracket | (tag aliases; seed/score keys per §35) |
| map | registry in §24B.9 (heat, size, width, label, style, clock) |
| goal | (no metadata) |
| countdown | (no metadata) |
| clock | (no metadata) |
Keys not in a chart type’s registry are not metadata. Tag aliases declared via tag Group as x are added to that chart’s effective registry for the duration of the parse.
1.4.4 Diagnostic catalog
The
*_REMOVEDrows below were deleted in 0.43.0 (#28) and no longer fire — see the 0.43.0 update note at the top of this spec. They are kept here, marked, for historical reference.
| Code | Severity | Fires when |
|---|---|---|
E_PIPE_OPERATOR_REMOVED | removed 0.43.0 | (Historical) A ` |
W_EMPTY_METADATA_VALUE | warning | A metadata pair has an empty value (Foo c:). The pair is dropped from the entity’s metadata. |
W_ATTRIBUTE_AT_PARENT_INDENT | warning | An indented attribute line (key: value where key is reserved) appears at the same indent level as preceding structural children, ambiguating which entity it attaches to. |
E_SEQUENCE_BARE_POSITION_REMOVED | removed 0.43.0 | (Historical) A sequence participant used the retired bare-keyword position N ordering shorthand; use colon-keyed position: N (§2.2). Now: no dedicated error — falls through to normal parsing. |
E_DATA_COMMA_REMOVED | removed 0.43.0 | (Historical) The short-lived 0.30.0 freeze hard-errored commas in data-chart VALUE position. Reversed by #28: commas are now accepted — thousands grouping (Revenue 1,000 → 1000) and separator commas (Q1 400, 700) both parse. Values are canonically space-separated; 1_000 also works. Not an error. |
1.4.5 Surviving uses of |
The | character is not reserved in DGMO 0.18.0; it survives as a literal in three specific contexts:
- Wireframe dropdown options:
{Option A | Option B | Option C}— inside{...}braces,|separates dropdown options (§19.3). - In-arrow label characters:
A -file|name-> B—|inside an arrow label is preserved verbatim (§1.10). - Quoted name characters:
"Order | Items"— inside"..."quoted strings,|is a literal name character.
Outside these three contexts, | used to trigger E_PIPE_OPERATOR_REMOVED; as of 0.43.0 (#28) that guard is gone, so a stray | is simply parsed as a literal / ignored rather than flagged. dgmo migrate still mechanically converts legacy pipe content.
1.5 Color Suffixes
Color is set by typing the color name at the end of a label, lowercase. Example: Done green colors Done green. Eleven colors exist: red, orange, yellow, green, blue, purple, teal, cyan, gray, black, white. To use a color word as a literal label, capitalize it: Red stays as the word Red.
These eleven names are the ONLY valid colors. Hex (
#e6194b),rgb(...)/hsl(...), and CSS keyword colors (crimson,pink, …) are NOT supported anywhere in DGMO — not on groups, series, nodes, edges, tags, or any other element. A hex or CSS value is rejected with an error diagnostic and the element falls back to its default / auto-assigned palette color. The only way to specify a color is one of the eleven names above. This is deliberate: a named color resolves to the right hex per active palette and light/dark theme, so the same source stays palette-portable — a hardcoded hex would not.
Label color // bare trailing color token
Done green // value=Done, color=green
Senior Engineer red // value="Senior Engineer", color=red
Red // value=Red, no color (capitalized → escape hatch)
The universal rule — color trails the label:
Color is the trailing whitespace-delimited token of a label region, when that token (case-sensitive, lowercase) is one of the 11 names above. Otherwise the label region has no color.
The “label region” is everything left after the parser strips off structural terminators it owns: same-line metadata (§1.4.1), as <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:
- Metadata cut (§1.4.1) — the first
<key>:token wherekeyis 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. - 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.
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 redandNorth 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
lanelines - 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,Greenstay 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)-> BandA -yes-> Bboth 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 aGroup not founderror). Forward references — using a group before its declaration line — are not allowed.
Collapsing groups. The canonical form is a bare collapsed trailing flag on the group/container line (e.g. [Backend] collapsed, [Done] collapsed). The older collapsed: true metadata (same-line or indented) remains accepted as legacy. Wireframe’s flag-enum collapsed and block/sketch’s comma-list collapsed are the same token in their native grammars.
| Chart type | Example |
|---|---|
| sequence (§2.2) | [Auth System] collapsed |
| infra (§4.6) | [Backend] collapsed |
| mindmap, pert | Research collapsed |
| gantt (§13) | [Backend] collapsed |
| kanban (§11) | [Done] collapsed (columns) |
| state | [Fulfillment] collapsed (composite groups) |
| c4 | [API Layer] collapsed |
| event-line (§28) | [Golden Age] collapsed (eras) |
| block (§30) | [Wide] span: 2, collapsed (comma-list) |
| sketch (§31) | [Armory] at: 0 2, collapsed (comma-list) |
| wireframe (§19.5) | trailing-keyword flag list |
The authoritative support list, stated once here: sequence, infra, gantt, kanban, mindmap, pert, state, c4, event-line (eras), block, sketch, wireframe. treemap and timeline collapse is interactive-only (no source marker; view-state, decision #32). §26.3 and §26.7 defer to this list.
A group whose name ends in the word “collapsed” uses the standard capitalize/quote escape (same class as trailing colors): capitalize it ([Mission Collapsed]) or quote the name (["Mission collapsed"]) to keep the word as name text.
The collapsed flag is portable view-state: because it lives in the source text, every renderer (the app, the dgmo CLI, remark-dgmo, Obsidian, code-fence embeds) reproduces the collapsed view from the .dgmo alone — a diagram in a code fence looks the way its author left it, not fully expanded. In the app, collapsing/expanding a node writes/removes this marker in the source (the source stays the single source of truth). A runtime viewState.cg (share-link / live app state) is applied in addition to the source markers. (Mindmap honored this field starting 2026-07; see the decision log. The broader effort to give every view-state key a native spelling is tracked separately.)
1.9 Boolean Options
option-name // on
no-option-name // off
- Bare keyword = on;
no-prefix = off - Must appear before diagram content
Cross-cutting boolean directives (recognized in every chart type that has the corresponding rendering surface):
| Directive | Effect |
|---|---|
fill-tint | The default, spelled explicitly — shapes get the canonical 25% tint fill with a solid intent-color outline |
fill-solid | Render nodes/bars at full intent saturation instead of the canonical 25% tint |
fill-outline | No fill — shapes take the theme background fill; the intent color is carried entirely by the outline |
no-title | Hide the diagram banner title in the rendered output (does not mutate the parsed model) |
no-legend | Suppress the legend on any chart type that renders one (universal — includes data charts; added by decision #48) |
legend-inline | Render the title and its tag/series legend on one line — title left, legend flushed right — instead of stacking the legend below a centered title, reclaiming a header row. Honoured by every chart with a top-center capsule legend: the data charts (bar, line, radar, scatter, function) and the structured tag-legend charts (state, treemap, block, event-line, boxes-and-lines, er, class, family, infra, sequence, sketch, bracket, gantt, pert). On any other chart type it emits a warning (W_LEGEND_INLINE_UNSUPPORTED) — “not supported for this chart type; title and legend render stacked” — so it never silently pretends to work; the chart still renders. Auto-falls back to the stacked header (and re-centers the title) when the legend can’t fit on a single row beside the title — so the diagram is always valid regardless of title length, entry count, or width (decision #50) |
The three fill-* directives form one fill family and are mutually exclusive; when more than one appears, the last one wins. The family replaces the earlier solid-fill boolean (decision #46) — fill-solid is the exact successor. Charts whose fill encodes data ignore the family: map regions (choropleth — saturation is the data), infra severity tints, gantt progress bars, and tech-radar blips. A small group honors fill-solid but ignores fill-outline because hollowing the fill would erase the chart: the line/function area fill and sankey/chord ribbons (the surface is the flow). Everything else — including goal meters (hollow meter, colored rim, §34.5), heatmap cells (bg cells, full-intent ramp strokes), countdown chips (§36), clock’s decorative surfaces (state tints like daylight stay), bracket, body, quadrant, and arc — renders outline for real (decision #47). Under fill-outline, colorless/untagged shapes render with a strengthened neutral outline so they don’t vanish against the background.
Layout direction is likewise cross-cutting: every direction-bearing chart accepts the bare booleans direction-lr / direction-tb (last-one-wins), each chart keeping its own sensible default — flowchart, org, and c4 default TB; infra, state, sitemap, boxes-and-lines, swimlane, pert, version-control, and event-line default LR (event-line accepts only direction-lr; direction-tb is a reserved seam); clock defaults rows (direction-tb), where direction-lr selects columns mode. The key+value form direction LR|TB is accepted legacy. Cycle’s direction-counterclockwise belongs to the same boolean family.
Example: no-title (all chart types with a banner title).
The fill-* family states one treatment for the whole chart, so it can never express a hierarchy within one. That axis — which elements are figure and which are ground — is the emphasis family (highlight / dim, §1.11). It is not a boolean family: both directives take a name list, so they live in their own section.
1.10 In-Arrow Message Labels
An in-arrow label is the text embedded inside an arrow between the opening delimiter and the arrow token, as in A -label-> B.
A -label-> B
^ ^---^ ^^
| | ||
| | |+- destination id
| | +- arrow token
| +- label text (plain, no markdown)
+- opening delimiter (matches arrow type)
Chart types that support in-arrow labels: sequence, flowchart, state, infra, c4, er, class, boxes-and-lines.
Cheat sheet
// happy-path: labels are plain text with punctuation allowed
A -location[]-> B // label = "location[]"
A -a[b]c-> B // label = "a[b]c"
A -{json}-> B // label = "{json}"
// unicode: all scripts and emoji preserved verbatim
A -café-> B
A -日本語-> B
A -🎉-> B
// punctuation is literal — no markdown interpretation
A -(parenthetical)-> B // label = "(parenthetical)"
A -*emphasis*-> B // label = "*emphasis*" (NOT bold)
A -`code`-> B // label = "`code`" (NOT a code span)
// forbidden: -> and ~> substrings inside a label
A -uses -> chain-> B // ERROR (E_ARROW_SUBSTRING_IN_LABEL)
// fix: rephrase the label, or move detail to same-line metadata
A -uses to chain-> B
A -uses-> B detail: chains to next
// migration from pre-gauntlet (legacy) syntax
A -Makes calls [HTTP]-> B // label is now the FULL "Makes calls [HTTP]"
A -Makes calls-> B tech: HTTP // preferred: technology on target metadata
Character-set contract
- Allowed: any Unicode codepoint except the forbidden list below. Brackets
[] {} (), pipes|, quotes"', backticks, punctuation, digits, emoji, ZWJ sequences, combining marks — all pass through as literal characters. - Forbidden substrings:
->and~>. These terminate the arrow. If you need them inside a label, rephrase it or move the detail to same-line metadata (A -uses-> B detail: chains to next); there is no escape mechanism. - Forbidden characters: C0 control characters U+0000–U+001F except U+0009 (tab), and U+007F (DEL). Silent renderer breakage and log-injection surface — no legitimate use case.
- Whitespace: leading and trailing whitespace is trimmed; internal whitespace runs (including tabs, non-breaking spaces, and zero-width spaces) are preserved, never collapsed.
- Plain text only: no markdown interpretation.
*foo*renders as*foo*, not italicized.[label](url)renders as literal[label](url), not a hyperlink. Clickable URLs belong in notes, not in in-arrow labels. - HTML-safe: all renderers emit label text as a DOM text node.
<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:
- C4 trailing
[technology]sugar is removed. A C4 arrow like-Makes calls [HTTPS]-> APIused to extractHTTPSas the technology annotation. The fullMakes calls [HTTPS]is now the label. Use the same-line form for technology:-Makes calls-> API tech: HTTPS.
No code migration is required for in-arrow label character escaping — any label that was valid before remains valid, with one exception: if your label happened to contain the literal substring -> or ~>, the parser now rejects it with E_ARROW_SUBSTRING_IN_LABEL. Move those labels to the post-colon form.
See also
- Engineering rationale, decision log, and party-mode trade-offs: dgmo-language-spec-decisions.md → “In-Arrow Message Labels”.
1.11 Emphasis — highlight / dim
Every diagram has a background element — losses, “other”, last year’s line, the control group — that must stay visible without competing for attention. Emphasis is the directive family that says so.
highlight <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 type | Forms | Closure |
|---|---|---|
sankey | highlight, dim | Upstream + downstream flow closure (§15.5) |
family | highlight only | The named person’s bloodline (§32.18) |
Every further chart type is a separate decision — the family is not silently available elsewhere. On a chart type that has not adopted it, highlight/dim are not directives and the line is read as ordinary content.
What dimming does
A receded element is drawn at reduced opacity, multiplied onto the paint it already had. It is never recolored.
- Hue survives, so emphasis composes with color instead of competing with it — a receded brand-red flow is still red. This is precisely why receding cannot be spelled by recoloring an element gray: gray is a full-weight palette color (§1.5), and spending the color slot on it destroys the element’s identity.
- Where a baseline translucency already exists (sankey ribbons), the dim multiplies it rather than replacing it. Replacing would render dimmed ribbons more solid than undimmed ones.
- Shapes and text recede by different amounts. Shapes carry the hierarchy; labels only need to stop competing, so they keep a legibility floor. The requirement is “recede”, not “remove”.
- The result is a baked attribute in the rendered output — no stylesheet, no script, no hover. PNG and SVG exports carry the emphasis exactly as the preview shows it.
There is no numeric opacity control. Two states — figure and ground — are the whole hierarchy a diagram can carry legibly, and the language spells states with named tokens rather than tuned values (the same reason it has no hex colors).
highlight lights a story, not a single element
highlight is the dual that names the foreground and derives the background from it. What counts as foreground is more than the named element alone: each chart type extends the named elements to the structure that explains them.
familylights the named person’s bloodline — that person plus all ancestors and all descendants.sankeylights the named node’s flow closure — everything upstream that feeds it plus everything downstream it feeds. Without this,highlight Barrel Agingwould 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 Lossesstill 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:
- NFC normalize the input
- Replace runs of Unicode whitespace with a single ASCII space
- Trim leading/trailing whitespace
- Case-fold via
toLocaleLowerCase('en-US')
'Auth Service', 'auth service', 'AUTH\tSERVICE', and ' auth service '
all normalize to 'auth service'. The first declaration wins for the
display label. Subsequent re-declarations with a different casing or
spacing fold into the first and emit I_NAME_MERGED (warning) so the
divergence is visible to the author.
The locale is pinned to en-US to keep behavior stable across build
environments — language-neutral case folding silently diverges on
Turkish dotted/dotless I and German ß the moment the default locale
changes. The shared utility lives at dgmo/src/utils/name-normalize.ts;
every parser imports from it so normalization can never drift between
chart types.
2.2 Reserved Characters
Bare names accept letters, digits, spaces, and hyphens. The following
characters are reserved and require "..." quoting if you want them in
a name:
| Character | Why it’s reserved |
|---|---|
| | Removed metadata delimiter (pre-0.18.0). Outside its surviving uses (§1.4.5) it used to emit E_PIPE_OPERATOR_REMOVED; that guard was dropped in 0.43.0 (#28), so it now falls through as a literal. Quote to use as a literal name character. |
: | Type separator in members + various metadata forms |
->, ~>, --, .. | Edge sigils. <- / <~ are not standalone arrows — left-pointing arrows are removed (write B -> A); they appear only inside the bidirectional operators <-> / <~> (and labeled <-label-> / <~label~>) |
[, ] | Group brackets and shape brackets |
(, ) | Shape brackets (rounded), sankey parenthetical syntax |
{, } | Shape brackets in flowchart |
<, > | Decision shape brackets |
| Leading or trailing whitespace | Stripped by normalization |
There is no "-inside-" escape — names cannot contain a double
quote.
2.2a Quoting + Alias + Metadata — Combination Constraints
Bare names compose freely with as <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.
| Combination | Bare name | Quoted name |
|---|---|---|
| name only | ✅ | ✅ |
name + as alias | ✅ | ❌ (parse error / Unexpected line) |
name + meta | ✅ | ✅ |
name + as alias + meta | ✅ | ❌ |
Workarounds when you need both quoting and aliasing:
- Quoting exists to escape reserved characters in a name. If the only
reason for quoting is multi-word, drop the quotes — bare names
already accept spaces and hyphens (
Auth Service as ais 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.
| Form | Accepted | Notes |
|---|---|---|
Alice | ✅ | bare implicit |
Alice role: x | ✅ | bare + metadata |
Alice as a | ❌ | standalone alias without is a is rejected |
Alice as a role: x | ❌ | same — needs typed form |
"Auth Service" | ❌ | quoted standalone is rejected |
"Auth Service" role: x | ❌ | same |
Alice is a person | ✅ | typed |
Alice is a person as a role: x | ✅ | typed accepts alias + metadata |
"Auth Service" is a database as a role: x | ✅ | typed unlocks quoted + alias + metadata |
Rule of thumb for sequence: if a participant needs an alias, a
quoted name, or both, declare it with is a <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
| Source | Where | Renders as | Notes |
|---|---|---|---|
Auth Service is a database | sequence | Auth Service | bare multi-word, no quoting needed |
Cache then cache | flowchart | Cache | first-seen wins; warning emitted |
"first name" varchar | er column | first name | quoting required to escape : type separator if you wanted it in name |
"Order | Items" | infra | Order | Items | quote the pipe |
class "Customer Service" | class | Customer Service | bare multi-word also accepts |
Auth Server is a database then auth server -hi-> DB | sequence | one participant Auth Server | message resolves via normalization |
[Order Items] | infra group | Order Items | brackets are the group sigil; name inside accepts spaces |
Customer Orders { ... } | er | Customer Orders | multi-word table |
webapp api gateway | infra | webapp api gateway | full string is the name |
2.4 Migration: aka Removed
Sequence’s Name is a type aka Alias modifier is removed. The
forgiving-normalization rule above handles casing/spacing variants;
short-code aliasing for genuinely-different tokens is now the
universal as postfix — see §2A.
// before — removed
Alice is an actor aka Authenticator
// after — universal alias (when a short-code is wanted)
Alice is an actor as a
Encountering \baka\b in a participant declaration produces
E_AKA_REMOVED with a migration hint in the message.
2.5 Carve-Outs by Design
These are intentionally outside the universal rule:
- D3 chart data rows (slope, venn, quadrant, arc) — labels in data rows are not entity names; they participate in the chart’s data model rather than the entity-graph that universal name handling governs.
tags:andimport: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— thepis a tag-system alias, not an entity name. It’s resolved through a separate alias map and stays unaffected.
2.6 Error Catalog
| Code | Severity | When | Message template |
|---|---|---|---|
I_NAME_MERGED | warning | Two source-distinct names normalize to the same key AND their displayed forms differ | merged '{incoming}' (line N) into '{existing}' (line M) — names differ only in case/whitespace |
E_NAME_RESERVED_CHAR | error | A bare name contains a reserved character without "..." quoting | name contains reserved character '{char}' — wrap in "..." to use literally |
E_AKA_REMOVED | error | The removed aka keyword appears in a sequence participant declaration | 'aka' is no longer supported — use the participant name directly |
E_PARTICIPANT_TYPE_REMOVED | error | A removed sequence participant-type keyword (service, frontend, networking, gateway, external) appears in an is a X declaration | '{type}' is no longer supported — drop 'is a {type}'; the participant renders as the default rectangle |
I_NAME_MERGED warnings are suppressible per-line with an
# allow-merge comment annotation at the call site for intentional
merges.
See also
- Engineering rationale: dgmo-language-spec-decisions.md
- 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:pmandPMare 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_DECLflags out-of-order references. - Flat global namespace: one alias literal has exactly one binding per source.
- Shape/type inference reads canonical:
*Serviceinference 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 ais valid. - SaaS-naming is safe:
Storage as a Service,Backend as a Servicedo 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.
Alice≡alice≡ALICE≡Alice(trailing space). Author intent is the same string with cosmetic variation. - Aliases = different-token short codes.
pm≡Product Manager. Author intent is two distinct strings bound at declaration time.
2A.5 Migration from legacy syntaxes
Pre-1.0 hard break — no compat shim:
| Was | Now |
|---|---|
tag Priority p (bare shorthand) | tag Priority as p |
tag Priority alias p (explicit) | tag Priority as p |
Swordsmanship(red) alias sw (venn) | Swordsmanship as sw red |
Diagnostics:
E_TAG_SHORTHAND_REMOVEDfires on legacy bare tag shorthand.E_VENN_ALIAS_KEYWORD_REMOVEDfires on legacy vennaliaskeyword.
Internal-fixture migration is automated via
dgmo/scripts/migrate-as-aliases.mjs (see also the CI guardrail
dgmo/scripts/lint-no-legacy-alias.sh).
2A.6 Diagnostic Catalog
| Code | Severity | Message |
|---|---|---|
E_ALIAS_BEFORE_DECL | error | Alias '<x>' used before declaration. Declare '<canonical> as <x>' on or above this line. |
E_ALIAS_COLLISION | error | Alias '<x>' is already bound to '<canonical-1>' (line <n>). Cannot rebind to '<canonical-2>'. |
E_ALIAS_SHADOWS_NAME | error | Alias '<x>' would shadow an existing canonical name. Choose a different alias. |
E_ALIAS_REBINDING | error | '<canonical>' is already aliased as '<x-1>' (line <n>). Cannot also alias as '<x-2>'. |
E_ALIAS_OF_ALIAS | error | '<x>' is itself an alias for '<canonical>'. Cannot alias an alias — alias the canonical instead. |
E_ALIAS_RESERVED_KEYWORD | error | '<x>' is a reserved keyword and cannot be used as an alias. |
E_ALIAS_INVALID_FORMAT | error | Alias '<x>' must match [A-Za-z][A-Za-z0-9_]{0,11} (letter start, letters/digits/underscore, max 12 chars). |
E_ALIAS_AFTER_CANONICAL | error | '<canonical>' was already used as a canonical name (line <n>). Aliases must be declared on or before first use. |
E_TAG_SHORTHAND_REMOVED | error | Bare tag shorthand 'tag <name> <x>' was removed. Use 'tag <name> as <x>' instead. |
E_VENN_ALIAS_KEYWORD_REMOVED | error | Venn 'alias' keyword was removed. Use 'as' instead — '<name>(color) as <x>'. |
W_ALIAS_CASE_NEAR_MATCH | warning | '<x>' differs only in case from declared alias '<X>'. Did you mean '<X>'? |
W_ALIAS_UNDERUSED | warning | Alias '<x>' is declared but referenced ≤1 time. Aliases earn their keep on names that repeat 3+ times. |
See also
- Engineering rationale: dgmo-language-spec-decisions.md → TD-18
- Tech-spec:
_bmad-output/implementation-artifacts/tech-spec-universal-alias-syntax.md
2B. Universal Date Handling
Every chart type that reads a date accepts the grammar below. Input is liberal; dates are stored internally as ISO, so rendering, sorting, and snapshots stay byte-deterministic. Applies to gantt (§13), timeline (§15), event-line (§28), countdown (§36), and pert (§13A). Family (§32) uses year-grain only; clock (§37) has no date input.
2B.1 Accepted input formats
| You write | Reads as | Note |
|---|---|---|
2026-07-04 | 2026-07-04 | ISO — canonical, always unambiguous |
2026-07-04 14:30 / …T14:30[:SS] | datetime | where the type supports a time |
2026-07 | month grain | day omitted |
2026 / 1716 | year grain | bare year |
07-04 | {year}-07-04 | bare MM-DD → resolved year (§2B.3) |
7/4 | {year}-07-04 | slash form, US month-first default (§2B.2) |
Jul 4 / July 4 | {year}-07-04 | month name — never order-ambiguous |
July 4, 2026 / 4 Jul 2026 | 2026-07-04 | full natural form (explicit year) |
753 BCE / 44 BC / 14 CE | era-signed year | timeline BCE support |
2B.2 Slash order — US month-first
Numeric slash/dash forms (7/4, 07-04) default to US month-first (MDY).
- The
date-order dmydirective flips the whole document to day-first (7/4→ Apr 7).date-order mdyis the default. - Out-of-range self-disambiguates regardless of the directive:
13/2can’t be month 13 → Feb 13. - Applies only to ambiguous numeric forms. Month-name forms (
Jul 4,4 Jul) and ISO are never affected bydate-order.
2B.3 Year resolution — first match wins
A date without a year resolves through this ladder:
- Explicit year on the date itself — wins outright.
year 20XXdirective anywhere in the document — the base year.- 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). - Current render year — last resort, only when the chart has zero full
dates anywhere. Emits a soft, non-blocking hint suggesting
year 20XXfor reproducibility. Suppressed byno-current-year, which instead makes a fully-bare chart a hard error.
Levels 1–3 never read the clock, so those charts are snapshot-stable. Only a chart with no full date at all touches the wall clock.
2B.4 Directives
Recognized in any date-bearing chart (colon-free; bare where boolean):
| Directive | Effect | Default |
|---|---|---|
year 20XX | base year for bare month-days below | derive / current year |
date-order dmy | numeric slash/dash dates read day-first | mdy (US) |
no-current-year | a fully-bare chart is a hard error, not current-year | off |
All three are position-independent (pre-scanned), so they may appear before or after the dates they govern.
Existing ISO diagrams are unaffected — the grammar is an additive superset and ISO output is byte-identical.
3. Sequence Diagrams
2.1 Participants
Name is a <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 Type | Shape | Name Patterns (examples) |
|---|---|---|
| actor | Stick figure | User, Customer, Admin, Agent, Person, Buyer, Seller, Guest, Visitor, Operator, Developer, Alice, Bob, Charlie, Fan, Purchaser, Reviewer, *User, *Actor, *Analyst, *Staff |
| database | Cylinder (vertical) | *DB, Database, Datastore, *Store, Storage, *Repo, Repository, SQL, Postgres, MySQL, Mongo, Dynamo, Aurora, Spanner, Supabase, Firebase, BigQuery, Redshift, Snowflake, Cassandra, Neo4j, ClickHouse, Elastic, OpenSearch, Druid, Trino, Pinecone, Weaviate, Qdrant, Milvus, Presto, *Table |
| cache | Dashed cylinder | *Cache, Redis, Memcache, KeyDB, Dragonfly, Hazelcast, Valkey |
| queue | Horizontal cylinder (pipe) | *Queue, *MQ, SQS, Kafka, RabbitMQ, EventBus, MessageBus, *Bus, Topic, *Stream, SNS, PubSub, *Broker, NATS, Pulsar, Kinesis, EventBridge, CloudEvents, Celery, Sidekiq, EventHub, *Channel |
| default | Rectangle | Everything else (no is a needed) |
Inference is intentionally conservative: only names whose role is unambiguous at a glance get a distinctive shape. Anything else falls through to the default rectangle. Adding new patterns should resist suffix catch-alls (e.g. *-er → service) that historically inflated the table and rarely held up across diverse diagrams.
Inference handles it (skip is a):
PostgresDB // database (matches *DB)
Redis // cache (exact match)
User // actor (exact match)
Kafka // queue (exact match)
Inference would miss (use is a):
Vault is a database // "Vault" matches no rule, but you want database
Notifications is a queue // "Notifications" matches no rule
Names that previously inferred to a removed type — AuthService, WebApp, Cloudflare, API Gateway, Stripe, Webhook, Upstream — now fall through to default (plain rectangle). That is the intended outcome of the trim: the visual differentiation is gone because the underlying distinction did not pull its weight.
Removed Type Keywords (0.16.0)
The keywords service, frontend, networking, gateway, and external were removed in 0.16.0. Using any of them in is a X emits E_PARTICIPANT_TYPE_REMOVED (one diagnostic per offending line — the parser collects all errors in a single pass rather than stopping at the first):
// All of these fail to parse:
Auth is a service // E_PARTICIPANT_TYPE_REMOVED
WebApp is a frontend // E_PARTICIPANT_TYPE_REMOVED
LB is a networking // E_PARTICIPANT_TYPE_REMOVED
API is a gateway // E_PARTICIPANT_TYPE_REMOVED
Stripe is an external // E_PARTICIPANT_TYPE_REMOVED
// Fix: drop the override — participant renders as default rectangle
Auth
WebApp
LB
API
Stripe
The trim is sequence-only — is a external / is a database / etc. remain valid in C4, infra, and org diagrams under their own taxonomies (see §7).
2.2 Participant Ordering
Participants are laid out left-to-right. The default order is first appearance in messages — the first participant mentioned in a message gets the leftmost position, and so on.
Declaring a participant (§2.2b) assigns its tag, type, or alias but does not pin a column — a declared participant that appears in a message is still placed by first appearance, in whatever order the arrows reach it. Declaration order decides placement only for participants that never appear in any message. So you can declare just the participants you need to tag (e.g. the External or Customer exceptions) without those declarations dragging them out of message-flow order; use an explicit position: when you do want to pin one.
Explicit position override:
Name position: N
positionis colon-keyed same-line metadata (§1.4) — the colon is required. The legacy bare-keyword formName position Nwas removed; a surviving bareposition NraisesE_SEQUENCE_BARE_POSITION_REMOVED.0is leftmost,1is second from left, etc.- Negative values count from the right:
-1is rightmost,-2is 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 usingas <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):
- Explicit
position: N - Group adjacency
- First appearance in messages
- Declaration order (for participants not mentioned in any message)
2.3 Participant Groups
[Group Name]
Participant1
Participant2
- Metadata goes outside brackets:
[Backend] t: Eng
Collapsible Groups
[Group Name] collapsed
Participant1
Participant2
The bare collapsed trailing flag (§1.8) declares a group as collapsed by default (legacy: collapsed: true). Collapsed groups render as a single participant lifeline with the group name as label. A drill-bar accent bar on the participant box signals expandability.
- Messages to/from group members remap to the group lifeline
- Messages between members of the same collapsed group render as self-referential on the group lifeline
- Unlabeled internal returns between members are suppressed
- Notes attached to members display on the group lifeline
- Fragment blocks (
if/loop/parallel) resize to reflect collapsed participant positions - In the app, clicking the collapsed participant expands it; clicking an expanded group label collapses it
- Export/share and CLI rendering honor the collapse state
2.3 Messages (Arrows)
| Type | Syntax | Example |
|---|---|---|
| Sync (labeled) | A -label-> B | Client -login-> API |
| Sync (bare) | A -> B | Client -> API |
| Async (labeled) | A ~label~> B | API ~notify~> Queue |
| Async (bare) | A ~> B | API ~> Queue |
- Whitespace around arrows is optional:
A-label->Bworks - 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 ifandelsealign with the openingifindent.
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→ useelse if fooelse foo(label after bareelse) → useelse if fooloopandparallelblocks do not supportelsebranchesend(ordeactivate/activate/autonumber) → not dgmo syntax. Blocks (and multi-line notes) close by dedenting, never with a terminator keyword; a bareendline is rejected. Activation is controlled by theactivationsoption (§2.7), not per-message keywords.- A tag/color on a message (
A -msg-> B tag: x) → tags color participants, not messages. Declare atag <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 coloringactive-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 aliasbinds 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 gatewayis 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:
| Property | Capability | Effect |
|---|---|---|
cache-hit | Cache | % requests served from cache, reduces downstream RPS |
firewall-block | Firewall/WAF | % requests blocked, reduces downstream RPS |
ratelimit-rps | Rate limiter | Max RPS passed through |
latency-ms | Latency | Adds to path latency |
uptime | Availability | Multiplied along path for SLO |
instances | Horizontal scaling | Multiplies capacity (number or min-max range) |
max-rps | Capacity ceiling | Max RPS node handles |
cb-error-threshold | Circuit breaker | Error rate trip threshold |
cb-latency-threshold-ms | Circuit breaker | Latency trip threshold |
concurrency | Concurrency limit | Max concurrent requests (serverless) |
duration-ms | Processing time | Time spent processing (serverless) |
cold-start-ms | Serverless | Cold start penalty |
buffer | Queue | Buffer size |
drain-rate | Queue | Consumption rate |
retention-hours | Queue | Message retention |
partitions | Queue | Partition count |
description | Display | Description text |
Mutually exclusive: concurrency ≠ instances ≠ max-rps; buffer ≠ max-rps. A node is serverless, traditional, or a queue — not two at once.
4.4 Connections
| Type | Syntax |
|---|---|
| Sync (bare) | -> Target |
| Sync (labeled) | -/api-> Target |
| Async (bare) | ~> Target |
| Async (labeled) | ~event~> Target |
- Connection metadata:
split: 50%, fanout: 3(same-line, comma-separated) - Indent under source node (see §1.7) — avoid flat
Source -> Targetform - 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 Nmust be ≥ 1; sub-1 values are warned and clamped- Sources with at least one
fanout > 1outgoing edge gain the Fan-Out capability badge - The legacy
xNsuffix (e.g.... -> Target x5) is removed — usefanout: 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
collapsedtrailing 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: indentedcollapsed true.) - Group properties (indented under the bracket line, colon-keyed like node properties §4.3):
instances: Norinstances: N-M— capacity multiplier on children (auto-scaling). The space formsinstances N/instances N-Mare accepted legacy.
4.7 Infra Options (Space-Separated, NO Colon)
direction-lr/direction-tb(booleans, §1.9; default is LR)default-latency-ms Ndefault-rps N— fallback edge RPS when norpsis declared on the edge nodedefault-uptime DECIMALslo-availability DECIMAL— target availability for SLO compliance highlightingslo-p90-latency-ms N— target p90 for SLO compliance highlightingslo-warning-margin DECIMAL— margin below SLO that triggers warning stateactive-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 thedescription: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 textrenders as• bullet text- Descriptions are ignored on
edgeandinternetnodes
5. Flowchart Diagrams
4.1 Declaration
flowchart [Title]
4.2 Node Shapes
| Shape | Syntax | Example |
|---|---|---|
| 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 aNode s: valuesuffix are all unsupported (a trailing suffix is ignored with a warning).
4.3 Arrows
| Type | Syntax |
|---|---|
| Unlabeled | -> |
| Labeled | -label-> |
Flowchart edges have no color slot. Node colors are automatic by shape (§5.2) — flowcharts have no manual node color and do not use the tag system.
4.4 Groups
[GroupName]
[Child nodes...]
Bracket syntax only.
4.5 Edges (Indented Under Source)
Prefer indented-under-source form (see §1.7).
Decision branches are indented under their source node — the most common and readable pattern:
(Start) -> [Validate Input] -> <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 formdirection LR|TBis accepted legacy.fill-solid/fill-outline(§1.9 fill family; default is the 25% tint —fill-solidrenders shapes at full intent color,fill-outlinedrops 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
NodeIdis 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 followsnote(e.g.note -> Active) is treated as an edge rather than a note — in state diagrams that is a transition from a state namednote. 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, andno-notesopt-out throughout. TheNodeIdis the node’s label: a class name, table name, or box label (quote multi-word labels). Org and sitemap are intentionally excluded — their indentation is the tree structure, which conflicts with the indented-body grammar (a note would silently swallow following nodes). Other chart types adopt the same syntax in later work.
6. State Diagrams
5.1 Declaration
state [Title]
5.2 States
StateName
[*] // initial/final pseudostate
5.3 Transitions
| Type | Syntax |
|---|---|
| Unlabeled | Idle -> Active |
| Labeled | Idle -submit-> Processing |
5.4 Groups
[Group Name]
5.5 Notes (States)
State diagrams support the same note <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 underfill-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 Textshow-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)hideactive-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).
| Type | Syntax |
|---|---|
| Sync labeled | -Makes API calls-> API |
| Sync with tech | -Uses [HTTPS]-> API |
| Async labeled | ~Sends emails~> Email |
Relationships are indented under the source element:
Web App is a container
description: SPA built with React
-Makes API calls-> API
-Loads-> CDN
API is a container
-Reads/writes-> Database
~Sends events~> Queue
7.6 Sections
containers
...
components
...
deployment
container Web App // reference existing container
7.7 Options
direction-lr/direction-tb(booleans, §1.9; default is TB)active-tag GroupName/active-tag none
7.8 Element Descriptions
// Indented metadata form (colon required)
Web App is a container
description: SPA built with React
description: Supports SSR and client-side routing
// Bare keyword form (DEPRECATED — emits a warning; prefer the colon form above)
API is a container
description Handles all REST endpoints
// Same-line metadata form
Database is a container description: PostgreSQL with read replicas
- The
descriptionkeyword requires a colon (description: text). The baredescription textform is removed at 1.0 and emitsE_DESCRIPTION_BARE_REMOVED(an error; the value is still applied so the diagram renders). - Multiple
descriptionlines accumulate into a multi-line description descriptionis extracted as a dedicated field, not stored in general metadata- Supports inline markdown:
**bold**,*italic*,`code`,[links](url) - bullet textrenders 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-notesopt-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; anynotationvalue other thanlabelsfalls back to crow’s-foot)active-tag GroupName/active-tag noneno-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-notesopt-out). It floats beside the class without moving it. See §4.7 for the full rules.
9.1 Declaration
class [Title]
9.2 Classes
Ship
abstract Vessel
interface Serializable
Ship extends Vessel
Galleon implements Serializable
Galleon extends Ship implements Serializable
enum ShipType
extends and implements may co-occur on the same declaration line. Order is fixed: extends before implements. Each may name only one parent / interface.
9.3 Members (Indented, Colon for Types)
Fields:
+ name: string
- speed: number
# protectedField: int
Methods:
+ sail(): void
- calculate(x: number): boolean
+ getName() {static}: string
Visibility: + public, - private, # protected
Enum values:
enum ShipType
Galleon
Sloop
9.4 Relationships (Indented Under Source Class)
Prefer indented-under-source form (see §1.7).
| Relationship | Arrow |
|---|---|
| Inheritance | `— |
| Implementation | ..|> |
| Composition | *-- |
| Aggregation | o-- |
| Dependency | ..> |
| Association | -> |
Relationships are indented under the source class:
Ship
--|> Vessel
*-- Cannon
Optional label: --|> Vessel : extends (colon optional before label)
extends and implements on class declarations still work as part of the declaration syntax.
9.5 Options
(No chart-specific options. The universal directives no-title and the fill-* family (§1.9) apply.)
11. Kanban Boards
10.1 Declaration
kanban [Title]
10.2 Columns
[Column Name]
[Column Name] color wip: 3
[Column Name] collapsed
A column may carry the bare collapsed trailing flag (§1.8) to start folded to
a narrow strip; the app writes/removes it on the column toggle and it
round-trips to any renderer (legacy: collapsed: true).
10.3 Cards (Indented Under Columns)
[To Do]
Card title priority: High, c: Owner
Detail text (indented deeper)
10.4 Options
hideactive-tag GroupName/active-tag nonelane-by GroupName— slice the board into swimlanes by a tag group (the swimlane axis). Namedlane-by, notswimlane, becauseswimlaneis 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 textrenders as• bullet text
13. Gantt Charts
Dates (
start-date, taskstart:, 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 default — Database Schema and Auth Module above start at the same time (no parallel keyword needed).
Arrow = sequential: -> Next Task 10bd indented under a task means “Next Task starts after parent finishes.”
Lag (delay): -3w-> Next Task 10bd — target starts 3 weeks after source finishes.
Lead (overlap): --2w-> Next Task 10bd — target starts 2 weeks before source finishes.
Cross-group references: -> [Backend].API Design — reference-only dependency (does not create a new task):
[Frontend]
Wireframes 10bd
-> [Backend].API Design
Multiple arrows from one task:
API Integration 10bd
-> E2E Testing 5bd
-> Documentation 3bd
13.10 Scheduling Model
- Bare siblings = parallel (start at chart start, or group start if offset).
- Arrow = sequential (target starts after source finishes, plus optional lag/lead).
- Offset prefix = pinned start relative to chart start.
This replaces the legacy parallel keyword — groups and arrows express all scheduling relationships.
Removed at 1.0 (error E_GANTT_LEGACY_REMOVED): the four pre-1.0
scheduling shorthands no longer parse as valid syntax — each raises
E_GANTT_LEGACY_REMOVED (error; the value is still applied best-effort so
the diagram renders):
- The
parallelkeyword — sibling tasks already run in parallel by default; delete it. - Duration-before-name —
30bd Database Layer→ positionalDatabase Layer 30bd(orDatabase Layer duration: 30bd). - Explicit-date task —
2024-01-15 Kickoff→Kickoff start: 2024-01-15. - Legacy timeline-duration —
2024-01-15 -> 30d Build→Build start: 2024-01-15, duration: 30d.
Kept (NOT legacy): duration: and start: are canonical fallback
metadata keys (§13.8) — they parse cleanly with no error. Use them for
ambiguous names where the positional/offset forms are awkward.
13.11 Sprints
The sp duration unit expresses task duration in sprints. Sprint length is configurable via sprint-length (default 2w). Using sp in any task auto-enables sprint mode; explicit sprint-* options also enable it.
Options (space-separated, no colons):
| Option | Default | Description |
|---|---|---|
sprint-length <dur> | 2w | Duration of one sprint. Only d and w units allowed. |
sprint-number <n> | 1 | Which sprint the chart starts at (positive integer). |
sprint-start <date> | chart start-date or today | YYYY-MM-DD anchor date for sprint-number. |
Duration resolution: 2sp with sprint-length 2w = 4 calendar weeks (28 days). Fractional sprints work: 0.5sp = half a sprint. Changing sprint-length rescales all sp-based durations across the chart.
Sprint bands: When sprint mode is active, alternating shaded vertical bands render on the timeline with adaptive labels (“Sprint N”, “N”, or every 5th). Dashed boundary lines mark sprint edges.
Eras vs sprints: Eras and sprints share the same visual lane. When both are defined, eras display by default (with a toggle in the app). In static SVG, eras always win.
Note: sp means sprints; the historical bare s still parses in Gantt/PERT context but is no longer documented — timeline’s s is seconds. Sprints use calendar time, not business days. For teams with irregular sprint calendars, use eras instead.
gantt
sprint-length 2w
sprint-number 5
sprint-start 2026-01-05
start-date 2026-03-01
[Phase 1]
Design 2sp
-> Build 3sp
-> QA 1sp
13A. PERT Diagrams
Anchor dates (
start-date,end-date,sprint-start) follow the liberal grammar + year ladder in §2B Universal Date Handling. 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 likeconfidenceandcollapsed. - Group headers also accept tag metadata:
[outfit ship] c: Bosuncolors 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 nonesuppresses 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:
| Form | Meaning |
|---|---|
recruit crew 1 2 4 | Three-point estimate: O M P (in time-unit weeks) |
recruit crew 2 | M-only: parser fills O and P from default-confidence factors |
celebrate | TBD: no estimate; downstream activities inherit ? |
Two-number durations are an error (the parser cannot disambiguate between O+M, M+P, or O+P).
pert
time-unit w
default-confidence medium
recruit crew 1 2 4 as rc confidence: low
careen hull 1.5
load powder
celebrate
13A.4 Dependencies
Indented -> dest lines under an activity declare a dependency from that activity to dest. The destination must reference a previously-declared activity name or alias — inline durations on the arrow line (-> name 1 2 4) are rejected by the parser. Declare the destination on its own source line first, then reference it.
// ✅ Declare destinations first; arrows reference by name/alias only
load powder 0.5 1 2
careen hull 1.5
recruit crew 1 2 4 as rc
-> load powder
-> careen hull
// ❌ Inline forward-declaration is rejected:
// "Inline forward-declaration not allowed — move attributes to a source line"
recruit crew 1 2 4 as rc
-> load powder 0.5 1 2
Each activity has one declaration site. Repeating an activity’s name on a non-indented line is allowed (it acts as a “source line” for further -> arrows) but providing conflicting duration tokens raises a diagnostic that names both line numbers.
Dependency types and lag/lead
Edges default to Finish-to-Start (FS) with zero lag — the dominant case. The arrow may carry an inline label between two dashes to override either piece:
A -> B # FS, 0 lag (default — unchanged)
A -SS-> B # Start-to-Start
A -2d-> B # FS with +2d lag (lag-only shortcut)
A -SS+2d-> B # SS with +2d lag
A -FF-1d-> B # FF with -1d lead (negative lag)
A -SF+3d-> B # SF with +3d lag
Grammar: -[TYPE][±LAG]-> — both pieces optional, but at least one must be present when a label is given.
| Type | Constraint | Use case |
|---|---|---|
| FS | B.ES ≥ A.EF + lag | Default; sequential work |
| SS | B.ES ≥ A.ES + lag | Parallel start (SS+2d = B starts 2 days after A starts) |
| FF | B.EF ≥ A.EF + lag | Synchronized finish (ship-together activities) |
| SF | B.EF ≥ A.ES + lag | Rare; included for completeness |
Type names are case-insensitive (SS, ss, Ss all valid). Lag amount inherits the diagram’s time-unit; per-edge unit overrides are accepted (-SS+2d->, -FF+4h->). A - sign denotes a lead (overlap): -FS-1d-> = successor starts 1 day before predecessor finishes.
The renderer paints a small midpoint label on every non-default edge (SS +2d, FF -1d, +2d for FS-only lag). FS+0 edges remain visually clean.
A diagnostic warning fires when a lead’s magnitude exceeds the predecessor’s duration (an FS edge that implies the successor starts before the predecessor — logically impossible).
There is no default-edge-type directive: every -> is FS regardless of what’s been written elsewhere in the file. This keeps each edge self-evident to readers and prevents copy-paste from silently flipping semantics.
13A.5 Aliases (as <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
| Key | Where | Meaning |
|---|---|---|
confidence | activity | Per-activity override of default-confidence (high / medium / low / O/P) |
collapsed | group | bare trailing flag (§1.8) — start the group collapsed |
tag aliases (e.g. c: Captain) | activity, group | Resolves to the declared tag group (tag Crew as c …); drives node fill when the group is active. See §13A.2a |
13A.9 Analysis
Mode is auto-derived from the data:
The forward/backward pass, slack, M-world critical path, project μ along that path, and project σ from the variance sum on that path are always computed. Activities downstream of a TBD activity inherit null for ES/EF/LS/LF/slack; the project end likewise becomes ? until all upstream TBDs are estimated.
On top of that base analysis, Monte Carlo simulation runs whenever at least one non-milestone activity has a duration — either an explicit three-point (O M P) estimate or an M-only value whose O/P are filled from default-confidence. The canonical run uses N = trials Beta-PERT samples with seed = seed, and reports P50/P80/P95 plus per-activity criticality index. The interactive duration scrubber uses scrubber-trials samples for live re-analysis.
When no non-milestone activity has any duration (or every terminal is poisoned by TBD propagation), MC is skipped and the diagram renders in analytical mode — caption shows expected duration only, no percentile rows or S-curve.
Heads-up: an all-M-only diagram still produces real MC percentiles, but the spread is a mechanical function of
default-confidence(every activity’s σ scales with M). The simulation is honest about its inputs; it cannot be more informed than the confidence directive it was given.
trials < 100 clamps mode back to analytical (with a one-sentence caveat in the project-stats caption) — ten-trial samples produce nonsense percentiles. The trials, seed, and scrubber-trials directives stay valid but are silently inert in analytical mode.
13A.10 Visual Encoding
| Channel | When applied |
|---|---|
| Project subtitle | always (when the analyzer produces μ or an anchor) — rendered as a single muted line under the diagram title (or in the title slot when no-title suppresses the title). Restates the project’s total duration with σ in parentheses so the headline number stays visible independent of the Analysis-row toggle. Shape per mode: forward → Expected finish: <date> · ≈ <μ> <unit> of work (± <σ>); backward → Expected 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 caption | always (when the analyzer succeeds) — rendered below the diagram body as a node-styled rounded rectangle with bullet-list, left-aligned content. Reports expected duration and — in Monte-Carlo mode — standard deviation and percentile dates. The percentile framing is mode-dependent: forward / no anchor → P50/P80/P95 finishes (start fixed, when do we finish?); backward → P50/P80/P95 latest-safe starts (end fixed, when must we start?). See §13A.12. Each percentile gets its own bullet. Critical path, bottleneck, and hidden-risk activities are conveyed visually via red/orange border bands rather than text |
| Analysis layer (tornado + S-curve) | Monte-Carlo only. Renders by default; precedence is viewState.an (app toggle / share link) if set, else suppressed by the bare no-analysis directive, else on. Auto-omitted in analytical mode (no MC). See §13A.2, §13A.9 |
| S-curve (completion probability) | part of the analysis layer (above). Forward / no anchor: x = finish date, y = P(finish ≤ x), rising 0 → 1 (“chance we’re done by date X”). Backward: x = candidate start date with x_max = end-date, y = P(finish ≤ end-date | start = x), falling 1 → 0 (“chance we still hit the deadline if we start by date X”). Same Monte-Carlo trial set under a different time axis. Vertical reference lines mark the percentile dates from the caption (P50/P80/P95 finishes vs latest-safe starts) so the chart and caption are visually linked. See §13A.12 |
| Tornado (sensitivity ranking) | part of the analysis layer (above). Horizontal bars ranking activities by Schedule Sensitivity Index — the swing in project completion between the activity’s optimistic (O) and pessimistic (P) ends. Sorted longest-first, top-N shown, each bar colored by the activity’s criticality band |
| Name + μ | always |
| Diamond | milestones only |
| Dashed border | TBD activity or downstream-of-TBD |
Red border + edge stroke (palette.colors.red) | binary critical path (analytical mode, no MC) |
| Red / orange / yellow / green / blue border + edge bands | criticality index when MC is on — red ≥ 0.80, orange ≥ 0.50, yellow ≥ 0.25, green ≥ 0.10, blue ≥ 0.02 |
| Slack bar | only when node-detail full |
| σ-as-border-thickness | only when node-detail full |
The project-stats caption is rendered below the title (or at the top when no title) as a centered, multi-line text block. It reports expected duration and — in Monte-Carlo mode — standard deviation and percentile dates (finishes in forward / no-anchor mode, latest-safe starts in backward mode; see §13A.12). Critical-path, bottleneck, and hidden-risk emphasis is delegated to the diagram itself (red/orange node-border bands).
Comments use # line-leading (or trailing-after-whitespace) for PERT specifically — both # and // are recognized.
13A.11 Worked Example
pert Pirate Voyage
time-unit w
default-confidence medium
voyage approved 0
-> recruit crew
[outfit ship]
recruit crew 1 2 4 as rc confidence: low
-> load powder
careen hull 1 1.5 2.5
-> load powder
load powder 0.5 1 2
-> sail to atoll
sail to atoll 3 5 8
-> count gold
-> repair hull
count gold 0.5d
-> divvy shares
repair hull 2 3 5 confidence: low
-> divvy shares
divvy shares 1 2 3
Variant: with date anchoring. Add start-date to render every ES / EF / LS / LF as a real calendar date instead of a numeric offset. Slack still reports as a duration (calendar days when anchored). Critical-path coloring is unchanged.
pert Pirate Voyage
time-unit w
default-confidence medium
start-date 2026-06-01 # NEW: anchor
voyage approved 0
-> recruit crew
recruit crew 1 2 4 as rc
-> load powder
load powder 0.5 1 2
-> sail to atoll
sail to atoll 3 5 8
-> count gold
-> repair hull
count gold 0.5
-> divvy shares
repair hull 2 3 5
-> divvy shares
divvy shares 1 2 3
With time-unit w and start-date 2026-06-01, the recruit crew activity (μ ≈ 2.17w ≈ 15 days) renders ES: 2026-06-01, EF: 2026-06-16. Slack normalizes to Xd everywhere.
13A.12 Date Anchoring
PERT charts compute ES / EF / LS / LF in the diagram’s time-unit. The start-date YYYY-MM-DD and end-date YYYY-MM-DD directives anchor those numbers to a calendar so all four cells render as real dates. They are mutually exclusive — author one or the other, not both.
Terminology. Anchor refers to the user-supplied date (start-date or end-date). projectStart refers to the date the formatter uses internally — equal to the start-date in forward mode, derived as end-date − projectMu in backward mode. The chart never displays projectStart directly except via the corner-cell labels of source/sink activities.
Critical-path framing. On critical-path activities, LS = ES and LF = EF — there is no slack — so the corner-cell labels are unambiguous regardless of forward/backward mode. The “earliest possible” framing of backward mode applies only to non-critical activities, whose ES/EF represent the earliest start/finish given the deadline. The chart’s title annotation establishes this framing once for the whole diagram, but readers can rely on critical-path nodes (red/orange band coloring) being mode-invariant.
Visual node-card specimens. The same activity (recruit crew, μ ≈ 2.17w) under each mode:
No anchor:
┌──────┬──────┬──────┐
│ 0 │ 2.17w│ 2.17w│
├──────┴──────┴──────┤
│ recruit crew │
├──────┬──────┬──────┤
│ 0 │ 0w │ 2.17w│
└──────┴──────┴──────┘
Forward anchor (start-date 2026-06-01):
┌────────────┬──────┬────────────┐
│ 2026-06-01 │ 2.17w│ 2026-06-16 │
├────────────┴──────┴────────────┤
│ recruit crew │
├────────────┬──────┬────────────┤
│ 2026-06-01 │ 0d │ 2026-06-16 │
└────────────┴──────┴────────────┘
Backward anchor (end-date 2026-09-15, derived projectStart = 2026-06-01):
┌────────────┬──────┬────────────┐
│ 2026-06-01 │ 2.17w│ 2026-06-16 │ ← critical-path: LS=ES, LF=EF
├────────────┴──────┴────────────┤
│ recruit crew │
├────────────┬──────┬────────────┤
│ 2026-06-01 │ 0d │ 2026-06-16 │
└────────────┴──────┴────────────┘
(italic title note above chart: "Backward-anchored from end-date 2026-09-15 (as of 2026-05-10)")
mu always renders as a duration (2.17w) regardless of mode — it’s a timespan, not a date. slack normalizes to days (0d) when anchored.
Latest-safe start (backward mode). When end-date is set and Monte Carlo is active, the project-stats caption reframes its percentile rows from finishes to latest-safe starts. Each row reports end-date − duration_PX — the latest date the project can begin and still hit the deadline with that confidence:
Pirate Voyage — Backward-anchored from end-date 2026-09-15 (as of 2026-05-10)
• Expected duration: 15w
• σ: 2w
• P50 latest-safe start: 2026-06-08
• P80 latest-safe start: 2026-05-27
• P95 latest-safe start: 2026-05-13
Higher confidence demands an earlier start: P95 is earlier than P50, mirroring how P95 finish is later than P50 finish in forward mode. Analytical mode (no MC) omits the percentile rows; the μ-derived projectStart already appears in source-activity ES cells, so the caption would only duplicate that single date.
Rounding direction. When sub-day precision arises (e.g. fractional days from time-unit h arithmetic), latest-safe-start dates round down to the nearest whole day — earlier date = more schedule buffer = more conservative. Forward-mode finishes round up for symmetric conservatism. This rule ensures that a reported date never overstates the safety margin.
Past-date annotation. When a latest-safe-start date falls in the past relative to the parse-time today date (i.e. duration_PX > end-date − today), the row appends (latest-safe start has passed) so readers don’t silently misread a pre-today date as actionable:
• P95 latest-safe start: 2026-04-01 (latest-safe start has passed)
The annotation is factual — no verdict on whether the project is “infeasible”; the math is reported honestly and the reader judges. P50/P80 rows in the same caption may still be feasible and render without the annotation.
“Today” capture timing. The past-date check uses the parse-time today date — captured once when the analyzer runs, then frozen. This matches the start-date now precedent (which substitutes its resolved date before share-link compression) so that a shared backward-anchored diagram preserves the author’s view rather than re-evaluating against each recipient’s local date. To make this freshness signal legible, the backward-anchored title annotation includes the parse-time today date in an (as of YYYY-MM-DD) suffix:
Backward-anchored from end-date 2026-09-15 (as of 2026-05-10)
Recipients who see a stale (as of …) know to re-render for an updated past-date check.
Analytical mode (no MC) + past projectStart. Analytical-mode backward anchoring renders only the μ-derived projectStart in source-activity ES cells — there are no percentile rows to annotate. The ES cell date itself communicates “past” via its calendar value; no separate past-date annotation is emitted. (If you need a confidence-banded readout, add three-point estimates to switch to Monte-Carlo mode.)
If any TBD activity makes projectMu indeterminate, all percentile rows render ? (the analyzer has no duration distribution to subtract from end-date); the expected-duration row likewise renders ?.
Completion-probability S-curve axes. The optional S-curve widget (§13A.10) interprets the Monte-Carlo trial distribution differently per mode:
| Forward / no anchor | Backward | |
|---|---|---|
| x-axis | finish date, x_min = projectStart + min(trial_durations), x_max = projectStart + max(trial_durations) | candidate start date, x_min = end-date − max(trial_durations), x_max = end-date |
| y-axis label | P(finish ≤ x) | P(hit deadline | start by x) |
| Formula | CDF_duration(x − projectStart) | CDF_duration(end-date − x) |
| Direction | Rising 0 → 1 | Falling 1 → 0 |
| Reads | ”chance we’re done by date X" | "chance we still hit the deadline if we start by date X” |
The backward curve is the forward curve mirrored horizontally and shifted — same trial distribution, different time axis. Both modes annotate the curve with vertical reference lines at the percentile dates reported in the caption (P50/P80/P95 finishes in forward mode, P50/P80/P95 latest-safe starts in backward mode). Authors should be able to read the chart and the caption as one cross-referenced unit.
S-curve degenerate cases:
- Monte Carlo did not run → S-curve silently omitted.
projectMuindeterminate (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 nowresolves 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 nowis a parse error.- Backward-anchored diagram with any TBD activity upstream →
projectStartcannot be derived; all four schedule cells render?. The title annotation still names the end-date. time-unit bd+ anchor → warning (bdis treated as calendar days for date math; for business-day scheduling use Gantt).time-unit min/h+ anchor → warning (date display rounds to whole days).
Diagnostic codes.
| Code | Severity | Triggered when |
|---|---|---|
E_PERT_BOTH_ANCHORS | error | start-date AND end-date both present (clears any partial anchor state) |
E_PERT_INVALID_DATE | error | YYYY-MM-DD malformed or fails round-trip date check |
E_PERT_END_DATE_NOW | error | end-date now (only start-date now is valid) |
W_PERT_BD_WITH_ANCHOR | warning | time-unit bd combined with an anchor |
W_PERT_SUBDAY_WITH_ANCHOR | warning | time-unit min/h combined with an anchor |
14. Boxes and Lines Diagrams
Notes. Annotate any box with
note <Box> text— the same hide-able note box as flowcharts (multi-line body, trailing-color word, forward refs,no-notesopt-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 formdirection LR|TBis 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 explicitlow highramp endpointsno-value— suppress the per-box numeric value labels (values render by default; a box value is authored data). Legacyshow-valuesis 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
descriptionis extracted as a dedicated field (not stored in generic metadata)- Multiple
descriptionlines accumulate into a multi-line description - Supports inline markdown:
**bold**,*italic*,`code`,[links](url) - bullet textrenders as• bullet text
13.8 Options
active-tag GroupName— set active tag group for coloringactive-tag none— suppress tag coloringactive-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 highendpoints, in that order — a diverging ramp likeheat Risk green redreads 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 bluekeeps the labelRedwith highblue.
- 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 nonesuppresses 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-solidopts into the full fill), consistent with tag-coloured boxes. Boxes without aheat: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.)
- Two-colour ramp (
- 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):
sort tag:X— tag swimlanes (cross-cutting), takes priority over groups[Group]headers present (nosortdirective) — group swimlanes (default)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 theE_*_REMOVEDfamily 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) orgroup(clustered, side-by-side).seriesis not accepted on a bar chart (decision #24).stack // or: group Cloud Platform blue Legacy Suite red - line — a
seriesblock (every series is plotted). Follows Rule B (prefer the indented block);series A, Bone-line form also parses.series Cloud Platform blue Legacy Suite red - radar — a
seriesblock 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. Sameseriesblock 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-yrestores 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 — barehole, orhole <0–0.9>for a custom inner-radius ratio. The value total shows in the center by default; suppress withno-center-total)- Stacked vs. clustered bars are chosen by the
stack/groupheader above — there is no separatestackedoption. titledirective removed — the chart title is line 1 (bar My Chart). Usingtitleraises 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 nameno-value— hide the numeric valueno-percent— hide the percentage (share-of-total on the pie family; stage-over-stage conversion on funnel)
Each chart family honors only the subset of flags that has a renderable atom on that chart. Flags that don’t apply are silently no-op (typos, future flags, copy-paste from other chart types). Per-chart honoring at runtime:
| Family | Charts | no-name | no-value | no-percent |
|---|---|---|---|---|
| share-of-total | pie, polar-area | yes | yes | yes |
| funnel | funnel | yes | yes | yes (conversion %) |
| cartesian | bar, line, radar | — (axis ticks are separate) | yes | — |
| point-cloud | scatter | yes | — | — |
| density | heatmap | — (axis ticks are separate) | yes | — |
| flow | sankey, arc | name suppression deferred (Phase 2c) | — (Phase 2c) | — |
| spatial | slope, quadrant, venn | name suppression deferred (Phase 2d) | — (Phase 2d) | — |
| single-label | wordcloud | — | — | — |
| functional | function | — | — | — |
There are no show-* flags (org’s show-sub-node-count is the one documented exception, §7.5). A flag exists only to suppress a default-on behavior. To restore a suppressed part, remove the flag.
Cartesian value labels: bar and line render values atop each bar / at each data point by default. On horizontal bars (orientation-horizontal), values render to the right of each bar. labelLayout: { hideOverlap: true } suppresses overlapping labels at high data-point density. Add no-value to opt out wholesale.
Stacked-bar clarification: on a stack-layout bar chart, no-value suppresses per-segment values inside each stack — not stack totals. Stack totals are a separate (currently unrendered) concept.
Eras (line only):
era Day 1 -> Day 3 Rough Seas red
15.1.1 Dual y-axes (line)
A line chart can plot series against two independent y-axes — one on the left, one on the right — so two metrics with unrelated units and ranges share an x-axis (e.g. oil price in $/barrel vs. reserve size in million barrels). Each axis auto-scales to the range of the series assigned to it.
Dual axes are expressed by grouping the series block under axis-label headers. A series block whose indented children include a y-label and/or y-right-label header is in grouped form: each header opens an axis group, and the series indented beneath it are assigned to that axis. The header’s text is that axis’ label.
line Oil Price vs Strategic Reserve
series
y-label $ / barrel
Oil Price blue
y-right-label Million barrels
SPR Size green
China Reserves orange
2020 40 640 920
2021 68 620 1010
2022 95 372 1090
- Left =
y-label, right =y-right-label. Side is keyword-driven, not positional; a group may list any number of series, and the right axis can hold more than one (above,SPR SizeandChina Reserves). - At most two axes. A second
y-label(or secondy-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
seriesblock (no headers) and a top-levely-labeloption behave exactly as before — single-axis charts are untouched. A grouped block with only ay-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-labelwith no series assigned to the right axis → warning (wrap the intended series in ay-right-labelgroup).
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, andbar-stackedchart types no longer exist. Useline+fill(area),line+ aseriesblock (multi-line),pie+hole(doughnut), andbar+ astackheader (stacked bars;groupfor 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 asline+fill, §14; 25% palette tint, or opaque withfill-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 grouping1,000→ 1000 and separator commas both parse, per #28) - Underscore grouping
1_000also 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), matchingorientation-horizontalon bars.- Groups:
[Group Name] colordeclares 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;chordarranges them around a circle (the circular “chord” layout). There is no standalonechordchart type — the circular layout is reachable only througharc+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 XemitsE_VENN_ALIAS_KEYWORD_REMOVEDper 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, yalso 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 Strategycreates a root node labeled “Product Strategy” mindmapalone (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)
descriptionis extracted as a dedicated field (not generic metadata)collapsed: truesets 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 baredescription textform is removed at 1.0 and emitsE_DESCRIPTION_BARE_REMOVED, an error)- Both same-line and indented forms set the same
descriptionfield - Same-line form takes precedence if both are specified
- Multiple
descriptionlines accumulate into a multi-line description - Indented
descriptionmust 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: Engineerbecomes a child node, not metadata) - Supports inline markdown:
**bold**,*italic*,`code`,[links](url) - bullet textrenders as• bullet text
17.5 Multi-Root
mindmap
Q1 Goals
Ship MVP
Q2 Goals
Launch marketing
- No title on first line enables multi-root mode
- Each indent-0 line starts a new root tree
- Title is inferred from first root’s label
17.6 Tags
Standard tag declarations (§1.3):
mindmap Root
tag Priority as p
High red
Low green
Task p: High
Parent → child cascade. A tag value set on a node cascades to its descendants: a sub-node with no value of its own for that group inherits the value of its nearest tagged ancestor. A sub-node’s own value always overrides the inherited one and re-roots the cascade for its own subtree. This lets you tag a category once and have the whole branch pick up the color:
mindmap Wine Varieties
tag Style as s
Red red
White white
Sparkling yellow
Whites s: White
Chardonnay // inherits White
Sauvignon Blanc // inherits White
Loire s: Sparkling // overrides → Sparkling (and its subtree)
Cascade only flows from an explicitly tagged ancestor. A node with no tagged ancestor falls back to the group’s global default (the first declared value) — it does not inherit across an untagged parent.
17.7 Options
| Option | Effect |
|---|---|
active-tag GroupName | Set default active tag group |
color-by-depth | Bare flag; colour nodes by depth instead of by tag (off by default). View-state directive written by the app’s Depth Colors toggle |
Options are bare keywords or key value on their own line, before content.
17.8 Collapse/Expand
- Nodes with children can be collapsed/expanded in the app
collapsed: truein 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 Pagecreates a wireframe titled “Login Page”
18.2 Form Factor
mobile
mobilekeyword switches to a narrow vertical layout (375px viewport)- Default (no keyword) is desktop layout (1200px viewport)
- Must appear before diagram content
18.3 Visual-Mnemonic Element Syntax
Elements use bracket shapes that visually suggest their rendered control:
| Syntax | Element | Notes |
|---|---|---|
[text] | Text input (leaf) or group (if has indented children) | See §18.8 for disambiguation |
(Button) | Button | |
{Option A | Option B} | Dropdown / select | Options separated by | |
<x> | Checkbox (checked) | |
< > | Checkbox (unchecked) | |
(*) Label | Radio button (selected) | |
( ) Label | Radio button (unselected) | |
# Heading | Heading | |
## Subheading | Subheading | |
--- | Divider | Horizontal rule |
- item | List item | |
| Bare text | Text / paragraph | Any line not matching other syntax |
18.4 Keyword Elements
Nine special keyword elements render common UI patterns:
| Keyword | Description | Hints / Options |
|---|---|---|
nav | Navigation bar | |
tabs | Tab bar | |
table | Data table | table RxC for skeleton shorthand (e.g. table 5x3) |
image | Image placeholder | round, wide |
modal | Modal dialog | Has indented children for modal content |
skeleton | Loading skeleton | |
alert | Alert / notification | |
progress | Progress bar | Value 0–100 (e.g. progress 65) |
chart | Chart placeholder | line, bar, pie |
Table explicit rows: comma-separated values, first row is the header.
table
Name, Email, Role
Alice, alice@co.com, Admin
Bob, bob@co.com, User
Image and chart hints appear after the keyword:
image round
image wide
chart line
chart bar
chart pie
18.5 Flags (Trailing-Keyword List)
(Submit) primary
[Email] disabled
(Delete) destructive ghost
Wireframe flags are a trailing-keyword list in the same slot occupied by the §1.5 trailing-token color rule on other chart types: any whitespace-delimited lowercase tokens at the end of an element line, drawn from the closed 16-keyword flag enum below, are extracted as flags. Multiple flags are space-separated (NOT comma-separated). Flag extraction stops at the first token that is not in the enum — everything to the left is the element’s literal label.
(Submit) primary // label = "Submit", flags = [primary]
(Submit) primary destructive // label = "Submit", flags = [primary, destructive]
(Save Changes) primary // label = "Save Changes", flags = [primary]
Available flags (closed enum — 16 keywords):
| Flag | Effect |
|---|---|
disabled | Greyed-out, non-interactive appearance |
active | Active / current state |
selected | Selected state |
empty | Empty / placeholder state |
ghost | Ghost / outline-only button style |
destructive | Destructive action (red-tinted) |
success | Success state (green-tinted) |
warning | Warning state (yellow-tinted) |
info | Info state (blue-tinted) |
scrollable | Scrollable container |
collapsed | Collapsed container |
toggle | Toggle switch appearance |
password | Password input (masked) |
textarea | Multi-line text input |
horizontal | Horizontal layout for group children |
primary | Primary / emphasized button style |
Indented fallback
For elements where the trailing-keyword form is ambiguous or visually noisy (long flag lists, labels that need to clearly read as labels), drop the trailing tokens and indent the flags one level deeper. Both forms produce the same parsed element.
(Submit) primary destructive // canonical trailing-keyword form
(Submit) // indented fallback — same semantics
primary
destructive
Indented flag lines under a wireframe element follow the same reserved-key-registry dispatch (§1.4.2): the parser recognizes lines whose first token is in the 16-keyword flag enum and attaches them as flags. Indented lines that don’t match flag keywords fall through to child-element grammar.
Label-vs-flag collision
When an element’s literal label happens to lexically match a flag keyword (e.g. a button literally named “Primary”), authors have two escape hatches:
- §1.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 loneprimaryis the label, not a trailing token). - Indented form. Drop the trailing tokens and indent flags — unambiguous regardless of label content.
(Primary) // label = "Primary", no flags
(Primary) primary // label = "Primary", flags = [primary]
(Primary) // indented fallback for clarity
primary
Authoring guidance: capitalize wireframe element labels ((Submit), (Cancel), (Delete) rather than (submit), (cancel), (delete)) to match real-world UI button conventions and to keep the lowercase flag enum from colliding lexically with labels. Lowercase labels remain valid but produce visually confusing source where label and flag share the same token shape.
18.6 Multi-Element Lines
Two or more elements on the same line, separated by 2+ spaces:
[First Name] [Last Name]
(Cancel) (Submit) primary
- 2 segments with bare text + element = label-field pairing:
Email [user@example.com] - 3+ segments = inline items laid out horizontally:
(Back) (Next) (Skip)
18.7 Layout Model
- Desktop (default): 1200px viewport, horizontal regions. Region names like “sidebar” get smart sizing (~25% width); “main” or “content” fills remaining space.
- Mobile: 375px viewport, all regions stack vertically.
Regions are defined by top-level groups:
wireframe Dashboard
[Sidebar]
nav
- Overview
- Settings
[Main]
# Dashboard
chart line
18.8 Group Disambiguation
The [text] syntax is ambiguous — it renders as a text input when it has no children, or a group/container when it has indented children:
[Search] // text input (no children = leaf)
[Search Form] // group (has children below)
[Query]
(Search)
Group-only flags (horizontal, scrollable, collapsed) force group interpretation even without children:
[Empty Panel] collapsed // group (forced by flag)
18.9 Full Example
wireframe E-Commerce Product Page
[Header]
nav
[Search] empty
(Cart) (Account)
[Breadcrumb] horizontal
- Electronics
- Phones
- Galaxy S25
[Main Content] horizontal
[Product Images]
image wide
image image image
[Product Details]
# Galaxy S25
## $899.99
{Black | White | Blue}
Quantity [1]
(Add to Cart) primary
(Buy Now) ghost
---
## Description
The latest flagship phone with advanced AI features,
a stunning display, and all-day battery life.
---
## Specifications
table
Feature, Detail
Display, 6.2" AMOLED
Processor, Snapdragon 8 Gen 4
RAM, 12GB
Storage, 256GB
[Reviews]
# Customer Reviews
progress 85
table 3x4
[Footer]
- About
- Contact
- Privacy Policy
- Terms of Service
20. Tech Radar Diagrams
Tech radars visualize technology adoption decisions across an organization, inspired by the ThoughtWorks Technology Radar format.
Declaration
tech-radar Title
Rings Block
Ring names are declared in a rings block, one per indented line. Order is innermost (first) to outermost (last). Variable count supported.
rings
Adopt
Trial
Assess
Hold
Quadrant Sections
Exactly 4 quadrants required, each as a top-level section header with same-line metadata specifying position:
Techniques quadrant: top-right
Tools quadrant: top-left
Platforms quadrant: bottom-left
Languages & Frameworks quadrant: bottom-right
Positions: top-left, top-right, bottom-left, bottom-right. Each position must be used exactly once.
Optional color override: Tools quadrant: top-left, color: purple
Default colors by position: top-left=blue, top-right=green, bottom-left=red, bottom-right=orange.
Blips
Blips are indented under their quadrant. Each blip requires ring metadata matching a declared ring name. Optional trend metadata specifies movement direction.
Continuous Deployment ring: Adopt, trend: stable
Micro Frontends ring: Trial, trend: up
ring (required): Must match a declared ring name exactly (case-insensitive).
trend (optional): new, up, down, stable. No indicator rendered if omitted.
Descriptions
Further-indented lines below a blip are collected as description text. Supports simple markdown (bold, italic, inline code, links).
Rust ring: Assess, trend: new
Evaluating for **performance-critical** services.
See [evaluation doc](https://wiki.example.com/rust).
Global Numbering
Blips receive sequential global numbers for unambiguous reference. Order: quadrants clockwise (top-left, top-right, bottom-right, bottom-left), then by ring (innermost first), then by declaration order.
Directives
Top-level bare keywords that toggle rendering behavior. Place after the chart-type declaration, before the rings block.
| Directive | Description |
|---|---|
no-blip-legend | Suppress the four-column blip listing. The listing renders by default on every surface (CLI export and live/interactive alike; the old surface-dependent default is gone — decision #48). Legacy show-blip-legend accepted as a no-op. |
Complete Example
tech-radar Engineering Tech Radar Q2 2026
rings
Adopt
Trial
Assess
Hold
Techniques quadrant: top-right
Continuous Deployment ring: Adopt, trend: stable
Fully adopted across all services.
Infrastructure as Code ring: Adopt, trend: stable
Micro Frontends ring: Trial, trend: up
Tools quadrant: top-left
GitHub Copilot ring: Trial, trend: new
Vite ring: Adopt, trend: up
Webpack ring: Hold, trend: down
Migrating to Vite across all projects.
Platforms quadrant: bottom-left
Kubernetes ring: Adopt, trend: stable
Cloudflare Workers ring: Trial, trend: up
Languages & Frameworks quadrant: bottom-right
TypeScript ring: Adopt, trend: stable
Rust ring: Assess, trend: new
Evaluating for performance-critical services.
React ring: Adopt, trend: stable
21. Cycle Diagrams
Cycle diagrams show a circular process flow where nodes are arranged on a circle and directed edges connect each node to the next, with the last node wrapping back to the first. Common use: OODA loops, PDCA, product lifecycles, continuous improvement.
Render category: visualization
20.1 Chart Type Declaration
cycle [Title]
20.2 Nodes
Top-level (non-indented) lines declare nodes. Nodes are positioned on a circle in declaration order.
NodeLabel
NodeLabel color: blue, span: 3, description: inline text
Node Same-Line Metadata
| Key | Type | Default | Description |
|---|---|---|---|
color | palette name | first palette color (uniform) | Node fill color |
span | positive number | 1 | Relative arc distance to next node. Decimals allowed (e.g., 1.5). Zero or negative → parse error |
description | string | — | One-liner description (alternative to indented text) |
20.3 Node Descriptions
Indented lines under a node are descriptions. Markdown inline formatting is supported: **bold**, *italic*, `code`, [links](url). Bullet points: - item renders as • item.
Observe blue
Gather raw information from the environment
Monitor unfolding circumstances
If both description: text (same-line) and indented lines exist, they concatenate (same-line first, then indented).
20.4 Edges
Edges are implicit — every node connects to the next in declaration order, with the last wrapping to the first. The -> syntax annotates edges with labels, descriptions, and styling; omitting -> lines just means unstyled edges.
-Label-> color: red, width: 6
->
-> width: 4
Indented under a node, before or after description lines. Explicit targets after -> (e.g., -> Orient) are accepted but silently ignored — cycle edges always connect to the next node. If the explicit target doesn’t match the actual next node, an info diagnostic is emitted.
Edge Same-Line Metadata
| Key | Type | Default | Description |
|---|---|---|---|
color | palette name | inherits source node color | Edge stroke color |
width | number (px) | 3 | Edge stroke width in pixels (values below the 2 px floor are clamped up) |
Edge Descriptions
Indented lines under an edge line are edge descriptions:
Observe blue
-Unfold circumstances->
Synthesize raw data into actionable context
Identify **key patterns** and anomalies
20.5 Directives
| Directive | Description |
|---|---|
direction-counterclockwise | Reverse cycle direction (default: clockwise) |
circle-nodes | Render nodes as circles instead of rounded rectangles (uniform diameter) |
20.6 Parsing Constraints
- Node labels cannot contain
->or<-— parse error with helpful message - Lines starting with
-followed by text and->are edges; bare-followed by non-arrow text is a description (bullet point) - Minimum 2 nodes required — parse error on fewer
spanmust 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>) emitsE_JOURNEY_BARE_SCORE_REMOVEDwith hint to usescore: <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
| Directive | Effect |
|---|---|
active-tag GroupName | Set the active tag group for coloring |
Visual Rendering
- Emotion curve: filled area chart (mood landscape) with gradient — green above midline (3), red below. The curve is the hero visual element.
- Grid lines: subtle horizontal lines at score levels 1–5 behind the curve.
- Step cards: color-intensity mapped to score (1=red → 5=green, palette-aware).
- Phase headers: color auto-derived from average step score within the phase.
- Sharp-drop zones: auto-accent where score drops by ≥ 2 between consecutive scored steps.
- Score legend: auto-generated.
Flat Mode
Steps at root level (no [Phase] brackets) render as a continuous horizontal strip with emotion curve:
Full Example
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 outMinimal Example
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.
| Key | Value | Example |
|---|---|---|
color | palette color name | Safety color: blue, description: "Security, health" |
description | description (quote when containing commas) | Safety description: "Security, health" |
When color is the only thing you need to set, use the trailing-token form (Safety blue). The color: <name> long form is reserved for cases where color rides alongside another key.
Love & Belonging orange
Friendship, intimacy, family.
23.5 Directives
| Directive | Effect |
|---|---|
inverted | Flip the pyramid so the apex points down (funnel orientation). Source order is always top-to-bottom visually — the first layer in the file is the widest (visual top); the last layer is the apex (visual bottom). |
23.6 Rendering
- Default colors: each layer’s fill is a ramp from
palette.primarymixed withpalette.bg— apex is the palest, base is full primary (the one documented exception to §1.3’s canonical rotation — decision #48). Per-layercolor: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
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
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.
| Key | Value | Example |
|---|---|---|
color | palette color name | Crew color: blue, description: Deckhands and gunners |
description | description (quote when containing commas) | Crew description: "Deckhands, gunners" |
When color is the only thing you need to set, use the trailing-token form (Crew blue). The color: <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
| Directive | Effect |
|---|---|
fill-solid / fill-outline | Render rings with full intent color, or outline-only, instead of the default 25% tint (§1.9 fill family). |
inverted is not valid on ring diagrams (rings are rotationally symmetric — visual inversion is meaningless). Using it emits an error-severity diagnostic and the line is discarded so it does not become a layer named “inverted”.
24.6 Rendering
- Concentric circles drawn core-out. Each ring band has equal radial thickness (
outerRadius / N). The innermost element is a filled disc, not an annulus. - Default colors: layers cycle through the canonical categorical rotation (§1.3): red, orange, yellow, green, blue, purple, teal, cyan — reused when there are more than 8 layers.
- Contrast stroke: a 1px stroke at every ring’s outer edge keeps adjacent tints from blurring together. Renderer-only; no syntax surface.
- Labels: centered horizontally on the diagram axis. The innermost label sits at the center of the disc; outer-ring labels stack vertically along the band’s midline above center. Font size scales with band thickness, clamped 12–20 px.
- Side description list: rendered to the right of the diagram, top-to-bottom in source order. Each entry has a colored accent bar matching its ring. When no layer carries a description, the list is omitted and the diagram expands to fill the canvas.
24.7 Complete Example
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_000is 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:
| Input | Ramp |
|---|---|
| no colors, data crosses 0 | diverging red · neutral · green, midpoint pinned at 0 |
| no colors, one-sign data | sequential neutral · accent (palette primary) |
one color c | neutral → c (map’s one-color rule, §24B) |
two colors lo hi | lo → high with a neutral midpoint |
Named palette colors only — hex is rejected (an unrecognized token is left as label text, never a color).
24C.5 Node Metadata
| Key | Value | Example |
|---|---|---|
heat | number (signed/float) | AAPL 180 heat: -2.4 |
<tag> | a declared tag value | Engineering t: Eng |
value is not a metadata key — size is the bare trailing number.
24C.6 Directives
| Directive | Effect |
|---|---|
heat <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 N | Render N levels; deeper subtrees collapse to a drillable solid block. A render budget, not a data limit (the full tree is always drillable; static export renders it whole). |
no-value | Hide value labels (plural no-values accepted as silent alias). |
no-percent | Hide percentage labels. |
no-headers | Hide parent header bars. |
no-legend | Hide the legend. |
Everything is on by default; subtract with no-*. Numbers auto-compact (1.2M, 940k).
24C.7 Color Mode Selection
The source-declared default mode is resolved heat → tag → branch: any heat: present → the heat ramp colours cells at rest; else tags declared → the first tag group; else branch (each top-level branch a distinct hue, tinted with depth). This is the universal precedence shared with map (§24B.4) and boxes-and-lines — decision #48. The active-tag directive (§24C.6) overrides the resting dimension from source. The desktop app adds a runtime switcher (heat/tag/branch) in the legend/settings pull-tab — live preview only, it does not edit source (same contract as the map’s active-tag legend click).
24C.8 Depth Collapse
A depth-collapsed node keeps its full summed value (area unchanged); only its descendants are not drawn. Drilling re-applies the depth window from the new root.
Removed:
other-below N(small-leaf rollup into an “Other” bucket) was dropped — small children render as their own cells. The directive is still recognized but ignored with a warning so older sources don’t break.
24C.9 Interactivity vs. Export
In the desktop app a treemap is interactive: click a parent to drill in (with a breadcrumb), hover for a tooltip (path / value / % of parent / % of root / heat), legend hover-dim, and the color-mode switcher. Static SVG / PNG export is the clean, full tree with none of that chrome (focus icons carry data-export-ignore) — the same interactive-vs-export split the map chart uses.
24C.10 Radial Mode (Sunburst / Hierarchical Pie)
A bare radial flag renders the same parsed hierarchy as a sunburst — a hierarchical, multi-ring pie — instead of nested rectangles. Same data model, polar geometry.
- 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-headersis 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.
depthstays 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) — usepiethere.
radial is a directive on treemap, not a separate chart type.
24C.11 Complete Example
24A. RACI Matrices (RACI / RASCI / DACI)
A tasks × roles assignment matrix with author-time linting against the
defining structural rules of each variant. One chart type — raci —
covers all three variants. The variant is inferred from the markers
used — there is no directive to lock it.
| Variant | Marker alphabet | Constraint |
|---|---|---|
| RACI | R A C I | Exactly one Accountable per task |
| RASCI | R A S C I | Exactly one Accountable per task |
| DACI | D A C I | Exactly one Driver and one Approver per task |
Grammar
raci [Title]
[directives]
// optional bracketed phase header (trailing-token color, or same-line metadata when other keys ride along)
[Phase Label] [color]
Task name
// description lines are multi-line, before the first role
Optional description line
// markers from the resolved variant's alphabet
Role: <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
Dpresent → DACI. - Any
Spresent → 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 yellowWhen present, the
rolesdirective enables theW_RACI_UNKNOWN_ROLEwarning 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 indentedrolesblock consumes the following lines.
Diagnostic codes
| Code | Severity | Fires when |
|---|---|---|
E_RACI_MULTI_ACCOUNTABLE | error | A RACI/RASCI task has more than one A |
E_DACI_MULTI_DRIVER | error | A DACI task has more than one D |
E_DACI_MULTI_ACCOUNTABLE | error | A DACI task has more than one A |
E_RACI_INVALID_MARKER | error | A marker isn’t in the resolved variant’s alphabet |
E_RACI_UNEXPECTED_LINE | error | Free-text line appears after the first role assignment |
E_RACI_MIXED_VARIANTS | error | Both D and S markers appear in the same chart |
W_RACI_MISSING_ACCOUNTABLE | warning | RACI/RASCI task has no A |
W_RACI_MISSING_RESPONSIBLE | warning | RACI/RASCI task has no R |
W_DACI_MISSING_DRIVER | warning | DACI task has no D |
W_DACI_MISSING_ACCOUNTABLE | warning | DACI task has no A |
W_RACI_UNKNOWN_ROLE | warning | Role used without being declared in roles: |
Warnings are non-blocking; sketching authors can leave them noisy and address them when polishing. There is no chart-wide or per-task suppression mechanism in source — by design, source should not silently mute the linter.
Display vs source ordering
Markers in cells are always rendered in canonical alphabet order
(R A C I, R A S C I, D A C I) regardless of source order. Source casing
and order are preserved in the file itself; mutations operate on source order
to keep round-trips byte-stable for everything except the cell that changed.
24B. Map Diagrams
Geographic concept maps: highlight/shade political subdivisions, drop points of interest, and connect them with routes or edges. Built for “share a concept” business maps (low detail), not cartography. Renders at a fixed position — no pan/zoom (cf. §17 visualization conventions). Basemap and viewport are inferred from the content you reference — most maps need no directives beyond the data itself.
24B.1 Declaration
map [Title]
Requires explicit first line — no heuristic detection.
24B.2 Basemap & viewport (inferred)
Both are inferred from the content you reference — normally there is no directive:
- Boundaries — country outlines are the base, always. For any country whose subdivisions you reference (value/tag a US state), that country’s subdivision boundaries auto-load. Data follows content.
- Viewport — auto-fits to the bounding box of everything referenced (valued/tagged subdivisions + POIs) + padding. List Caribbean POIs and you get a Caribbean map; no preset needed. A country fill frames on its dominant landmass cluster, not its full territorial bbox: far-detached minor territories (overseas DOM such as French Guiana, distant small islands) are excluded from the fit, so a Europe map that names France stays on metropolitan France rather than zooming out across the Atlantic. Large detached parts (Alaska-scale) and near-shore islands stay in frame, and the territories still draw — they are just not allowed to balloon the viewport.
- Resolution is a post-parse pass — the parser collects referenced names, then a resolver determines the basemap(s), validates names (did-you-mean), lazy-loads geometry, and computes the fit. This is why “Georgia” is never ambiguous mid-parse.
The US-state mesh and US name-scoping are inferred from the content: reference any US state (California heat: 92, Texas red), a US-scoped POI, or locale US and the map renders as the conventional US states map — the full state mesh on albers-usa, every state outlined even where it carries no data. A POI-only US map (named US cities, no state) gets the same national US frame; you don’t draw three floating dots on a blank country. There is no region directive — the world basemap is the default and US subdivisions follow the content.
A map is “US-oriented” (gets the full state mesh, and AK/HI insets whenever it frames the whole nation) when it has US content and all other content stays within North America — i.e. every other POI/region resolves to the US, Canada, or Mexico. A US-oriented map frames the whole nation on albers-usa when its data is national in spread; a region/choropleth map whose coloured states are geographically compact auto-zooms to that region (on mercator — North stays straight up, the same projection a sub-national POI cluster uses) instead of stranding the data in a corner of an empty nation. “Compact” is inferred from the raw data bounding box — it must occupy less than ~40% of the contiguous-US area (so the Northeast, or a single state plus its neighbours, zooms, while a coast-to-coast spread or a near-national diagonal stays national). Two cases always stay national regardless of compactness: any data touching Alaska or Hawaii (the composite insets are inherently a national device), and an explicit locale US (country, no subdivision) — the documented escape hatch for a compact map you want framed as the whole country. There is no force-zoom override (the failure being avoided is “too zoomed out”). POI-only US maps are unaffected — they keep their fit-to-cluster behaviour (below). A near-border neighbour is framed alongside the states rather than clipped: a single Canadian-city POI (e.g. the Toronto Blue Jays in an otherwise-US baseball map) no longer drops the map to a world projection, and no locale US is required. A Canada/Mexico POI expands the frame by just its point (the US barely shrinks); a Canada/Mexico country fill expands it to that country’s full geometry (so the whole neighbour shows). Content reaching beyond North America — any non-NA POI, or a non-US/CA/MX country fill — keeps a geographic world/regional projection that frames everything (nothing is clipped). The US state mesh draws when US states are referenced as data (California heat: 92) or the map is US-oriented (all content within North America). It is suppressed on a global map whose only US content is bare POIs — a worldwide backbone with us-east-1 + Tokyo renders country-only, since exploding the US into 50 states reads as noise once the frame spans beyond NA. (A US-state data reference always wins: California heat: 92 + Tokyo still outlines the states, because the states are the subject.) Alaska & Hawaii always appear as insets on the national frame — see below; the main frame itself shows the lower-48 + DC, with AK/HI in their lower-left boxes. A country-level United States fill on its own is not US content — it keeps the US as a single shape (no state mesh).
Projection is always inferred — there is no projection directive:
projectionis auto-picked by extent span: the US-oriented national case (US content + only US/Canada/Mexico elsewhere, national spread) getsalbers-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 useconic-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 — usemercatorso 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): thealbers-usachoice 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 toconic-equal-areaframed 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 keepsalbers-usa(its insets are the right tool).- Alaska & Hawaii insets under
albers-usaare 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 underno-colorize), so they read as the rest of the country, not as data. Onlyalbers-usa(the whole-nation frame) shows them; a compact/sub-national US map (the auto-zoom or a fit-to-cluster POI map onmercator) frames just its region with no insets. There is no flag.
Basemap dress — how the land/water themselves are coloured — is inferred from content, with one opt-out (no-colorize). Colorize (distinct political fills) is the default; the only things that turn it off are region data or the opt-out:
| Content | Dress |
|---|---|
Any region data (heat: or a tag) | Choropleth / categorical hues on a neutral-gray basemap — the data owns the only saturation (the choropleth convention). Colorize is auto-suppressed (no error). |
No region data — named regions, POI/route-only, or a bare map | Colorized — every drawn region gets a distinct pastel “political map” fill (see below). POIs/routes draw on top. |
no-colorize | Forces the plain green land + blue water dress (the opt-out). |
- Colorize (inferred political fills). Unless a region carries data (a
heat:or a tag), the map is dressed as a political map: every region drawn at the resolved extent is filled a distinct light pastel such that no two bordering regions share a hue — the conventional “colour the countries/states so neighbours separate” look, with zero config. This holds across the international border too: a US state never shares a hue with the Canadian/Mexican land it touches (e.g. Maine vs Canada). It is the default for any non-data map — named-region maps, POI/route-only maps, and even a baremap(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.dgmocolours 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 anyheat:or tag flips the map to the data dress automatically (colorize is auto-suppressed — no error). Useno-colorizefor 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-reliefis 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.jsondata asset; if a build predates it, relief simply draws nothing (no error).
Coastline — faint nautical-chart water-lines (default ON; no-coastline turns it off):
- Draws 5 faint, equal-width coast-parallel water-lines stepping offshore and fading seaward — the antique nautical-chart depth-contour look — so the eye instantly separates a water border (ocean/lake shore) from a land border (the shared edge between two adjacent regions). It applies to the outer ocean coast, inland lake shores, and the AK/HI inset boxes.
- The lines sit on the water side only: each land outline is buffered into a thin ring and revealed only where it falls on water (via an SVG mask of water = canvas − land + lakes), so land/land borders self-remove (no separate boundary data needed). They draw above the water/land/choropleth fills and below rivers, POIs, edges, and labels. Where relief also draws, the two never overlap — water-lines stay on water, relief stays on land. Decorative: no knobs, no labels, no data attributes.
- The offshore distance is a fixed fraction of the canvas, so the rings stay proportional at any export size and any geographic extent, and they carry through to every edge of the visible map. Small islands (Hawaii, the Caribbean chain, etc.) are ringed — the island hashing is part of the nautical look; only sub-threshold specks are dropped to avoid confetti. Polygon holes/enclaves (e.g. Lesotho inside South Africa) are not ringed; very narrow inlets/fjords may fill solid at small scale (accepted/artistic).
- Needs no data asset — geometry is derived from the drawn region paths.
Context labels — orientation backdrop (default ON; no-context-labels turns it off):
- Paints a sparse backdrop — water-body names plus a few notable unreferenced countries — so a reader can place the view at a glance. It is orthogonal to region labels: region labels name the regions you referenced; context labels name the surrounding context (oceans, seas, neighbouring countries) and never double-label a region already labeled.
- Water (oceans, seas, gulfs, bays, straits, channels, sounds — no rivers) is named in full, always, in a muted blue-gray italic with letter-spacing; names and prominence come from Natural Earth marine data (neutral, citable provenance — the single editorial override is
Gulf of Mexico→Gulf of America). - Countries that you didn’t reference are picked by on-screen size and labeled only where the name fits the footprint; they use the ISO code at the compact render width, the full name otherwise (mirrors the region-label abbreviation policy).
- Density is deliberately low. A small per-view label budget plus a span-derived priority (oceans → major seas → minor water; biggest countries first) keeps it clean: a world view shows oceans and major seas only — never bays; broader-detail views admit smaller features as space allows. Under dense data it degrades to zero context labels (data already orients the reader); context labels are placed dead-last and never displace a data, region, or POI label. There is no density knob — it should never look cluttered.
- On a US (
albers-usa) view the surrounding oceans (Pacific to the west, Atlantic to the east) and the Gulf of America, plus neighbouring countries (Canada, Mexico, …), are labeled too — oceans whose centre is just off-frame are edge-clamped to the margin, while distant oceans and off-frame AK/HI water are dropped (so nothing is mislabeled). Smaller basins keep the strict on-screen rule. - Water names sit only over open water — a candidate whose footprint would touch land (or doesn’t fit the canvas) is excluded rather than placed. Country names sit over their country.
- Needs the optional
water-bodies.jsonasset; absent → no water labels (no error).
- 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
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
poimarker (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 takesize:→ marker radius, edges takewidth:→ 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 → explicitlow highendpoints 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 bluekeeps the labelRed, highblue).- The ramp auto-fits — there is no
scaledirective. 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 ascale/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.
Californiaover39.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-valuesuppresses the value line entirely while keeping the name. - A subdivision with no
heat:(and no tag) renders as the neutral base. Heat input is theheat: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>— theregion-heatlabel, orValueif none — re-selects the choropleth ramp. (There is no magicscore/heattoken; 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-tagis 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 positional —
poi <lat> <lon>(e.g.poi 39.74 -104.99). Two leading numbers occupy the name region, soas <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 (seepoi-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 orclock: <zone>, §24B.14), the applied tag alias,as. Noiconin 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 Orangeis the place,poi orangeis 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-> destinationedge may coexist under apoi(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 label | labeled | |
|---|---|---|
| directed straight | A -> B | A -ships-> B |
| directed arc | A ~> B | A ~trade~> B |
| undirected straight | A -- B | A -ferry- B |
| undirected arc | A ~~ B | A ~cable~ B |
// one-off, directed
A -> B
// undirected line; width = thickness (see flow-width, §24B.7)
A -ferry- B width: 12
// undirected arc with a label
A ~cable~ B
// inline chain (ordered, but not a numbered route)
A -> B -> C
// hub/star — ONE edge per line, repeat the origin
// (indented `-> dest` is a parse error outside a route)
JFK ~daily~> LAX
JFK ~daily~> LHR
// the connector label carries the relationship
JFK ~2x daily~> SFO
- Edge label = the relationship (
-ships->,-ferry-);width:(line thickness) rides as edge metadata. - For a hub/star (many edges from one origin — an airport’s daily flights, a depot’s shipments), repeat the origin on a one-line edge per spoke (
JFK ~daily~> LAX,JFK ~daily~> LHR). There is no indented / shared-source hub form for native edges — an indented-> destoutside arouteerrors asMalformed edge. For an ordered chain use arouteblock or an inlineA -> B -> C; route legs are inherently directional voyages. Edge endpoints auto-create their POIs, so a connected map needs no separatepoilines. - 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
poiline (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);captionis a directive (in-image data-source attribution — the only provenance carrier on an exported PNG). There is notitleorsubtitledirective — the title is the first line. -
region-heat <label>(region colour ramp),poi-size <label>(POI marker size), andflow-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 onregion-heatsets 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):Flag Turns 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-heatchoropleth (§24B.3). The name still renders (that’sno-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, likeno-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-colorizeis 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-GAresolves a bareAthensto 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 naminglocaleat 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 anas <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 (sopoi Portland ORworks standalone). Any other bare two-letter token (CR,FR,JP) is a 3166-1 country code; the explicitUS-XXform 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 (
Georgiais both 3166-1 countryGEand US stateUS-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 redundantGeorgia US-GA(still accepted, and a mismatched code likeGeorgia US-CAis rejected). Bare codes never warn; an explicit scope on a named region silences the ambiguity warning. An inferred US-scope signal (alocale USdirective, 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, sopoi JFKandroute JFK -> LAXjust 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/JFKalike). 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 — useSNA); the airport name shows in editor completion to disambiguate look-alike codes (ORD vs ORF). A POI’s label is the typed code (poi JFKrenders “JFK”); override withlabel: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 anas <CODE>coordinates hint. - A city name must match the gazetteer’s canonical form.
New York City, notNew York;Washington, D.C., notWashington. 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,--jsonfor machine output) and thelookup_map_locationMCP tool substring-search the gazetteer cities and the bundled airports, returning the exact token to use (york→New York City;heathrow→LHR). There is deliberately no exhaustive printed catalog — thousands of cities + ~1,500 airports — so search/completion is the discovery interface; both reuse the same fold→lookup data the resolver does. - Positional coordinates (
poi <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.
| Condition | Behavior |
|---|---|
| Subdivision not in basemap | error + did-you-mean |
| POI name not in gazetteer & no coords | error + did-you-mean |
| Unknown three-letter (airport-shaped) token | error naming it an airport code + as <CODE> coordinates hint |
| Token resolves to a city but is also an IATA code | city wins; non-blocking info hint names the airport escape (as <CODE>) |
heat: + tag on same region | both kept as selectable colouring dimensions; active one fills (default: the heat ramp). No warning — §24B.4 |
| Ambiguous bare city name | resolves most-populous in scope + info note |
| Duplicate region / POI | last-wins + duplicate warning |
| Empty map (no referenced content) | valid — renders inferred base map |
Wrong-channel key on an element (size:/width: on a region, heat:/width: on a POI, heat:/size: on an edge) | error naming the element’s correct channel key (regions heat:, points size:, edges width:) |
Bare-coord clock pin with no derivable zone | warn W_MAP_CLOCK_TZ_NEEDED; pin renders without a card — add clock: <zone> (§24B.14) |
clock: zone contradicts a named city’s real zone | warn W_MAP_CLOCK_TZ_OVERRIDE; the override is honored (§24B.14) |
Unparseable clock: zone | warn W_MAP_CLOCK_TZ_INVALID; pin renders without a card (§24B.14) |
24B.11 Data & rendering (v1)
- Boundaries: world countries (8% simplified ≈ 11 KB gz coarse + 50m for regional views) + US states (15% ≈ 7 KB gz). Lazy-loaded TopoJSON; not bundled into non-map diagrams.
- Gazetteer: GeoNames-derived (geonameid + population + alternateNames), world majors + US cities, minified to
{name → [lat, lon], iso, pop}(~25–30 KB gz). Single asset shared by the renderer and the editor completion source. - Airports: OurAirports-derived (public domain), large international hubs + all US scheduled-commercial airports, in a separate optional asset keyed by IATA code (~38 KB gz). Lets
poi JFK/route JFK -> LAXresolve 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-poisforces the fanned form everywhere); parallel/opposing edges between the same endpoints fan out and each keeps its ownweight; 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:anddescription: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+ edgewidth: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; theworld/us-statesselector 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), andscale … center <n>parse-acceptance (the diverging-ramp seam below stays documented but is no longer parsed until built). Mergeddefault-country+default-stateinto onelocale <ISO>directive — and wired its subdivision part (the olddefault-statewas parsed but never consumed;locale US-GAnow genuinely scopes bare-city resolution to Georgia). Withregion us-statesgone, the US-state mesh now renders for any all-US-content map — including POI-only named-city maps, which snap to the nationalalbers-usaframe with every state outlined (replacing the old “POI-only US maps stay on a geographic projection” behavior). Theregion-heat/poi-size/flow-widthtrio is retained pending richer hover/legend interactions that will lean on the per-channel labels.Cut 2026-06-02 (defaults-on review, 16 → 12): cosmetic directives flipped to on-by-default with bare
no-*opt-outs —coastline→no-coastline,relief→no-relief,context-labels on|off→no-context-labels,region-labels full|abbrev|off→no-region-labels(now auto full → abbrev → hide, US-state abbrev only),poi-labels off|auto|all→no-poi-labels(auto only; theallmode dropped). Removedprojection(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, andsurface— both the directive and thesurface:leg/edge metadata (parser stripped in this cut; thesurface-route.tsrenderer module plus theResolvedEdge.surface/MapDirectives.surface/route-leg/edge type fields were fully removed in a 2026-06-02 follow-up).captionwas 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; onlyno-reliefhides it. See the relief decision entry.)Add 2026-06-03 (colorize, 12 → 13): added
no-colorize(the only new directive — a seventhno-*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 baremapcolour 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 (anyheat:/tag → the gray choropleth/categorical dress, no error) orno-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-colorizeis 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 eighthno-*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 ninthno-*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). Explicitpoimarkers always win — a dot never sits under a referenced marker. Reuses the bundledgazetteer.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-usano 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 underno-colorize), so they don’t masquerade as data. Compact/sub-national US maps (the auto-zoom region/choropleth and fit-to-cluster POI maps, bothmercator) are unchanged — they frame just their region with no insets. Still no directive — content/extent decides; theno-insetsopt-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 optionalairports.jsonasset (§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 theas <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 theas <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_airporttag 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 —
scaleis 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 existingimport:/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
timezonein the gazetteer (§24B.11), so a bareclockflag needs no zone typed —poi Denver clockderivesAmerica/Denvercorrect-by-construction (Austin→America/ChicagoCentral, 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). Bareclockvsclock: <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’sasalias is a single ≤12-char routing token and does not render (§24B.5). -
Availability window — two header directives,
hours <start>-<end>andworkweek <range>(hours 9-17,workweek mon-fri; legacydaysaccepted as an alias), define an open/closed schedule evaluated per pin in its own local zone. Each card carries a status dot: green open · amber soon (near a window edge) · hollow closed (outside hours, or a weekend/non-listed day). -
Card contents: the local time (large
HH:MM, with seconds + am/pm bracketing it), the status dot, and the weekday — the weekday shows only when the pin’s day differs from the viewer’s (dateline safety, so a card never redundantly restates today). -
Zero runtime deps —
Intl.DateTimeFormatsupplies time/offset/weekday and the gazetteer supplies the zone (the same solar math used elsewhere on the map is reused for daylight cues). -
Warnings (non-blocking — the pin still renders, just without a card):
Warning Cause W_MAP_CLOCK_TZ_NEEDEDa bare-coordinate clockpin with no derivable zone — addclock: <zone>W_MAP_CLOCK_TZ_OVERRIDEa named city’s clock:contradicts its real gazetteer zone — honored, but flaggedW_MAP_CLOCK_TZ_INVALIDthe clock:value is an unparseable zone
25. Colon Usage Summary
Constructs Where Colons Are REQUIRED
| Construct | Diagram Type | Example |
|---|---|---|
| Same-line metadata | all metadata-bearing types (wireframe uses a trailing-keyword flag list; flowchart has none; state takes tag-value keys only) | Foo key: value, key2: value2 |
| Indented metadata (reserved-key) | all metadata-bearing types (same carve-outs as above) | description: Main gateway (indented) |
| Class field types | class | + name: string |
| Class method returns | class | + sail(): void |
| Function expressions | function | f(x): x^2 + 1 |
| Hide tag values | boxes-and-lines | hide phase:Planning |
| Sort/lane tag selector | gantt, timeline | sort tag:Team |
| Infra node properties | infra | latency-ms: 50 |
Colons OPTIONAL
| Construct | Diagram Type | Example |
|---|---|---|
| Class relationship label | class | `— |
| RACI role assignment | raci | Cap: A or Cap A (role marker lines) |
Colons NOT USED
| Construct | Diagram Type | Example |
|---|---|---|
| Chart type declaration | all | bar Title |
| Tag declarations | all | tag Name as x |
| Boolean options | all | no-activations, fill-solid, no-title |
| Key-value options | all | start-date 2026-03-15, active-tag Team |
| Series declarations | data charts | series A B C |
| Data rows | bar/line/pie/etc | Label 100 |
| ER columns | er | id int pk |
| Sequence messages | sequence | A -msg-> B |
| Groups/containers | all | [Group Name] |
| Section dividers | sequence | == Phase == |
| Comments | all | // comment |
| Wordcloud data | wordcloud | swordsmanship 95 |
| Slope data rows | slope | Blackbeard 40 4 |
| Slope period directive | slope | period 1715 1725 |
| Venn intersections | venn | sw + nav Sea Raiders |
| Wireframe flags | wireframe | (Submit) primary destructive |
Preferred Metadata Form Per Chart Type
Both same-line metadata (§1.4.1) and indented metadata (§1.4.2) are valid wherever metadata is supported, but the spec names ONE canonical form per chart type so generators have a deterministic default. Deviate from the preferred form when a specific situation calls for the alternative.
| Chart type | Preferred form | Rationale |
|---|---|---|
| sequence | Same-line | Participants are declared once with all metadata; no rich indented-child grammar |
| infra | Indented for node properties (latency-ms: 50, etc.); same-line for tags | Node properties use key: value like standard metadata |
| org | Indented | Org hierarchies are deep and same-line metadata gets long |
| c4 | Indented | Multi-key metadata (description, tech, …) is long |
| er | Same-line | Tables have indented columns; attributes belong on the table declaration line |
| kanban | Same-line | Cards are short; indented slot is for card detail prose |
| sitemap | Same-line | Indented slot is for sub-pages |
| gantt | Same-line | Indented slot is for dependencies (-> Next) |
| boxes-and-lines | Same-line | Indented slot is for edges and descriptions |
| timeline | Same-line | Events are flat; no indented child concept |
| pert | Same-line | Indented slot is for dependencies (-> Next); reserved-key/tag-alias metadata (confidence: low, tag aliases) may also appear there, intermixed with the arrows |
| mindmap | Same-line | Mindmaps are visual trees; indented attributes break scan-readability |
| tech-radar | Same-line | Indented slot is for blips (under quadrants) |
| cycle | Same-line for nodes; indented for descriptions | Description prose is naturally multi-line |
| journey-map | Same-line for steps; indented for annotations (pain:/opportunity:/etc.) | Existing convention |
| pyramid / ring | Same-line for color; indented body for description | Existing trailing-token color shortcut applies |
| raci | Same-line for phase headers and roles; indented for tasks/role-assignments | Chart-type-specific |
| wireframe | Trailing-keyword-list (not key-value metadata) | Flags are positional; indented form is the fallback for collision disambiguation |
| map | Same-line for 1–2 attributes; indented (one per line) for 3+ | §24B’s own form rule; directives are colon-free |
| treemap | Same-line | Size is a bare trailing number; heat: and tag keys ride the node line |
| swimlane | Same-line | Tag metadata rides node lines/references; indented slot is the lane’s nodes and edges |
| event-line | Same-line | Tag values trail the event title; indented slot is description prose |
| version-control | Same-line | id:/tag:/type: ride the commit line; indented slot is commits under a branch |
| block | Same-line, OUTSIDE the bracket | [Backend] l: Service, span: 2; indentation is a container’s sub-grid |
| sketch | Same-line | shape:/at:/tags on the shape line; indented slot is edges |
| family | Same-line | GEDCOM keys on the person line; indented slot is children |
| body | Same-line | Tag value trails the part name; indented slot is notes |
| goal | — (no element metadata) | Values are space-separated key value directives (now, target) |
| bracket | Same-line | Tag values on seed/roster lines; indented slot is commentary |
| countdown | — (no element metadata) | Space-separated key value directives and rule-slot lines only |
| clock | — (no element metadata) | Flat entries — anchor + as <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:
- Metadata assignment —
key: valuein same-line or indented metadata (per §1.4), registry-gated. The general case. - 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). - Tag-value selector in a directive —
hide phase:Planning(boxes-and-lines) andsort tag:Team(gantt/timeline, the back-compat spelling oflane-by): a filter/axis predicate, not assignment. - Role assignment —
Cap: A(raci), colon optional.
The one true carve-out: ER columns (id int pk) are an indented typed-property list like
infra node properties, yet ER is space-separated while infra requires the colon (latency-ms: 50).
This is deliberate — ER follows SQL DDL muscle memory; infra follows metadata convention. ER is
the single exception to memorize.
Colons never appear in:
- Directives and options — space-separated (
start-date 2026-03-15,x-label Low, High,workweek mon-fri) - Tag declarations and chart type declarations
- Series declarations and data rows for simple/data charts (space-delimited — incl. sankey/arc links
Source -> Target valueand 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: Waitingis 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 noLoginpage declared anywhere) →Arrow target "Login" not found(sitemap) - ❌
[Backend] -> [Frontend]with no[Frontend]group declared →Group '[Frontend]' not found(boxes-and-lines) - ❌
lp→-> celebratewithcelebratenever declared →Unknown activity 'celebrate'(pert) - ✅ Declare the target first, then reference it; OR use a chart-type-specific inline forward declaration that includes the required schema (pert durations, etc.).
26.2 Don’t Declare Twice — Combine Metadata + Edges
When a node has both metadata and outgoing edges, put same-line metadata on the declaration line and indent the edges below it. Splitting into two declarations triggers a Duplicate node warning.
// ❌ Duplicate-node warning
API description: Main gateway
…
API
-routes-> UserService
// ✅ Combined
API description: Main gateway
-routes-> UserService
26.3 Chart-Type Scope of Universal-Looking Features
Some constructs look universal but are scoped to specific chart types. Don’t transplant them across charts.
| Construct | Scope |
|---|---|
Bare collapsed flag (legacy collapsed: true) | see §1.8’s authoritative list |
| Same-line / indented metadata on element declarations | all chart types except flowchart and data charts (state takes tag-value keys only, decision #48); see §1.4 reserved-key registry |
| Trailing-keyword flag list | wireframe only (§18.5) |
progress: <N> key | gantt only (§13.8) |
score: <N> + emotion: <Word> keys | journey-map only (§22) |
description: <text> shorthand for layers | pyramid, ring (§23, §24) |
milestone <name> keyword | removed — use <name> 0 (§13A.6) |
columns | Two different constructs sharing a word — heatmap: an indented column-label block (§15 Rule B); block: a columns N grid-count directive (§30.2). Don’t conflate them |
tag: <label> commit key | version-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) withoutis a TYPE— use the typed form - The
|operator as metadata delimiter — removed in 0.18.0; itsE_PIPE_OPERATOR_REMOVEDguard 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:
- Every edge target appears as a declaration on a prior line (or as an inline forward declaration with required schema).
- No entity is declared more than once with conflicting metadata.
- Metadata uses the canonical form per §1.4 — same-line
key: value, ...after the name region, or indentedkey: valuefor reserved keys. No|delimiter anywhere except wireframe dropdowns, in-arrow label characters, and quoted name characters. - Wireframe flags are written as space-separated lowercase trailing keywords from the closed 16-flag enum, or indented one level under the element.
- Journey-map steps use
score: N, emotion: Word; gantt tasks useprogress: N; pyramid/ring layers usedescription: <text>(quote when the value contains commas). - Groups start folded via a bare trailing
collapsedflag (§1.8; legacycollapsed: trueaccepted). - Quoted names don’t carry
as <alias>on the same line. - Sequence participants with alias or quoted names use
is a <type>. - No
milestonekeyword in pert — use<name> 0. - 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 anE_SWIMLANE_UNKNOWN_NODEerror, 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). ALane.Nodequalifier 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;laneblocks indent under it (3-deep: phase ▸ lane ▸ nodes). The[...]-heading-over-children idiom matches flowchart[Group]/ journey-map[Phase]; a bracketed leaf is still a process node.- Phase membership can only push a node right (
finalRank = max(baseRank, phaseFloor[phase])), never pull it left of its column; a backward edge across phases becomes a routed back-edge.
27.5 Node Tokens (v1, closed vocabulary)
| Token | Shape / meaning |
|---|---|
Submit Claim | task (bare) |
<Validate> | exclusive (XOR) gateway |
<+ Fork> | parallel (AND) gateway |
(Start) | terminal (neutral) |
(!Rejected) | error terminal (! prefix → red) |
(Paid) success | typed terminal — trailing word success (→ green) or terminate |
[[Inspect Property]] | subprocess (double border) |
Delimiters are optional sugar in a flow reference — Validate ≡ <Validate> — but echoing them keeps the source self-documenting. A terminal is implied by position: a node with no inbound edge is the start, no outbound is the end; (…) only forces the event circle.
27.6 Edges (inline, inside lane blocks)
In-arrow labels per §1.10 — A -invalid-> B, never a trailing word. Chains inline (A -> B -> C). When a source has several branches, write the source on its own line and indent the arrow lines beneath it; every indented arrow fans out from that header (not from the previous line’s tail). A back-edge to an earlier-ranked node draws a routed loop (dashed). A cross-lane edge is authored in the source lane; its target resolves by name (§27.4). Same-line tag metadata (Maroon r: high, §1.4.1) may ride on either a node’s declaration or a reference — on a reference it applies to the resolved node.
27.7 Color Cascade
First match wins: active tag value (r: high → the tag’s color) → event / symbol type ((!x)→red, (x) success→green, gateways neutral) → lane shade. A tag group is an optional second dimension that deliberately overrides the lane fill.
27.8 Reserved Seams (fast-follow — rejected, not silently dropped)
The following each emit an E_SWIMLANE_UNSUPPORTED diagnostic in v1, so promoting one later is one glyph + one key:
| Token | Diagnostic |
|---|---|
<o …> inclusive gateway | inclusive gateway is fast-follow |
<* …> event-based gateway | event-based gateway is fast-follow |
timer: … (and message: / signal:) | timer/message/signal events are fast-follow |
note: … | notes are deferred past v1 |
data: … | data objects are deferred past v1 |
A ~> B (message flow) | message flow is fast-follow |
27.9 Diagnostics
| Code | When |
|---|---|
E_SWIMLANE_DUPLICATE_NODE | a lane declares the same node name on two bare lines |
E_SWIMLANE_UNKNOWN_NODE | an edge references an undeclared bare task (only delimited endpoints auto-create), or a Lane.Node qualifier resolves nowhere |
E_SWIMLANE_AMBIGUOUS_NODE | warning — a bare reference matches nodes in more than one lane; qualify it Lane.Node (resolved to the first match otherwise) |
E_SWIMLANE_UNKNOWN_LANE | a node appears outside any lane block |
E_SWIMLANE_NO_LANES | a [Phase] is declared but no lanes exist |
E_SWIMLANE_UNSUPPORTED | a fast-follow token (§27.8) |
27.10 Complete Example
swimlane Weekly Publishing
direction-lr
lane Writer gray
Draft Post -> Review
Revise -> Review
lane Editor blue
<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:MMor… HH:MM:SS), plus BCE/BC/CE/AD year suffixes (753 BCE). The date renders as a muted subtitle inside the event’s card, directly adjacent to the title on the side away from the spine — so the date travels with its event instead of sitting on the axis (which produced confusing opposite-side captions and diagonal connectors in dense runs). It is formatted for reading (2008-09-02→Sep 2, 2008;753 BCE→753 BCE), matchingtimeline’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). Ifscaleis on (default) but some/all dates are absent or unparseable, the renderer falls back to even spacing rather than erroring.TBD(future events): aTBDdate line-prefix (case-insensitive —TBD First Crewed Landing) marks a not-yet-scheduled future event. Its caption readsTBDand it draws a hollow dot + a faded leader (inno-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: aTBDwith a dated event after it is interpolated into that gap; a trailingTBD(nothing dated after it) parks just past the latest date and the spine trails off dashed — the open horizon. MultipleTBDs 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,TBDevents 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):
| Directive | Effect |
|---|---|
no-scale | space events evenly instead of by date (date subtitles still print; the spine ruler is omitted) |
side above / side below | place all cards on one side instead of alternating; side alternate is the default (omit it). Keyword+value form |
no-box | card-less style for slides — a tag-colored label + muted date on a soft tag-tinted “shelf” (no full box/border), with the description below. The shelf’s colored spine-side edge is the leader’s landing pad, keeping the dot→block link solid in dense charts; the title sits nearest the spine |
no-legend | hide the tag legend |
now / now <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 — underside above/side belowit 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 barecollapsedflag (§1.8; legacycollapsed: trueaccepted) and/or a named color ([The 1960s] collapsed blue). Named colors only; a hex value is rejected (§1.5) and leaves the era neutral. collapsedfolds 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 explicitno-scaleor 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
nowis 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 thenowcaption (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-stopstop-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-scalethere is no date position to anchor it — the directive is ignored and emits anE_EVENT_LINE_UNSUPPORTEDwarning. - Position is interpolated over the placed dated events, so it honors the broken axis (collapsed-era capsules, per-run scaling) for free. A
nowearlier than the first event clamps to it. Anowlater than the last dated event clamps to that last dot — unless the spine trails off into a dashed open horizon (a trailingTBD, §28.4), in which case the rule rides out onto that tail (“now, in the unscheduled-future era”), anchored at the nearest trailing-TBDdot. - TBD interaction:
nowinterpolates over real dated events only —TBDpositions are inferred (§28.4), so they never anchor the rule. A bracketedTBD(dated event after it) is simply interpolated across; a trailingTBDopens the horizon the rule can ride onto (above). - Last one wins if
nowis repeated. Anowvalue that isn’t a valid date emitsE_EVENT_LINE_BAD_DATEand no rule is drawn.
28.7 Reserved Seams (fast-follow — rejected, not silently dropped)
Each emits E_EVENT_LINE_UNSUPPORTED in v1:
| Token | Diagnostic |
|---|---|
direction-tb | vertical orientation is a fast-follow; direction-lr (boolean, §1.9) is the only supported mode — horizontal — and the default. direction-tb is a reserved seam and emits the unsupported-direction diagnostic. Key+value direction LR accepted legacy |
section <Name> | superseded by the [Name] era delimiter (§28.6a); the diagnostic points there |
a trailing ^ / v on an event | per-event side override is fast-follow |
28.8 Diagnostics
| Code | When |
|---|---|
E_EVENT_LINE_NO_EVENTS | declaration with no event lines |
E_EVENT_LINE_BAD_DATE | a date-looking prefix that isn’t valid ISO |
E_EVENT_LINE_UNSUPPORTED | a fast-follow token (§28.7) |
E_EVENT_LINE_ERA_DATE_ORDER | warning — an event is dated outside its era’s chronological run (dated after a later era begins, or before an earlier era ends), which makes the era brackets overlap (§28.6a). Only checked when the date scale is active. |
28.9 Complete Example
event-line Super Bowl Halftime Shows
tag Genre as g
Pop blue
Rock red
R&B teal
2011-02-06 XLV g: Pop
**The Black Eyed Peas**, Usher, and Slash. High-octane EDM staging.
2012-02-05 XLVI g: Pop
**Madonna** with LMFAO, Nicki Minaj, M.I.A, and Cee Lo Green.
- Greek-temple set, gladiators
- Marching-band finale
2013-02-03 XLVII g: R&B
**Beyoncé** reunites Destiny's Child under a stadium light rig.
29. Version-Control Diagrams
A version-control graph: a commit DAG drawn as parallel branch lanes (the git / Mercurial / SVN branch-and-merge picture). Deliberately VCS-agnostic — not git-graph — because commits, branches, and merges are universal. The grammar is keyword-less: a bare top-level line is a branch, a bare indented line is a commit (the org / event-line indentation idiom). Only merge and cherry-pick are required verbs. At parity with Mermaid gitGraph and beyond it (HEAD / remote-tracking / ahead-behind, the rebase/reset/revert/squash operations, step notes). Visual language is “metro map” — thick rounded colored lanes, big dots, ref-pill badges, diagonal commit labels.
29.1 Chart Type Declaration
version-control [Title]
Space-separated, no colon; title optional. Detection is by the first-line token (version-control).
29.2 Branches (bare top-level line — no branch keyword)
A bare line at indent 0 (that isn’t a directive or an operation verb §29.7/§29.8) is a branch:
develop from main
- Name — the leading token (slashes allowed:
feature/login). The first top-level branch (or any commit that appears before one) is the trunk. from <parent>— sets the branch point: the parent branch’s current tip, or a specific commit (by message orid:). Omitted on the trunk.- Re-naming an existing branch resumes it (appends following commits) — this is the only mechanism for what git calls
checkout/switch; there is no checkout keyword. Re-opening a branch later in the file places its new commits later in time (temporal interleave). - Trailing color token —
hotfix red(§1.5 named colors only; the venn/wardley trailing-token idiom). Omitted → auto-assigned by lane order. The branch name is labeled in the gutter, aligned to its lane (no legend). order: N— optional same-line metadata fixing the branch’s lane order (default = declaration order).
29.3 Commits (bare indented line — no commit keyword)
A bare indented line is a commit; its text is the message, in source order on the active branch:
develop from main
Set up CI
Add test suite tag: ci-green
- No
commitkeyword. It survives only as the opt-in form for an empty/dotless commit (commitalone → 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). Noid:→ no SHA is drawn, so the source maps 1:1 to the render (no invented hashes). Anauto-idmode 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 amerge). Version-control charts do NOT take §1.3 tag groups —tag:here is a git ref pill, a different concept (§26.3).type: normal | highlight | reverse—normal= filled dot (default);highlight= filled square;reverse= hollow crossed dot (a backed-out commit).
29.4 Merge and Cherry-Pick (the two required verbs)
Indented, inside the active (target) branch:
develop
merge feature/login tag: v1.0.0
cherry-pick e90d5f3
merge <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):
| Directive | Effect |
|---|---|
direction-lr | horizontal lanes, newest right (default, the iconic look); boolean per §1.9 (key+value direction LR accepted legacy) |
direction-tb | column lanes, newest down (the git log view); boolean per §1.9 (key+value direction TB accepted legacy) |
no-labels | hide commit messages (Mermaid showCommitLabel: false) |
no-lanes | hide branch lane lines (Mermaid showBranches: false) |
no-head | suppress the auto HEAD marker (§29.7) |
29.7 HEAD, Remote-Tracking & Ahead/Behind (one verb: ref)
ref origin/main at Add README
ref HEAD at Login form
ref <name> at <commit-ref> [remote]— drops a labeled pointer pill at an existing commit (by message orid:). One mechanism covers remote-tracking branches, extra tags, and detached HEAD.- Remote-tracking — a ref whose name is
origin/…/upstream/…(or carries the trailingremoteflag) renders ghosted (dashed pill). When a local branchXand aorigin/Xref 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-headsuppresses 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: reversestyling, messageRevert <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 orid:, resolved to the most recent match; duplicate messages require an explicitid:(elseE_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 explicitcommitprefix or quote it.
29.11 Reserved Seams (fast-follow — rejected, not silently dropped)
Each emits E_VERSION_CONTROL_UNSUPPORTED in v1:
| Token | Diagnostic |
|---|---|
auto-id | hash + display every commit (the dense git log look) is fast-follow |
stash / conflict / PR-overlay / diffstat | tier-3 git features are fast-follow |
a stateful checkout / switch keyword | the linear Mermaid-mirror mode is fast-follow |
Repo import (git log → version-control source) is a tooling fast-follow, not syntax.
29.12 Diagnostics
| Code | When |
|---|---|
E_VERSION_CONTROL_NO_COMMITS | declaration with no commits |
E_VERSION_CONTROL_UNKNOWN_BRANCH | from/merge/rebase/reset names a branch with no tip |
E_VERSION_CONTROL_AMBIGUOUS_REF | a commit ref matches more than one commit (require id:) |
E_VERSION_CONTROL_CYCLE | a cycle in the commit DAG (self-merge / bad from) |
E_VERSION_CONTROL_UNSUPPORTED | a fast-follow token (§29.11) |
29.13 Complete Example
version-control Feature Branch Workflow
main
Initial commit
Add README
develop from main
Set up CI
Add test suite
feature/login from develop
Login form
Form validation
develop
merge feature/login
Address review notes
main
merge develop tag: v1.0.0
Hotfix typo type: highlight
30. Block Diagrams
An author-controlled grid of rectangular blocks — for diagrams where the 2-D arrangement itself is the meaning (system block diagrams, hardware/signal chains, layered stacks, deployment topologies). The deliberate exception to dgmo’s auto-layout default: you place the blocks (columns, spans, nesting); the renderer still derives every pixel. No net-new grammar — every construct reuses an existing idiom ([brackets] + tail metadata from boxes-and-lines §14, indentation nesting + tag cascade from treemap §24C). Distinct from boxes-and-lines (§14, topology auto-laid-out) and flowchart (§5, decision flow): pick block when you want a specific grid, not a connected graph.
30.1 Chart Type Declaration
block [Title]
Space-separated, no colon; title optional. Detection is by the first-line token (block).
30.2 The Grid
One source line = one row; a [Label] is a block, filling the row left-to-right.
- Columns are inferred from the widest row —
columns Nis 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 thetagblock, 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: 2widens a block across columns (metadata, §1.4.1, OUTSIDE the bracket). A span larger than the column count clamps to it.
30.3 Containers & Collapse
A block becomes a container by indenting a sub-grid under it (the org / treemap indentation idiom). Sibling containers stack vertically — each needs its own indented body — so the horizontal grid is for the leaf blocks inside a container; side-by-side containers are a reserved seam.
A bare collapsed flag on a container line starts it folded: it renders as a header band with the shared collapse-bar (the org §7 / sitemap / mindmap signal — no count). Collapse/expand is interactive in the desktop app (interactive-vs-export split, like map §24B / treemap §24C); static export renders the authored state.
30.4 Tags
Metadata goes OUTSIDE the bracket — [Backend] l: Service — never inside (the boxes-and-lines group precedent, §14.4; this keeps a colon inside a label, [API: v2], as label text). A group tag cascades to its children; an individual box tag overrides the cascade. Declare the tag group before content; named palette colors only (§1.3, §1.5).
30.5 Node Metadata & Directives
| Key / token | Effect |
|---|---|
span: N | Column span (integer ≥ 1; clamps to the column count) |
collapsed | Bare flag — start this container folded |
<tag>: V | Tag value via the group’s alias or name (cascades / overrides) |
columns N | Directive — override a grid’s inferred column count |
no-legend | Directive — hide the tag legend |
30.6 Diagnostics
| Code | When |
|---|---|
E_TAG_DECLARED_AFTER_CONTENT | a tag group appears after the first block row |
Over-indented orphan lines and unclosed [ brackets warn and are skipped; an empty diagram (no blocks) errors.
31. Sketch Diagrams
A GUI-first constrained canvas: free placement of uniformly-sized shapes on a snap grid, arrows between them, meaning through tags. The markup is generated by the canvas editor — it looks like dgmo and diffs like dgmo, but human writability is a non-goal (the parser still tolerates hand-authored files; see the leniency rules below). The renderer owns every pixel of styling; the author owns only placement, connection, naming, and tags. Pick sketch when the drawing IS the content (“get it out of your head”); pick boxes-and-lines (§14) when you want topology auto-laid-out.
31.1 Chart Type Declaration
sketch [Title]
Space-separated, no colon; title optional. Detection is by the first-line token (sketch).
31.2 Shapes
One top-level source line = one shape. Shapes are bare names with same-line key: value metadata (the infra §4.2/§4.3 idiom — multi-word names parse; quotes for reserved characters per §2.2a):
Spyglass Feed shape: database, at: 0 0, crew: Deck
Captain's Console as con at: 2 0, crew: Deck
- Org-style card — every shape renders as a uniform rounded card: a header with the name (in the tag color), a rule, and one metadata row per tag the shape carries (
Group: value). A shape with no tags centers its name. shape:— closed set, full words:database,queue,person,document,note. Rectangle is the default and is never written. Type appears as a small badge in the card header (not a silhouette).noteis 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. (cloudwas 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 withI_NAME_MERGED. - Quoted names (reserved characters, §2.2a) cannot take
as; they may still be edge targets by exact quoted-label reference when unambiguous. Apostrophes are legal in bare names (Captain's Console).
31.3 Coordinates — at: C R
The only coordinate construct. Integers, counting half-slot steps (a slot = one footprint + the mandatory gap between shapes; the half-step permits brick-like offsets). Origin-normalized on emit: the content bounding box starts at 0 0, and box children are relative to their box’s origin — so panning the whole sketch, or moving a box, is a tiny or zero diff.
at:is optional. Un-positioned shapes auto-place via a deliberately dumb flow layout — appended in rows below existing content, reading order, nothing smarter. Lazy hand-authoring is legal; the GUI writes real coordinates on next save.- Overlap is unrepresentable via the GUI (enforced spacing + make-room). Hand-authored overlap is legal input: it auto-resolves at render with
W_SKETCH_OVERLAP_RESOLVED. Never reject, never render broken.
31.4 Lines
Edges are indented under their source shape (§1.7), targeting an alias — or a bare/quoted label when unambiguous (infra §4.4 precedent; ambiguous → E_SKETCH_AMBIGUOUS_TARGET):
Spyglass Feed shape: database, at: 0 0
-sightings-> con
~haul~> dvy crew: Hold
-- ledger
Six grammar forms — three head-configs × solid/dashed:
| Form | Meaning |
|---|---|
-label-> | one head, solid |
<-label-> | both heads, solid |
-label- | no heads, solid |
~label~> / <~label~> / ~label~ | dashed twins |
- Unlabeled headless lines are written
--(solid) and~~(dashed) — never a bare-/~. Unlabeled arrow forms (->,<->,~>,<~>) are legal. - Dashed means “secondary” in sketch — a softer stroke, NOT async. This deliberately diverges from infra’s
~>(async); generators must not carry the async reading over. - There is no source-head-only form — left-pointing arrows are banned language-wide; the GUI normalizes “head at source only” by swapping (
A <- B→B -> Aconvention) 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
collapsedflag (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 / token | Applies to | Effect |
|---|---|---|
shape: <kind> | shape | Morph from the default rectangle (closed shape set; type shows as a header badge) |
at: C R | shape, box | Half-slot position (optional; omit → flow auto-place) |
as <alias> | shape, box | Reference handle for edges (§2A) |
collapsed | box | Bare flag — start folded to a virtual node card |
<tag>: V | shape, box, edge tail | Tag value (cascades from box; individual overrides; colors edge lines) |
no-legend | directive | Hide the tag legend |
31.8 Diagnostics
| Code | When |
|---|---|
E_SKETCH_NESTED_BOX | a [Box] inside a [Box] — inner box degrades to flat children |
E_SKETCH_AMBIGUOUS_TARGET | an edge targets a bare label shared by multiple shapes |
W_SKETCH_UNKNOWN_SHAPE | unknown shape: value — falls back to rectangle |
W_SKETCH_OVERLAP_RESOLVED | hand-authored overlapping at: positions were auto-resolved |
31.9 Generator Guidance
Sketch is a GUI-authored format. If generating it anyway: use integer at: on the half-slot lattice with shapes ≥ 2 half-slots apart on at least one axis (or omit at: entirely and let flow placement work); alias any shape an edge references; never invent size:, colors, or shapes outside the closed set; ~ means secondary, not async. Malformed coordinates never break a render — they resolve with warnings — but tidy input diffs better.
32. Family Diagrams
A genealogy / family tree: people, the couples (unions) they form, and the children those unions produce. The whole grammar is two line shapes — a person line (a bare name, optionally with metadata) and a union line (NameA + NameB) — plus indentation for parent→child descent (§1.6). There are no arrow edges: kinship is expressed by the + join and by nesting, the way org charts (§7) express reporting lines by nesting alone. Pick family for ancestry, lineage, and dynasty diagrams; pick org (§7) for pure single-parent hierarchies with no unions.
32.1 Declaration
family [Title]
Space-separated, no colon; title optional. Detection is by the first-line token (family).
family House of Blackwater
32.2 People
A person is a bare name on its own line, optionally carrying same-line metadata (§1.4.1). Names may be quoted (§2.2), as-aliased (§2A), and carry a bare trailing color token per §1.5.
Anne b: 1665, d: 1714, sex: f
"Long John Silver" as ljs sex: m
Redbeard purple
- Person metadata lives on a standalone person declaration only — the first place the name appears on its own line. Restating the name later (e.g. as one side of a union) re-references the same person; it does not accept a fresh metadata set. Declare the person standalone (with all their metadata) before or after the union that uses them.
b/drender 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-sidekey: valuemetadata is NOT supported — a side cannot carry its ownb:/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:
- Union metadata cut first (§1.4.1). The trailing
m: <year>is peeled off the end of the line. +split second, within the remaining name region only. The name region left after the metadata cut is split on+into the two sides.
Because the metadata is removed before the split, a + living inside a quoted name ("Anne + Jack" is one person) or inside a metadata value is protected — it never triggers a couple split. Each side is then run through the universal name pipeline (quote → color → alias) exactly as §1.5’s cut order specifies.
32.4 Children (Indentation = Descent)
Lines indented under a union line are that couple’s children. Children are themselves person lines (§32.2) — so a child can carry sex:, b:, a color, an alias, everything a top-level person can:
Anne + Jack m: 1701
Mary b: 1702, sex: f
Tom b: 1704, sex: m
A child can later head their own union line, extending the tree another generation. Reusing the child’s name as a union side re-references the same person:
Anne + Jack m: 1701
Mary b: 1702, sex: f
Tom b: 1704, sex: m
Mary + Silas m: 1725 // Mary from above, now a parent
Grace b: 1726, sex: f
Single parent — a lone person line (no +) with indented children is a single-parent family. No extra syntax:
Bess sex: f
Nan b: 1690, sex: f
Will b: 1692, sex: m
32.5 Adoption
A child line ending in a bare adopted token marks an adopted child; the edge from parents to that child renders dashed:
Anne + Jack m: 1701
Mary b: 1702, sex: f
Ned b: 1705, sex: m adopted
A literal person actually named “Adopted” must be quoted ("Adopted") so the token is read as a name rather than the flag.
32.6 Metadata Keys (GEDCOM-Flavored, Fixed Set)
The reserved metadata keys are a closed, GEDCOM-flavored set. Colons are required (key: value, §1.4). Unknown keys emit a warning (not an error) and are otherwise ignored.
| Level | Key | Value | Renders as |
|---|---|---|---|
| person | sex | m | f | drives the default card color (§32.7) |
| person | b | birth year | left end of the year range |
| person | d | death year | right end of the year range |
| person | bp | birth place | detail line |
| person | dp | death place | detail line |
| person | occupation | free text | detail line |
| person | military | free text | detail line |
| person | education | free text | detail line |
| person | religion | free text | detail line |
| person | burial | free text | detail line |
| union | m | marriage year | marriage-bar label |
b and d together render as a year range under the name (e.g. 1665–1714); either alone renders as a partial range (1665– / –1714).
32.7 Sex → Color (Legend Is the Guaranteed Channel)
When a person carries sex:, their card takes a default color (a named-palette color at 25% tint, §1.5):
sex: | Default color |
|---|---|
m | blue |
f | purple |
| unset | gray |
An explicit tag (§1.3) or a bare trailing color token on the person overrides the sex-derived color.
Caveat — treat the legend, not the hue, as the sex channel. Blue and purple sit close in hue and desaturate further under the 25% tint, so they separate weakly under deuteranopia. Where color-vision accessibility matters, distinguish the sexes with tags (whose legend entries carry text labels) rather than relying on the fill. The legend is the guaranteed, readable sex channel; the fill is a convenience.
32.8 Remarriage
A person who appears in two union lines renders as one card with two marriage bars — reuse the same name in a second union to express a remarriage:
Redbeard sex: m
Redbeard + Anne m: 1701
Mary b: 1702, sex: f
Redbeard + Grace m: 1716 // same Redbeard — one card, second marriage bar
Ned b: 1717, sex: m
32.9 Same-Sex Unions
Same-sex couples need zero extra syntax — both sides are just names, and each partner’s sex: (declared standalone) is honored for color:
Anne sex: f
Bess sex: f
Anne + Bess m: 1720
Nan b: 1721, sex: f adopted
32.10 Tags & Colors
Tag declarations (§1.3) and trailing-color suffixes (§1.5) work as the universal constructs describe — nothing family-specific. Declare a tag group before content, apply it via <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
| Code | Severity | Fires when |
|---|---|---|
W_FAMILY_UNKNOWN_KEY | warning | A metadata key outside the §32.6 fixed set (and not a declared tag alias) is used — the pair is ignored. |
W_FAMILY_SIDE_METADATA | warning | A key: value pair appears on one side of a union (per-side metadata unsupported) — declare the partner standalone (§32.3). |
W_FAMILY_HIGHLIGHT_UNKNOWN | warning | A highlight <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:
| Facet | Tokens | Default |
|---|---|---|
| form | muscle | skin | muscle |
| sex | male | female | male |
| view | front | back | front |
body Back & Legs
muscle
female
back
musclerenders the segmented muscle map;skinrenders the same figure as a plain silhouette (head + hair, no segmentation).skeletalis reserved.- View is a set. Naming both
frontandbackrenders 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_PARTand 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 as0with 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.
| Flag | Face |
|---|---|
| (none) | Progress bar — horizontal rounded fill (default). |
thermometer | Vertical tube + bulb; the column rises with progress. |
gauge | Semicircular dial with a needle and value arc. |
All three faces consume the same now/target pair — the flag only changes the drawing.
34.4 Over-target & edge cases
- Over target (
now > target): the fill clamps at 100% but the % label stays truthful (e.g.120%); the gauge needle pins at max. - Negative
now: fill clamps to 0%, label stays honest. - Single value only — this type has no children/rows. Indented content is ignored with a warning, except an indented
noteblock 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:
- An explicit trailing color token on the title line (
goal Marathon Fund ($) green, §1.5) always wins. - Otherwise the auto band color (needs a
target). no-auto-colordisables 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
| Directive | Effect |
|---|---|
no-percent | Hide the % label. |
no-value | Hide the raw now / target label. |
fill-solid | Full-saturation fill instead of the 25% tint (fill-outline hollows the meter — color on the rim, §1.9). |
no-title | Hide the banner title. |
no-notes | Suppress the note block even if authored (singular no-note accepted as a silent alias). |
no-auto-color | Disable 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 pairs3v6/4v5in 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 → genericRound 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]+ indentedValue color, exactly as block/kanban §30/§11). A competitor carries a tag value via §1.4 same-line metadata on itsseedline (seed 1 Blue Jays tk: MLB Ballpark) or, in casual mode, a standalone roster line (Blue Jays tk: MLB Ballpark— a name + metadata, nobeats/vs). The tag colors that competitor’s outline; a legend renders andno-legendhides 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’sradial).double-elimis reserved: it parses but emits a “not yet supported” diagnostic and renders the winners’ side only. A losers’-bracket grammar is deferred (the positionalA beats Bform 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/fromdate follows the liberal grammar + year ladder in §2B Universal Date Handling;nowand 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 literalnowresolves 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 thetzzone 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. Withtzset thetarget(bare date, offset-free datetime, or recurringat) 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. Atargetcarrying its own ISO offset is already absolute, sotzdoesn’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! 🍕
| Slot | Values | Notes |
|---|---|---|
every | year | month | week | N days|weeks|months | the cadence |
on | Aug 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 |
at | 18:00 — also accepts am/pm forms (6pm, 6:30pm), normalized to 24h (decision #48) | default midnight |
from | 2026-07-03 — anchor date | interval 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:00is 18:00 on the viewer’s clock (a shared cross-tz page shows each viewer their own local time). Addtz <IANA>(§36.2) to pinat/onto 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
everyline is cursor-position aware: completion offers the cadence afterevery, month names afterevery year on, ordinal+weekday afterevery month on, a weekday afterevery week on, theon/at/fromconnectors after a cadence, and time/date hints afterat/from; the sub-keywords and closed instant vocab highlight distinctly (context-gated to countdown, soon/at/fromnever 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 whereNth→ the ordinal word (7th) andN→ 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 toNth <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.dayswhole days (“400 days”) for people who want the raw number;fullticksNd HH:MM:SS;clocktotalHH:MM:SS(may exceed 24h);weeks;wordsloose English.full/clockbake the day count and only tick sub-day on live surfaces.round <up|down|nearest>— ceil (default) / floor / round for day & week modes.upkeeps a target later today at “1”.fields <d,h,m,s>— whichfull-mode segments show (dropsfor 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 target | Band |
|---|---|
| > 3 years | years-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 years | year blocks — one row of 12 month rectangles per year |
| ~3–12 months | month rectangles — one rectangle per month (the year-block idiom, un-grouped), muted/role fill, only the now- and target-months dated |
| ~3 weeks – 3 months | month 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 days | day-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 accent — red 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 green — time 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-soliddirective (§1) opts into full-saturation chips with white text;fill-outlinerenders 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:SSclock 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 go→agoand 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).
36.8 Live rendering, footer & the “as of” stamp
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 acceptsHH:MMand 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;daysis 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 unlessno-sunis 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 olddirection <tb|lr>key+value and thecolumnsalias are accepted legacy.color-by <place|work|daylight|time|none>— which dimension colors the zones (see §37.5). Default isplace;color-by nonegoes 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:
- Starts
UTCorGMT→ a fixed offset (§37.3a).UTC,UTC+1,UTC-7,UTC+5:30,GMT+2.GMTis an exact synonym ofUTC. - Contains a
/→ an explicit IANA id (Europe/London,America/Argentina/Buenos_Aires). The escape hatch for ambiguous or obscure places. - 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, orLondon purplewith 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 /
UTCescape 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 theanalogflag is set. - Middle — the alias, the place, and the current UTC offset (e.g.
UTC−7), resolved live fromIntlso 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/workweekwindow, 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.work— semantic, availability: green while a zone is inside itshours/workweekwindow, amber in the last hour before it closes, grey when off. Needshoursto be set — without it the board stays neutral.daylight— semantic, light: warm where the sun is currently up, cool where it is down, using the same sunrise/sundown solar math as thesunline (§37.2). Renamed fromdayto avoid clashing with the (now-legacy)daysalias ofworkweek.time— semantic, 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.)