@diagrammo/dgmo 0.32.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.cursorrules +7 -2
  2. package/.github/copilot-instructions.md +7 -2
  3. package/.windsurfrules +7 -2
  4. package/SKILL.md +7 -2
  5. package/dist/advanced.cjs +1434 -443
  6. package/dist/advanced.d.cts +41 -1
  7. package/dist/advanced.d.ts +41 -1
  8. package/dist/advanced.js +1433 -443
  9. package/dist/auto.cjs +1242 -318
  10. package/dist/auto.js +91 -91
  11. package/dist/auto.mjs +1242 -318
  12. package/dist/cli.cjs +138 -136
  13. package/dist/index.cjs +1308 -317
  14. package/dist/index.d.cts +35 -1
  15. package/dist/index.d.ts +35 -1
  16. package/dist/index.js +1307 -317
  17. package/docs/ai-integration.md +7 -2
  18. package/docs/language-reference.md +39 -8
  19. package/package.json +2 -2
  20. package/src/advanced.ts +6 -1
  21. package/src/boxes-and-lines/label-placement.ts +339 -0
  22. package/src/boxes-and-lines/layout-grouped.ts +909 -0
  23. package/src/boxes-and-lines/layout-search.ts +34 -3
  24. package/src/boxes-and-lines/layout.ts +32 -7
  25. package/src/boxes-and-lines/parser.ts +3 -2
  26. package/src/boxes-and-lines/renderer.ts +67 -139
  27. package/src/c4/layout.ts +2 -1
  28. package/src/c4/parser.ts +3 -2
  29. package/src/c4/renderer.ts +3 -2
  30. package/src/cli.ts +68 -0
  31. package/src/er/parser.ts +2 -1
  32. package/src/er/renderer.ts +2 -1
  33. package/src/gantt/parser.ts +3 -5
  34. package/src/gantt/renderer.ts +7 -6
  35. package/src/index.ts +6 -1
  36. package/src/infra/parser.ts +2 -1
  37. package/src/infra/renderer.ts +4 -3
  38. package/src/journey-map/parser.ts +27 -8
  39. package/src/journey-map/renderer.ts +26 -9
  40. package/src/kanban/parser.ts +4 -3
  41. package/src/kanban/renderer.ts +6 -5
  42. package/src/map/completion.ts +116 -0
  43. package/src/map/layout.ts +4 -3
  44. package/src/map/parser.ts +3 -2
  45. package/src/mindmap/parser.ts +4 -3
  46. package/src/mindmap/renderer.ts +2 -1
  47. package/src/org/layout.ts +3 -2
  48. package/src/org/parser.ts +3 -2
  49. package/src/org/renderer.ts +5 -4
  50. package/src/pert/parser.ts +3 -2
  51. package/src/sequence/renderer.ts +10 -12
  52. package/src/sequence/tag-resolution.ts +2 -1
  53. package/src/sitemap/layout.ts +3 -2
  54. package/src/sitemap/parser.ts +36 -2
  55. package/src/sitemap/renderer.ts +12 -6
  56. package/src/timeline/renderer.ts +4 -3
  57. package/src/utils/legend-d3.ts +4 -3
  58. package/src/utils/legend-svg.ts +3 -2
  59. package/src/utils/tag-groups.ts +28 -7
  60. package/src/visualizations/parse.ts +2 -1
@@ -64,6 +64,7 @@ LLMs default to Mermaid / PlantUML habits; DGMO differs. These rules prevent the
64
64
  - **Show-everything is the default.** Every label / value / percent renders by default. Emit `no-name` / `no-value` / `no-percent` / `no-*` ONLY when the user explicitly asks to hide something — never defensively.
65
65
  - **`//` comments only** (never `#`). **Indentation closes blocks** — never `end`.
66
66
  - **Declare before reference.** An edge target must be declared on a prior line; put metadata and edges on/under one declaration to avoid `Duplicate node` warnings.
67
+ - **No reference scaffolding in output.** Emit only DGMO source. This doc is organized with HTML-comment anchors (the `TYPE`, `TIPS`, and `AI-CORE` markers, each wrapped in comment delimiters); never copy any such `<!-- … -->` comment into a diagram. They mark sections of the docs, not DGMO syntax; the parser flags a stray HTML comment as an `Unexpected line` warning. (DGMO's only comment form is `//`.)
67
68
 
68
69
  Two traps in the *other* direction (DGMO wants a colon / a space where you might not expect):
69
70
 
@@ -90,8 +91,12 @@ Billing t: Product
90
91
 
91
92
  Valid markup is the floor, not the goal. A good diagram reads at a glance. Apply these unless the prompt says otherwise:
92
93
 
93
- - **Title it.** Put a short title on the type-declaration line (`flowchart Checkout Flow`, `boxes-and-lines Service Map`) so the diagram is self-describing.
94
- - **Color by grouping, not at random.** When items fall into categories, declare a tag group and assign members so each category shares one color — far clearer than ad-hoc per-node colors. Use the tag-group syntax shown in the per-type section below (declare `tag Team as t`, indent the values — a trailing color is optional, bare values auto-pick a palette color — then assign `Node t: Team`). Only the 11 named palette colors exist.
94
+ <!-- TITLE start -->
95
+ - **Always title it.** Every diagram gets a short title on the type-declaration line (`flowchart Checkout Flow`, `sequence Checkout`, `boxes-and-lines Service Map`) so it is self-describing — never leave the bare type keyword alone. Infer a fitting title from the request when the user gives none.
96
+ <!-- TITLE end -->
97
+ <!-- CATEGORIZE start -->
98
+ - **Categorize and color — by default, not only when obvious.** Before drawing, find the axis that sorts the items into kinds and color by it: layer (frontend / backend / data), role (client / service / datastore), trust zone (internal / external / third-party), status (done / active / blocked), owner/team, or read-path vs write-path. Almost every diagram has such an axis — actively look for one rather than leaving everything monochrome. Express it with a **tag group**, never ad-hoc per-node colors: declare `tag <Axis> as t` (the name is a single word — `tag TrustZone as tz` — or quote a multi-word name: `tag "Trust Zone" as tz`), indent the category values (a trailing color is optional — bare values auto-pick a palette color), set `active-tag <Axis>`, then assign each item `Node t: <Category>` (see the tag-group syntax in the per-type section below). Only the 11 named palette colors exist, and they re-resolve per palette/theme. Color the grouping so the categories — and the boundaries between them — read at a glance. **Buckets, not name tags — never 1:1.** A tag group must have *fewer* values than it has members: each color should bucket two or more items so the palette compresses the diagram into a few meaningful kinds (aim for ~2–4 categories, and keep distinct colors well under half the item count). If you find yourself giving nearly every item its own value, you've enumerated, not categorized — merge the singletons up a level until each color groups at least two (a `cache` and a `database` are both `datastore`; a caller and a gateway are both `client`; two microservices are both `service`). One color per item is the same visual noise as random colors — the grouping only earns its place when a color means "these belong together." Leave items uncolored only when they genuinely form a single undifferentiated kind, or the user asked for no color.
99
+ <!-- CATEGORIZE end -->
95
100
  - **Keep labels short.** A few words per node. Move detail into notes or metadata, never a full sentence inside a label.
96
101
  - **Let the defaults show.** Don't add `no-*` opt-outs unless the user asked to hide or disable something — they strip helpful labels, values, and color.
97
102
  - **Pick a fitting direction.** Top-to-bottom for hierarchies and processes; left-to-right for pipelines and flows (where the type supports a direction).
@@ -60,6 +60,7 @@ LLMs default to Mermaid / PlantUML habits; DGMO differs. These rules prevent the
60
60
  - **Show-everything is the default.** Every label / value / percent renders by default. Emit `no-name` / `no-value` / `no-percent` / `no-*` ONLY when the user explicitly asks to hide something — never defensively.
61
61
  - **`//` comments only** (never `#`). **Indentation closes blocks** — never `end`.
62
62
  - **Declare before reference.** An edge target must be declared on a prior line; put metadata and edges on/under one declaration to avoid `Duplicate node` warnings.
63
+ - **No reference scaffolding in output.** Emit only DGMO source. This doc is organized with HTML-comment anchors (the `TYPE`, `TIPS`, and `AI-CORE` markers, each wrapped in comment delimiters); never copy any such `<!-- … -->` comment into a diagram. They mark sections of the docs, not DGMO syntax; the parser flags a stray HTML comment as an `Unexpected line` warning. (DGMO's only comment form is `//`.)
63
64
 
64
65
  Two traps in the *other* direction (DGMO wants a colon / a space where you might not expect):
65
66
 
@@ -88,8 +89,12 @@ Billing t: Product
88
89
 
89
90
  Valid markup is the floor, not the goal. A good diagram reads at a glance. Apply these unless the prompt says otherwise:
90
91
 
91
- - **Title it.** Put a short title on the type-declaration line (`flowchart Checkout Flow`, `boxes-and-lines Service Map`) so the diagram is self-describing.
92
- - **Color by grouping, not at random.** When items fall into categories, declare a tag group and assign members so each category shares one color — far clearer than ad-hoc per-node colors. Use the tag-group syntax shown in the per-type section below (declare `tag Team as t`, indent the values — a trailing color is optional, bare values auto-pick a palette color — then assign `Node t: Team`). Only the 11 named palette colors exist.
92
+ <!-- TITLE start -->
93
+ - **Always title it.** Every diagram gets a short title on the type-declaration line (`flowchart Checkout Flow`, `sequence Checkout`, `boxes-and-lines Service Map`) so it is self-describing — never leave the bare type keyword alone. Infer a fitting title from the request when the user gives none.
94
+ <!-- TITLE end -->
95
+ <!-- CATEGORIZE start -->
96
+ - **Categorize and color — by default, not only when obvious.** Before drawing, find the axis that sorts the items into kinds and color by it: layer (frontend / backend / data), role (client / service / datastore), trust zone (internal / external / third-party), status (done / active / blocked), owner/team, or read-path vs write-path. Almost every diagram has such an axis — actively look for one rather than leaving everything monochrome. Express it with a **tag group**, never ad-hoc per-node colors: declare `tag <Axis> as t` (the name is a single word — `tag TrustZone as tz` — or quote a multi-word name: `tag "Trust Zone" as tz`), indent the category values (a trailing color is optional — bare values auto-pick a palette color), set `active-tag <Axis>`, then assign each item `Node t: <Category>` (see the tag-group syntax in the per-type section below). Only the 11 named palette colors exist, and they re-resolve per palette/theme. Color the grouping so the categories — and the boundaries between them — read at a glance. **Buckets, not name tags — never 1:1.** A tag group must have *fewer* values than it has members: each color should bucket two or more items so the palette compresses the diagram into a few meaningful kinds (aim for ~2–4 categories, and keep distinct colors well under half the item count). If you find yourself giving nearly every item its own value, you've enumerated, not categorized — merge the singletons up a level until each color groups at least two (a `cache` and a `database` are both `datastore`; a caller and a gateway are both `client`; two microservices are both `service`). One color per item is the same visual noise as random colors — the grouping only earns its place when a color means "these belong together." Leave items uncolored only when they genuinely form a single undifferentiated kind, or the user asked for no color.
97
+ <!-- CATEGORIZE end -->
93
98
  - **Keep labels short.** A few words per node. Move detail into notes or metadata, never a full sentence inside a label.
94
99
  - **Let the defaults show.** Don't add `no-*` opt-outs unless the user asked to hide or disable something — they strip helpful labels, values, and color.
95
100
  - **Pick a fitting direction.** Top-to-bottom for hierarchies and processes; left-to-right for pipelines and flows (where the type supports a direction).
@@ -502,7 +507,7 @@ rarely benefit. Aliases should aid comprehension, not obscure it.
502
507
  <!-- TYPE:sequence -->
503
508
 
504
509
  <!-- TIPS start -->
505
- **Styling tips:** stay terse — participants are created by the messages that name them, so a plain flow needs only the arrow lines (no participant declarations). Skip `is a TYPE`: the glyph is inferred from the name (`User`→actor, `*DB`→database, `Redis`→cache, `Kafka`→queue); add a declaration, a `[Group]`, or `position: N` only when it genuinely aids reading order or grouping, never as boilerplate. Give each participant a short role name; group related exchanges under `== Section ==` dividers rather than a wall of arrows; label a return arrow only when its value matters (unlabeled returns are auto-pruned). Drop a `note` where something subtle happens. When participants fall into trust or role categories, color the participants (not the messages) by category with a tag group using the named palette colors, so the boundary reads at a glance.
510
+ **Styling tips:** stay terse — participants are created by the messages that name them, so a plain flow needs only the arrow lines (no participant declarations). Skip `is a TYPE`: the glyph is inferred from the name (`User`→actor, `*DB`→database, `Redis`→cache, `Kafka`→queue); add a declaration, a `[Group]`, or `position: N` only when it genuinely aids reading order or grouping, never as boilerplate. Give each participant a short role name. Reach for `== Section ==` dividers only when the flow splits into **two or more** distinct phases (e.g. `== Authentication ==` then `== Checkout ==`) — a single divider over one run of arrows just repeats the diagram's subject and adds a redundant band, so a one-phase flow should have no dividers at all. Label a return arrow only when its value matters (unlabeled returns are auto-pruned). Drop a `note` where something subtle happens. Categorize the participants and color them (not the messages) by that category with a tag group using the named palette colors — client vs service vs datastore, trust zones (internal / external), or owning team are almost always present, so default to coloring by one rather than leaving the lifelines monochrome, and the boundaries read at a glance.
506
511
  <!-- TIPS end -->
507
512
 
508
513
  ### 2.1 Participants
@@ -1279,7 +1284,7 @@ Optional label: `--|> Vessel : extends` (colon optional before label)
1279
1284
  <!-- TYPE:kanban -->
1280
1285
 
1281
1286
  <!-- TIPS start -->
1282
- **Styling tips:** name columns for workflow stages (`Todo`, `Doing`, `Done`); keep card titles to a short task phrase; color cards by owner or priority with a tag group; let column length convey WIP instead of annotating counts. If you use a tag group, tag EVERY card — an untagged card currently inherits the first tag value (mislabeling it), so omit the tag group entirely for an uncolored board.
1287
+ **Styling tips:** Color EVERY column — write a lowercase color word right after the closing bracket (`[Done] green`). Don't match on column names; reason about what each column *means* for a card sitting in it, then pick the color whose everyday connotation fits, so the board reads at a glance. The guiding sense: green = good / finished / success, red = bad / stuck / needs attention, yellow = waiting / caution / in review, blue = active work in motion, gray = inert / not yet started / parked. Apply that to whatever columns exist — e.g. a terminal success column reads green even if it's called `Shipped`, `Hired`, or `Live`; a column meaning a card is stuck reads red whether it's `Blocked`, `On Hold`, or `Rejected`; an early holding column reads gray whether it's `Backlog`, `Ideas`, or `Inbox`. When several columns share a sense (two active-work stages), it's fine to keep a couple uncolored or reuse a color rather than force eleven distinct hues. Name columns for workflow stages, keep card titles to a short task phrase, and let column length convey WIP instead of annotating counts. Card color is separate from column color: color cards by owner or priority with a tag group, and if you use one, tag EVERY card — an untagged card currently inherits the first tag value (mislabeling it), so omit the tag group entirely for cards you want left uncolored.
1283
1288
  <!-- TIPS end -->
1284
1289
 
1285
1290
  ### 10.1 Declaration
@@ -1328,7 +1333,7 @@ tag Crew as c
1328
1333
  <!-- TYPE:sitemap -->
1329
1334
 
1330
1335
  <!-- TIPS start -->
1331
- **Styling tips:** Mirror the real navigation: group child pages under their section and keep the tree shallow (2–3 levels), ordering siblings by prominence. Connect pages with indented `-> Target` lines under each parent (a top-level `Home -> Products` renders as one box) and declare every target as its own page.
1336
+ **Styling tips:** Build a real navigation graph, not a flat column of boxes. Three things make a sitemap read well, and a good one uses all three. (1) **Cluster pages into `[Area]` group sections** when the site has distinct areas — e.g. `[Shop]`, `[Account]`, `[Admin]` — indenting each page under its bracket header, and leave only the true entry page (`Home` / `Landing`) at the top level. (2) **Show how a visitor moves** with an indented labeled arrow under the source page: `-submit-> Home`, `-checkout-> Payment`, `-back-> Dashboard`, where the label is the action or link text. Every arrow target must be declared as its own page; an arrow can point at a whole area with `-> [Admin]`. If pages share no links you've drawn a list, not a sitemap — connect them. (3) **Classify pages with a colored tag group**, so the legend organizes the map by meaning rather than by position. Declare a facet with colored values and assign it to every page in same-line metadata — access level works well: `tag Access` with `Public green`, `Member blue`, `Admin red`, then `Pricing Access: Public`; or a functional facet (`Browse`, `Purchase`, `Admin`). A second tag group for page kind (`Landing purple`, `Form orange`, `Content cyan`) adds a second color dimension. Reason about which facet actually distinguishes this site's pages rather than reaching for a fixed list. Keep the tree shallow and order siblings by prominence.
1332
1337
  <!-- TIPS end -->
1333
1338
 
1334
1339
  ### 11.1 Declaration
@@ -1707,7 +1712,7 @@ See spec §13A for full date-anchoring semantics, S-curve axes, and diagnostic c
1707
1712
  <!-- TYPE:boxes-and-lines -->
1708
1713
 
1709
1714
  <!-- TIPS start -->
1710
- **Styling tips:** Show one clear direction of flow; group related boxes with a `[Group]` bracket and label each edge with the relationship rather than a bare arrow. Keep every box a short noun phrase.
1715
+ **Styling tips:** Show one clear direction of flow, label each edge with the relationship rather than a bare arrow, and keep every box a short noun phrase. **Color by category with a tag group:** declare a `tag <Facet> as <alias>` with a distinct color per value, then assign each box `<alias>: <Value>` so like things share a color and unlike things visibly differ — pick the facet that actually divides this system (tier: `Client`/`Edge`/`Compute`/`Data`, ownership, internal vs external, stateful vs stateless) rather than coloring at random. **Organize when it gets busy:** with more than ~4 boxes there is almost always a structure to surface — cluster the boxes that belong together into `[Group]` sections (a backend tier, a region, a subsystem) and draw edges to the group with `-> [Group]` where a whole cluster is the target. A flat row of many boxes is a missed opportunity; derive the grouping from how the parts relate. The `[Group]` brackets handle spatial clustering and the tag group handles color — use both, and they can cut across each other (e.g. group by subsystem, color by tier).
1711
1716
  <!-- TIPS end -->
1712
1717
 
1713
1718
  ### 13.1 Declaration
@@ -1843,6 +1848,30 @@ Flagship -> Sloop
1843
1848
  legend shows a gradient capsule; boxes without a `value:` get a neutral fill.
1844
1849
  - `show-values` additionally prints each box's number as text.
1845
1850
 
1851
+ ### 13.9 Tag groups (categorical color)
1852
+
1853
+ Color boxes by a category. Declare a `tag <Facet> as <alias>` group (§1.3) with one color per value — **values are indented under the `tag` header**, one per line, color as a trailing word — then assign each box `<alias>: <Value>` in its metadata. Do **not** write the values as `<alias> <Value>` lines; that is not the grammar.
1854
+
1855
+ ```
1856
+ boxes-and-lines Service Map
1857
+ tag Tier as t
1858
+ Client blue
1859
+ Edge teal
1860
+ Compute green
1861
+ Data purple
1862
+
1863
+ [Client]
1864
+ Web App t: Client
1865
+ [Edge]
1866
+ API Gateway t: Edge
1867
+ [Compute]
1868
+ Auth Service t: Compute
1869
+ [Data]
1870
+ Postgres t: Data
1871
+ ```
1872
+
1873
+ The tag group (color) composes with `[Group]` clustering (spatial) and can cut across it — group by subsystem, color by tier. The group must be declared before any content line (§1.3).
1874
+
1846
1875
  ---
1847
1876
 
1848
1877
  ## 15. Timeline Diagrams
@@ -2794,7 +2823,7 @@ Act red
2794
2823
  <!-- TYPE:journey-map -->
2795
2824
 
2796
2825
  <!-- TIPS start -->
2797
- **Styling tips:** Label a single-word `emotion:` on each step so the highs and lows are named, not just shaped; give one concrete action per stage and name the persona. Mark the trough with `pain:` and the peak with `opportunity:` so the arc’s meaning lands.
2826
+ **Styling tips:** A journey map is a story about one specific person, so author it richly even when the prompt is thin — fill the gaps with plausible, concrete detail rather than leaving it skeletal. **Persona:** give them a real first name, a color, and a short background — `persona Gina green`, then a sentence or 2–3 indented bullet (`- …`) lines covering who they are, their goal, and their constraints. Always color the persona (trailing color word after the name, or `persona Name color: green`) so the protagonist's card stands out. When the prompt implies a customer archetype, let the name's first letter echo it (a fanatic → Fred, a casual user → Carl, a beginner → Betty) — a light touch, never forced. **Structure:** organize the journey into 2–4 `[Phase]` sections, each holding 1–3 steps — and never one step per phase across the board. If every phase has exactly one step you've just bracketed a flat list; that 1:1 mapping is wrong — either give phases multiple steps or drop the brackets and let the steps run flat. Give every step a 1–5 `score:` (and a single-word `emotion:` at the highs and lows) so the mood curve has shape. **Depth per step:** every step gets a `description:` (what actually happens there) plus 2–5 annotations drawn from `pain:` (friction, red), `opportunity:` (a fix, green), and `thought:` (what they're thinking, italic) — choose the ones that genuinely fit that moment; a smooth early step might be all `thought:`/`opportunity:`, a trough heavy on `pain:`. Don't pad every step identically — let the friction cluster where the score dips so the arc reads true.
2798
2827
  <!-- TIPS end -->
2799
2828
 
2800
2829
  Persona-centric mood landscapes. Steps carry a 1–5 score and optional emotion label; the renderer draws an emotion curve over phase-grouped step cards. **Declaration is required** — the `journey-map` keyword must appear on the first line (no inference, to avoid colliding with kanban's `[Column]` + indented items shape).
@@ -3159,7 +3188,7 @@ Markers in cells are always **rendered in canonical alphabet order** (`R A C I`,
3159
3188
  <!-- TYPE:map -->
3160
3189
 
3161
3190
  <!-- TIPS start -->
3162
- **Styling tips:** the zero-config map already looks good — name places and stop. When POIs fall into categories, tag them so each category gets its own color; keep place labels to the place name; leave region colorize and coastlines on unless the user asks to hide them. **For routes/flows from a hub** (e.g. an airport's daily flights, a distribution center's shipments), write ONE edge per line and repeat the origin — `JFK ~daily~> LAX`, `JFK ~daily~> LHR`, … — never indented edges (those error). Use **arcs** (`~>`) for flights and long-haul links so the spokes separate; the connector label carries the relationship (`~daily~>`, `~2x daily~>`). Endpoints auto-create POIs, so don't add separate `poi` lines for places that already appear in an edge. Reach for a `route` block only when the trip is an **ordered voyage** that continues stop→stop (a cruise itinerary), not a set of independent routes from one origin.
3191
+ **Styling tips:** the zero-config map already looks good — name places and stop. When POIs fall into categories, tag them so each category gets its own color; keep place labels to the place name; leave region colorize and coastlines on unless the user asks to hide them. **For flights and airport routes, use IATA codes, not city names** — `LHR ~> JFK`, `LHR ~> DXB`, `LHR ~> SIN`, `LHR ~> HND` — the bundled airport set (large international hubs + all US commercial airports) resolves them to the right coordinates and labels them with the code. **For cities, use the exact canonical name**: "New York City" (NOT "New York"), "Washington, D.C.", etc. — a name the gazetteer doesn't have **silently drops that point** and the map just reframes around the rest, so it looks fine but is wrong. When unsure of the exact token, look it up: `dgmo map search "<place>"` (or the `lookup_map_location` MCP tool) returns the city name or airport code to paste; fall back to coordinates (`poi Name as 40.71,-74.0`) for anything not found. **For routes/flows from a hub** (an airport's daily flights, a distribution center's shipments), write ONE edge per line and repeat the origin — never indented edges (those error). Use **arcs** (`~>`) for flights and long-haul links so the spokes separate; the connector label carries the relationship (`~daily~>`, `~2x daily~>`). Endpoints auto-create POIs, so don't add separate `poi` lines for places already in an edge. Reach for a `route` block only when the trip is an **ordered voyage** that continues stop→stop (a cruise itinerary), not a set of independent routes from one origin.
3163
3192
  <!-- TIPS end -->
3164
3193
 
3165
3194
  Geographic concept maps: highlight/shade political subdivisions, drop points of interest (POIs), and connect them with routes or edges. For "share a concept" business maps, not cartography. Renders at a fixed, auto-fit position — no pan/zoom. Basemap and viewport are **inferred from the content you reference** — most maps need no directives. v1 boundaries: world countries + US states.
@@ -3302,6 +3331,8 @@ cove -> hav l: March # the LINE turns green
3302
3331
  - **Name + scope** (readable): `Georgia US value: 5` → the state, `Georgia GE value: 5` → the country.
3303
3332
  - The redundant `Georgia US-GA` still works but isn't needed (a mismatched code like `Georgia US-CA` is rejected). A bare ambiguous `Georgia` follows the inferred US-scope signal and warns with both fixes named.
3304
3333
  - **IATA airport codes** resolve to airport coordinates — `poi JFK`, `route JFK -> LAX` — with no new syntax (large international hubs + all US scheduled-commercial airports). Case-insensitive (`jfk`/`JFK`). Airports are the **lowest-precedence** identifier: a token that is both a city and a code resolves to the **city** (`Ufa` → the city, not UFA airport), with a hint naming the airport. Resolution is by **code only** (never by airport name); the POI label is the typed code. An unknown three-letter code errors with an `as <CODE>` coordinates hint.
3334
+ - **A city name must match the gazetteer's canonical form** — `New York City`, not `New York`; `Washington, D.C.`, not `Washington`. An unrecognized name is dropped (with a did-you-mean note) and the map reframes around the places that *did* resolve, so the result can look complete but be missing a point. Use the exact name or coordinates.
3335
+ - **Discover the exact token** with the lookup surface rather than guessing — `dgmo map search "<place>"` (CLI; `--json` for machine output) or the `lookup_map_location` MCP tool. It substring-searches cities **and** the bundled airports and returns the precise token to paste (e.g. `york` → `New York City`; `heathrow` → `LHR`). There is intentionally **no exhaustive printed list** — the gazetteer holds thousands of cities plus ~1,500 airports; search is the interface.
3305
3336
  - Positional coordinates are the escape hatch for anything missing/ambiguous (including forcing an airport over a colliding city: `poi 54.56 55.87 as UFA`).
3306
3337
 
3307
3338
  ### Directives & reserved keys
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.32.1",
3
+ "version": "0.34.0",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -134,7 +134,7 @@
134
134
  "check:deadcode": "knip",
135
135
  "check:spelling": "cspell \"src/**/*.ts\" \"tests/**/*.ts\"",
136
136
  "check:all": "pnpm check:deadcode && pnpm check:spelling && pnpm check:duplication && pnpm check:circular && pnpm check:deps && pnpm check:security && pnpm build && bash scripts/check-api.sh check && pnpm check:publish && pnpm check:types",
137
- "check:circular": "madge --circular --extensions ts src/ --json | node -e \"const c=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); const n=c.length; if(n>5){console.error('New circular deps found ('+n+' > 5 known type-only/dynamic cycles)');process.exit(1)}else if(n>0){console.log(n+' known type-only/dynamic cycles (safe)')}else{console.log('No circular dependencies')}\"",
137
+ "check:circular": "madge --circular --extensions ts src/ --json | node -e \"const c=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); const n=c.length; if(n>7){console.error('New circular deps found ('+n+' > 7 known type-only/dynamic cycles)');process.exit(1)}else if(n>0){console.log(n+' known type-only/dynamic cycles (safe)')}else{console.log('No circular dependencies')}\"",
138
138
  "check:deps": "depcheck --ignores='@diagrammo/dgmo,@codemirror/language,@lezer/*,husky,lint-staged,tsup,axe-core,type-coverage'",
139
139
  "check:security": "pnpm audit --prod",
140
140
  "check:publish": "publint",
package/src/advanced.ts CHANGED
@@ -569,11 +569,16 @@ export type {
569
569
  ProjectionFamily,
570
570
  GeoExtent,
571
571
  } from './map/resolved-types';
572
- export { completeMapPlaces, completeMapRegions } from './map/completion';
572
+ export {
573
+ completeMapPlaces,
574
+ completeMapRegions,
575
+ searchMapLocations,
576
+ } from './map/completion';
573
577
  export type {
574
578
  MapPlaceCompletion,
575
579
  MapRegionCompletion,
576
580
  MapCompletionOptions,
581
+ MapLocationMatch,
577
582
  } from './map/completion';
578
583
  export type {
579
584
  Gazetteer,
@@ -0,0 +1,339 @@
1
+ // ============================================================
2
+ // Boxes and Lines Diagram — Edge label placement
3
+ // ============================================================
4
+ //
5
+ // Makes edge/arrow labels legible regardless of length or layout via a priority
6
+ // ladder (each step only fires when the previous can't make the label legible):
7
+ //
8
+ // 1. WRAP — cap label width and wrap onto up to MAX_LABEL_LINES lines.
9
+ // 2. MOVE — if the wrapped box still overlaps a node box, slide it to a clear
10
+ // spot near its own line (along the polyline, then a small
11
+ // perpendicular offset), staying proximal. No leader line.
12
+ // 3. RELAYOUT (last resort, owned by layout.ts) — re-run the search reserving
13
+ // dagre label space so a gap opens; this module only reports which
14
+ // labels stayed unresolved so the caller can decide to escalate.
15
+ //
16
+ // Placement runs in layout (not the renderer) so it sees node/group boxes and so
17
+ // the relayout decision can re-invoke the search. The renderer is a pure consumer
18
+ // of the labelX/labelY/labelWidth/labelHeight/labelLines fields written here.
19
+
20
+ import type { BLLayoutResult } from './layout';
21
+ import {
22
+ measureText,
23
+ wrapTextToWidth,
24
+ truncateText,
25
+ } from '../utils/text-measure';
26
+
27
+ // Keep in sync with EDGE_LABEL_FONT_SIZE in renderer.ts (ScaleContext is identity
28
+ // there, so layout-space px == screen px for the unscaled diagram coords).
29
+ export const EDGE_LABEL_FONT_SIZE = 11;
30
+
31
+ const LABEL_MAX_WIDTH = 160; // ≈ DESC_NODE_WIDTH + slack; flat (not canvas-relative)
32
+ const MAX_LABEL_LINES = 3;
33
+ const LABEL_LINE_HEIGHT = 1.3;
34
+ const H_PAD = 6; // horizontal halo padding (each side)
35
+ const V_PAD = 3; // vertical halo padding (each side)
36
+ const BOX_CLEAR_PAD = 4; // min clearance kept between a label box and a node box
37
+ const PERP_STEP = 8; // perpendicular offset increment (px)
38
+ const PERP_MAX = 40; // max perpendicular offset before giving up
39
+ const SLIDE_SAMPLES = 9; // arc-length samples per side when sliding along the edge
40
+
41
+ type Pt = { readonly x: number; readonly y: number };
42
+
43
+ interface Rect {
44
+ readonly minX: number;
45
+ readonly minY: number;
46
+ readonly maxX: number;
47
+ readonly maxY: number;
48
+ }
49
+
50
+ export interface MeasuredLabel {
51
+ readonly lines: readonly string[];
52
+ /** Halo box width, including 2·H_PAD. */
53
+ readonly width: number;
54
+ /** Halo box height, including 2·V_PAD. */
55
+ readonly height: number;
56
+ }
57
+
58
+ /**
59
+ * Wrap + measure an edge label into a bounded multi-line box. Long unbreakable
60
+ * tokens are hard-broken at the char boundary; overflow past MAX_LABEL_LINES is
61
+ * ellipsized so the box is bounded in both axes.
62
+ */
63
+ export function measureEdgeLabel(
64
+ label: string,
65
+ fontSize: number = EDGE_LABEL_FONT_SIZE,
66
+ maxWidth: number = LABEL_MAX_WIDTH
67
+ ): MeasuredLabel {
68
+ let lines = wrapTextToWidth(label, fontSize, maxWidth, { hardBreak: true });
69
+ if (lines.length > MAX_LABEL_LINES) {
70
+ const kept = lines.slice(0, MAX_LABEL_LINES - 1);
71
+ const rest = lines.slice(MAX_LABEL_LINES - 1).join(' ');
72
+ kept.push(truncateText(rest, fontSize, maxWidth));
73
+ lines = kept;
74
+ }
75
+ let textW = 0;
76
+ for (const ln of lines) {
77
+ const w = measureText(ln, fontSize);
78
+ if (w > textW) textW = w;
79
+ }
80
+ return {
81
+ lines,
82
+ width: textW + 2 * H_PAD,
83
+ height: lines.length * fontSize * LABEL_LINE_HEIGHT + 2 * V_PAD,
84
+ };
85
+ }
86
+
87
+ /** Point at the half-way arc length along an edge polyline — the geometric
88
+ * centre of the connector, so a label seeds in the gap BETWEEN the two nodes
89
+ * rather than drifting under the target. Degenerate point lists fall back. */
90
+ export function edgePolylineMidpoint(points: ReadonlyArray<Pt>): Pt {
91
+ return pointAtArcFraction(points, 0.5).p;
92
+ }
93
+
94
+ /** Position + unit normal at arc-length fraction t∈[0,1] of a polyline. The
95
+ * normal is perpendicular to the local segment (for proximal perp offsets). */
96
+ function pointAtArcFraction(
97
+ points: ReadonlyArray<Pt>,
98
+ t: number
99
+ ): { p: Pt; nx: number; ny: number } {
100
+ if (points.length === 0) return { p: { x: 0, y: 0 }, nx: 0, ny: 1 };
101
+ if (points.length === 1)
102
+ return { p: { x: points[0]!.x, y: points[0]!.y }, nx: 0, ny: 1 };
103
+ const segLen: number[] = [];
104
+ let total = 0;
105
+ for (let k = 1; k < points.length; k++) {
106
+ const len = Math.hypot(
107
+ points[k]!.x - points[k - 1]!.x,
108
+ points[k]!.y - points[k - 1]!.y
109
+ );
110
+ segLen.push(len);
111
+ total += len;
112
+ }
113
+ let want = total * Math.max(0, Math.min(1, t));
114
+ for (let k = 1; k < points.length; k++) {
115
+ const len = segLen[k - 1]!;
116
+ if (want <= len || k === points.length - 1) {
117
+ const f = len === 0 ? 0 : Math.min(1, want / len);
118
+ const ax = points[k - 1]!.x;
119
+ const ay = points[k - 1]!.y;
120
+ const bx = points[k]!.x;
121
+ const by = points[k]!.y;
122
+ const dx = bx - ax;
123
+ const dy = by - ay;
124
+ const dlen = Math.hypot(dx, dy) || 1;
125
+ // Unit normal = perpendicular to the local tangent.
126
+ return {
127
+ p: { x: ax + dx * f, y: ay + dy * f },
128
+ nx: -dy / dlen,
129
+ ny: dx / dlen,
130
+ };
131
+ }
132
+ want -= len;
133
+ }
134
+ const last = points[points.length - 1]!;
135
+ return { p: { x: last.x, y: last.y }, nx: 0, ny: 1 };
136
+ }
137
+
138
+ function rectFromCenter(cx: number, cy: number, w: number, h: number): Rect {
139
+ return {
140
+ minX: cx - w / 2,
141
+ minY: cy - h / 2,
142
+ maxX: cx + w / 2,
143
+ maxY: cy + h / 2,
144
+ };
145
+ }
146
+
147
+ /** Does a label box centred at (cx,cy) overlap any obstacle (within pad)? */
148
+ function overlapsAny(
149
+ cx: number,
150
+ cy: number,
151
+ w: number,
152
+ h: number,
153
+ obstacles: readonly Rect[],
154
+ pad: number
155
+ ): boolean {
156
+ const minX = cx - w / 2 - pad;
157
+ const minY = cy - h / 2 - pad;
158
+ const maxX = cx + w / 2 + pad;
159
+ const maxY = cy + h / 2 + pad;
160
+ for (const o of obstacles) {
161
+ if (minX < o.maxX && maxX > o.minX && minY < o.maxY && maxY > o.minY)
162
+ return true;
163
+ }
164
+ return false;
165
+ }
166
+
167
+ /** Arc-length fractions sampled centre-outward (keeps the label proximal to the
168
+ * true midpoint), clamped away from the endpoints. */
169
+ function slideFractions(): number[] {
170
+ const out = [0.5];
171
+ for (let k = 1; k <= SLIDE_SAMPLES; k++) {
172
+ const off = k * 0.08;
173
+ if (0.5 - off >= 0.12) out.push(0.5 - off);
174
+ if (0.5 + off <= 0.88) out.push(0.5 + off);
175
+ }
176
+ return out;
177
+ }
178
+
179
+ /** Find the nearest clear centre for a label box of (w,h) along/near `points`.
180
+ * Pass 1 slides along the line; pass 2 offsets perpendicular. null = no clear
181
+ * spot found within the proximity budget. */
182
+ function findClearPosition(
183
+ w: number,
184
+ h: number,
185
+ points: ReadonlyArray<Pt>,
186
+ obstacles: readonly Rect[]
187
+ ): Pt | null {
188
+ const ts = slideFractions();
189
+ for (const t of ts) {
190
+ const { p } = pointAtArcFraction(points, t);
191
+ if (!overlapsAny(p.x, p.y, w, h, obstacles, BOX_CLEAR_PAD)) return p;
192
+ }
193
+ for (let mag = PERP_STEP; mag <= PERP_MAX; mag += PERP_STEP) {
194
+ for (const t of ts) {
195
+ const { p, nx, ny } = pointAtArcFraction(points, t);
196
+ for (const sign of [-1, 1]) {
197
+ const x = p.x + nx * mag * sign;
198
+ const y = p.y + ny * mag * sign;
199
+ if (!overlapsAny(x, y, w, h, obstacles, BOX_CLEAR_PAD)) return { x, y };
200
+ }
201
+ }
202
+ }
203
+ return null;
204
+ }
205
+
206
+ interface LabelBox {
207
+ edgeIdx: number;
208
+ cx: number;
209
+ cy: number;
210
+ w: number;
211
+ h: number;
212
+ lines: readonly string[];
213
+ resolved: boolean;
214
+ }
215
+
216
+ /** Separate overlapping label boxes vertically (variable sizes), mirroring the
217
+ * old renderer pass. Runs after node-collision resolution; the caller re-checks
218
+ * node overlap afterwards. */
219
+ function resolveLabelOverlaps(boxes: LabelBox[]): void {
220
+ const MAX_PASSES = 8;
221
+ const PAD = 4;
222
+ for (let pass = 0; pass < MAX_PASSES; pass++) {
223
+ let moved = false;
224
+ for (let i = 0; i < boxes.length; i++) {
225
+ for (let j = i + 1; j < boxes.length; j++) {
226
+ const a = boxes[i]!;
227
+ const b = boxes[j]!;
228
+ const dx = Math.abs(a.cx - b.cx);
229
+ const dy = Math.abs(a.cy - b.cy);
230
+ const overlapX = (a.w + b.w) / 2 + PAD - dx;
231
+ const overlapY = (a.h + b.h) / 2 + PAD - dy;
232
+ if (overlapX > 0 && overlapY > 0) {
233
+ const shift = overlapY / 2 + 1;
234
+ if (a.cy < b.cy) {
235
+ a.cy -= shift;
236
+ b.cy += shift;
237
+ } else {
238
+ a.cy += shift;
239
+ b.cy -= shift;
240
+ }
241
+ moved = true;
242
+ }
243
+ }
244
+ }
245
+ if (!moved) break;
246
+ }
247
+ }
248
+
249
+ export interface PlaceEdgeLabelsResult {
250
+ readonly layout: BLLayoutResult;
251
+ /** Edge indices whose label still overlaps a node box (escalate to relayout). */
252
+ readonly unresolved: number[];
253
+ }
254
+
255
+ /**
256
+ * Wrap, then collision-resolve every edge label against node boxes (steps 1–2),
257
+ * writing labelX/labelY/labelWidth/labelHeight/labelLines onto each edge. Returns
258
+ * the edge indices that could not be cleared so the caller can trigger a
259
+ * last-resort relayout.
260
+ */
261
+ export function placeEdgeLabels(
262
+ layout: BLLayoutResult,
263
+ opts?: { fontSize?: number; maxWidth?: number }
264
+ ): PlaceEdgeLabelsResult {
265
+ const fontSize = opts?.fontSize ?? EDGE_LABEL_FONT_SIZE;
266
+ const maxWidth = opts?.maxWidth ?? LABEL_MAX_WIDTH;
267
+
268
+ // Obstacles = real node boxes + collapsed groups (drawn as boxes). Expanded
269
+ // groups are containers — their interior is valid label space, so they are NOT
270
+ // obstacles.
271
+ const obstacles: Rect[] = [];
272
+ for (const n of layout.nodes)
273
+ obstacles.push(rectFromCenter(n.x, n.y, n.width, n.height));
274
+ for (const g of layout.groups)
275
+ if (g.collapsed)
276
+ obstacles.push(rectFromCenter(g.x, g.y, g.width, g.height));
277
+
278
+ const boxes: LabelBox[] = [];
279
+ layout.edges.forEach((e, idx) => {
280
+ if (!e.label || e.points.length < 2) return;
281
+ const m = measureEdgeLabel(e.label, fontSize, maxWidth);
282
+ const mid = edgePolylineMidpoint(e.points);
283
+ let cy = mid.y;
284
+ // Fold in the parallel-edge fan offset so each line's label clears its line.
285
+ if (e.parallelCount > 1 && e.yOffset !== 0)
286
+ cy += (e.yOffset < 0 ? -1 : 1) * (m.height / 2);
287
+ boxes.push({
288
+ edgeIdx: idx,
289
+ cx: mid.x,
290
+ cy,
291
+ w: m.width,
292
+ h: m.height,
293
+ lines: m.lines,
294
+ resolved: true,
295
+ });
296
+ });
297
+
298
+ // Step 2: reposition any box that overlaps a node.
299
+ for (const box of boxes) {
300
+ if (!overlapsAny(box.cx, box.cy, box.w, box.h, obstacles, BOX_CLEAR_PAD))
301
+ continue;
302
+ const e = layout.edges[box.edgeIdx]!;
303
+ const clear = findClearPosition(box.w, box.h, e.points, obstacles);
304
+ if (clear) {
305
+ box.cx = clear.x;
306
+ box.cy = clear.y;
307
+ } else {
308
+ box.resolved = false;
309
+ }
310
+ }
311
+
312
+ // Separate stacked labels, then re-check node overlap (a vertical nudge can
313
+ // push a label back onto a box — that becomes an unresolved escalation).
314
+ resolveLabelOverlaps(boxes);
315
+ for (const box of boxes) {
316
+ if (overlapsAny(box.cx, box.cy, box.w, box.h, obstacles, BOX_CLEAR_PAD))
317
+ box.resolved = false;
318
+ }
319
+
320
+ const boxByIdx = new Map<number, LabelBox>();
321
+ for (const b of boxes) boxByIdx.set(b.edgeIdx, b);
322
+ const edges = layout.edges.map((e, idx) => {
323
+ const b = boxByIdx.get(idx);
324
+ if (!b) return e;
325
+ return {
326
+ ...e,
327
+ labelX: b.cx,
328
+ labelY: b.cy,
329
+ labelWidth: b.w,
330
+ labelHeight: b.h,
331
+ labelLines: b.lines,
332
+ };
333
+ });
334
+
335
+ return {
336
+ layout: { ...layout, edges },
337
+ unresolved: boxes.filter((b) => !b.resolved).map((b) => b.edgeIdx),
338
+ };
339
+ }