agentic-mermaid 0.1.2 → 0.3.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 (54) hide show
  1. package/AGENT_NATIVE.md +671 -0
  2. package/CHANGELOG.md +36 -0
  3. package/Instructions_for_agents.md +89 -0
  4. package/README.md +8 -8
  5. package/assets/hero.png +0 -0
  6. package/dist/agent-core.d.ts +2 -2
  7. package/dist/agent-core.js +2 -2
  8. package/dist/agent.d.ts +2 -2
  9. package/dist/agent.js +4 -4
  10. package/dist/agentic-mermaid-mcp.js +5 -5
  11. package/dist/am.js +8 -15
  12. package/dist/browser.global.js +777 -0
  13. package/dist/{chunk-WN3YDOBG.js → chunk-3QCXO4V6.js} +4 -4
  14. package/dist/{chunk-4QECACIA.js → chunk-AVCWKYII.js} +19 -19
  15. package/dist/{chunk-WI35L7IR.js → chunk-GILQUGFK.js} +2 -2
  16. package/dist/{chunk-QKDLMOJJ.js → chunk-HFD4MALA.js} +7 -9
  17. package/dist/{chunk-CDPKJ2BQ.js → chunk-UJNY65QS.js} +2 -2
  18. package/dist/{chunk-ZYMMKQ3I.js → chunk-WHYJLDYG.js} +299 -973
  19. package/dist/{index-D54dqSdC.d.ts → index-CLwYn6Im.d.ts} +7 -4
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/mcp-cli-SJVOGRA6.js +17 -0
  23. package/docs/README.md +35 -34
  24. package/docs/agent-api-cookbook.md +317 -0
  25. package/docs/agent-mutation-policy.md +104 -0
  26. package/docs/agent-workflow-examples.md +46 -0
  27. package/docs/api.md +418 -0
  28. package/docs/ascii.md +113 -0
  29. package/docs/browser.md +183 -0
  30. package/docs/code-mode-impact.md +214 -0
  31. package/docs/comparison.md +106 -0
  32. package/docs/computational-aesthetics-prototype-plan.md +252 -0
  33. package/docs/config.md +121 -0
  34. package/docs/custom-fonts.md +265 -0
  35. package/docs/custom-style-cookbook.md +275 -0
  36. package/docs/diagram-families.md +218 -0
  37. package/docs/features.md +154 -0
  38. package/docs/fork-differences.md +166 -0
  39. package/docs/getting-started.md +182 -0
  40. package/docs/issue-derived-test-cases.md +163 -0
  41. package/docs/mcp-code-mode-rationale.md +108 -0
  42. package/docs/mcp-http-transport.md +230 -0
  43. package/docs/mermaid-layout-complaints.md +851 -0
  44. package/docs/mutation-testing.md +285 -0
  45. package/docs/quality.md +224 -0
  46. package/docs/react.md +97 -0
  47. package/docs/style-authoring.md +336 -0
  48. package/docs/svg-semantic-contract.md +49 -0
  49. package/docs/testing-strategy.md +443 -0
  50. package/docs/theming.md +149 -0
  51. package/package.json +28 -9
  52. package/server.json +2 -2
  53. package/skills/agentic-mermaid-live-editor/SKILL.md +1 -1
  54. package/dist/mcp-cli-SBQSTJFC.js +0 -17
@@ -0,0 +1,671 @@
1
+ # Agentic Mermaid — agent-native architecture
2
+
3
+ **Status.** Architecture/spec rationale for the merged agent-native surface on `main`. Current capabilities live in [`docs/features.md`](./docs/features.md); active work lives only in [`TODO.md`](./TODO.md). Not intended for upstream.
4
+
5
+ **System architecture overview.** For the how-the-engine-works entry point — the rendered three-stacks diagram and the routing/layout design docs — start at [`docs/design/system/README.md`](./docs/design/system/README.md).
6
+
7
+ **Thesis.** Agents authoring Mermaid diagrams today regenerate the whole source on every edit, or render to PNG and read it back with vision. The Beautiful Mermaid renderer foundation already fixed the worst of the rendering side (sync + DOM-free + ASCII). Agentic Mermaid adds the editing surface — structured verification, typed mutation, round-trippable IR — so an agent can edit one node and trust the result without ever opening an image.
8
+
9
+ ---
10
+
11
+ ## Why fork Beautiful Mermaid
12
+
13
+ The stack is three layers, each contributing something the others can't:
14
+
15
+ | Layer | Contribution |
16
+ |---|---|
17
+ | **Mermaid (grammar)** | 20+ diagram families. Rendered inline by GitHub, GitLab, Obsidian, Notion. Frontmatter + init + runtime config plane. `accTitle`/`accDescr` directives. **The corpus moat.** |
18
+ | **Beautiful Mermaid (renderer foundation)** | Synchronous, zero DOM, pure TypeScript. ASCII output. Two-color theming. Semantic role styling. Multi-family layout/render coverage. Property + mutation + e2e test scaffold already in place. **The AI-era renderer Craft built for Craft Agents.** |
19
+ | **Agentic Mermaid (this product/workflow)** | `ValidDiagram` IR. Deterministic layout. `verify()`. `mutate()` + round-trip `serializeMermaid`. Agent-agnostic skills, CLI, `--agent-instructions`. **The editing surface.** |
20
+
21
+ D2 has a better language than Mermaid. The Beautiful Mermaid renderer foundation has a better fit than D2 for agent contexts. Mermaid has a corpus neither of them has. The bet: stacking the three wins.
22
+
23
+ ---
24
+
25
+ ## The three properties
26
+
27
+ 1. **Deterministic layout** — same input → structurally identical layout JSON across runs with the same ELK version. (Not "byte-identical SVG across versions" — that's a stronger claim that needs a forked ELK to actually deliver.)
28
+ 2. **Verifiable rendering** — structured "did this render cleanly?" check. Structural warnings (anchors, bounds, emptiness, group containment) are reliable; metric warnings (label fit, overlap) are best-effort because they depend on font-measurement parity with ELK.
29
+ 3. **Round-trippable** — `parseRegisteredMermaid` produces a `ValidDiagram` that carries source needed to re-emit the diagram. For flowchart/state, sequence, timeline, class, ER, journey, architecture, xychart, pie, quadrant, gantt, mindmap, gitgraph, and radar, `mutate` operates on structured bodies; for opaque-fallback bodies, preserved source is the round-trip mechanism.
30
+
31
+ (Composition — `@include`, templates, layered scenarios — was the fourth in earlier drafts and is deferred. Agents do not currently reach for composition; they paste and edit. Add when evidence demands.)
32
+
33
+ (1) is a prerequisite for (2) and (3). Skip it and the others check moving targets.
34
+
35
+ ## Honest scope
36
+
37
+ The checked family roster, capability states, edit policies, narrowers, and mutation operations are generated from `FamilyDescriptor`: read [`docs/project/section-a-capability-report.md`](./docs/project/section-a-capability-report.md) or `am capabilities --json`, and use `describeOps(family)` for exact operation schemas. This rationale deliberately does not copy that live inventory.
38
+
39
+ The generated edit policy decides where typed mutation is available. For an opaque fallback, the agent's tool surface is *parse → verify → render → serialize*, not *parse → mutate → verify → serialize*. Cross-cutting edits on those bodies happen at the preserved source level (`body.source` for opaque bodies), not at the typed mutation layer. Code Mode opportunity #1 covers this for the cases where it matters.
40
+
41
+ ---
42
+
43
+ ## (1) Deterministic layout — structural, not seeded
44
+
45
+ **Empirically established (v4):** ELK output in this configuration is already deterministic, byte-identical across separate processes. The determinism comes from ELK's `considerModelOrder.strategy: NODES_AND_EDGES` setting plus the absence of any `randomSeed` option — layout order is a pure function of model order, not of randomness. Verified by a cross-process test (three separate `bun` invocations on the same source produce identical layout JSON) and a determinism grid.
46
+
47
+ This is the honest, stronger position. Earlier drafts wrapped ELK in a `withSeededRandom(rng, fn)` helper and exposed a `LayoutContext.rng` seed, claiming the seed "drove" layout. **It did not.** Seed 1 and seed 999999 produced byte-identical output because ELK never consults `Math.random` on this path. That apparatus was theater and is removed. There is no layout seed because none is needed; determinism is a property of the engine configuration, guarded by test. (The render option `seed` that exists today is a *style* seed — it re-rolls the ink of styled looks and never touches layout.)
48
+
49
+ Enforcement that determinism stays true:
50
+ - A **grep-based lint test** (runs under `bun test`, not aspirational ESLint) fails if `Math.random`, `Date.now`, or `performance.now` appear in `src/agent/**`, `src/gantt/**` (the Gantt scheduler must never read the wall clock), or `src/layout-engine.ts`. Introducing ambient nondeterminism breaks the build.
51
+ - A **cross-process determinism test** spawns child processes and asserts byte-identical layout.
52
+ - A **drift sentinel** pins canonical layout JSON for a hand-picked corpus; any change requires conscious re-baseline.
53
+
54
+ The canonical artifact is the **layout JSON**, not the SVG:
55
+
56
+ ```json
57
+ {
58
+ "version": 1,
59
+ "kind": "flowchart",
60
+ "nodes": [{ "id": "A", "x": 12, "y": 36, "w": 80, "h": 40, "shape": "rect" }, ...],
61
+ "edges": [{ "id": "A->B", "from": "A", "to": "B", "path": [[..],[..]], "label": {...} }, ...],
62
+ "groups": [{ "id": "g1", "x": 0, "y": 0, "w": 200, "h": 120, "members": ["A","B"] }, ...],
63
+ "bounds": { "w": 320, "h": 180 }
64
+ }
65
+ ```
66
+
67
+ There is no `seed` field (it was always `0` — a placeholder masquerading as state). SVG is the visual projection. Two render results are equal iff their layout JSON is structurally equal.
68
+
69
+ ---
70
+
71
+ ## (2) Verifiable rendering
72
+
73
+ ```ts
74
+ verifyMermaid(source: string | ValidDiagram, opts?: VerifyOptions): VerifyResult
75
+
76
+ interface VerifyOptions {
77
+ suppress?: WarningCode[] // codes to omit, e.g. ['UNKNOWN_SHAPE']
78
+ labelCharCap?: number // default cap 40
79
+ }
80
+
81
+ interface VerifyResult {
82
+ ok: boolean
83
+ warnings: LayoutWarning[]
84
+ layout: RenderedLayout
85
+ }
86
+ ```
87
+
88
+ Warnings split into three tiers by how reliable/actionable the underlying check is:
89
+
90
+ ### Tier 1 — Source-and-structure (reliable)
91
+
92
+ Derived from parsed structure or character-level source properties. Deterministic, no font measurement dependency.
93
+
94
+ | Code | Severity | Description |
95
+ |---|---|---|
96
+ | `EMPTY_DIAGRAM` | error | Diagram contains no renderable elements |
97
+ | `EDGE_MISANCHORED` | error | Edge endpoint does not attach to a real node / participant |
98
+ | `OFF_CANVAS` | error | Node or edge segment lies outside the canvas |
99
+ | `GROUP_BREACH` | error | Member node lies outside its group's bounds |
100
+ | `UNKNOWN_SHAPE` | warning | Shape name unrecognized; default used |
101
+ | `LABEL_OVERFLOW` | warning | Label character count exceeds the configurable limit (default 40 chars). Payload includes `charCount` and `limit`. Source-based, no font-table dependency. |
102
+ | `UNRESOLVABLE_SCHEDULE` | error | The diagram parses and round-trips but its semantics cannot resolve, so rendering will fail loudly. Emitted for structured gantt bodies whose scheduler raises a named `GANTT_*` error (bad calendar date, dependency cycle, everything-excluded calendar); the payload's `reason` carries that error. |
103
+ | `RENDER_FAILED` | error | Any family: the source verifies structurally but the strict render parser throws on its canonical source, so rendering would fail. Generalizes `UNRESOLVABLE_SCHEDULE`'s seam-closing — a clean verify proves the diagram actually renders; the payload's `reason` carries the renderer error. |
104
+
105
+ ### Tier 2 — Geometric (advisory)
106
+
107
+ Correctly detect what they claim to detect, but the occurrence may be intentional. Suppress when intent is clear; do not gate CI on them alone.
108
+
109
+ | Code | Severity | Description |
110
+ |---|---|---|
111
+ | `NODE_OVERLAP` | warning | Two laid-out node bounding boxes intersect |
112
+ | `ROUTE_SELF_CROSS` | warning | An edge route crosses itself |
113
+ | `ROUTE_HITCH` | warning | An edge bends although a direct lane for it is provably clear (route-contract tripwire) |
114
+ | `ROUTE_UNEXPLAINED_BEND` | warning | An edge contains a diagonal segment under orthogonal routing (route-contract tripwire) |
115
+ | `ROUTE_LABEL_ON_SHARED_TRUNK` | warning | A label pill sits on a line segment another edge shares (route-contract tripwire) |
116
+ | `ROUTE_SELF_LOOP_OCCUPANCY` | warning | A self-loop certificate has invalid side/boundary geometry or collides with another loop/label |
117
+ | `ROUTE_CONTAINER_MISANCHOR` | warning | A container edge does not terminate on the container border (route-contract tripwire) |
118
+ | `ROUTE_SHAPE_MISANCHOR` | warning | An endpoint is off the rendered shape boundary (route-contract tripwire) |
119
+ | `ROUTE_STALE_AFTER_NODE_MOVE` | warning | An endpoint detached from its node entirely (route-contract tripwire) |
120
+
121
+ Codes are the contract surface agents reason about. Emitting an undocumented code fails CI; documenting an unemitted one also fails CI. Agents omit known-irrelevant codes via `VerifyOptions.suppress`.
122
+
123
+ ### Tier 3 — Lint (advisory)
124
+
125
+ Tier 3 warnings are family-specific quality hints for "common LLM mistakes" that parse and render but are probably not what the agent intended. Advisory lint never flips `verify.ok`; an explicitly authored Brand constraint with `action: "error"` emits `BRAND_CONSTRAINT_ERROR` and does flip it.
126
+
127
+ | Code | Severity | Description |
128
+ |---|---|---|
129
+ | `DUPLICATE_EDGE` | warning | Flowchart/state contains an exact repeated edge with the same endpoints, label, style, and markers. Usually accidental regeneration or duplicate mutation. |
130
+ | `UNREACHABLE_NODE` | warning | Flowchart/state contains a node not reachable from any entry root when the graph has roots. Usually a stranded branch after an edit. |
131
+ | `DECISION_BRANCH_UNLABELED` | warning | A decision diamond has two or more exits and this branch carries no condition label. ISO 5807 (10.3.1.2) and ANSI X3.5 (4.10.2) require every exit of a multi-exit decision to be labeled with its condition value. |
132
+ | `COMMENT_DROPPED` | warning | The source contains in-body `%%` comments that this diagram's structured serialization does not preserve (reported with `count` and `lines`). The leading wrapper — frontmatter, `%%{init}%%` directives, comments before the header — always round-trips byte-verbatim; in-body comments survive only in opaque bodies or preserved opaque segments. Re-home load-bearing comments into the wrapper, or accept the loss as canonicalization. |
133
+ | `UNSUPPORTED_SYNTAX` | warning | The source uses Mermaid syntax that is preserved losslessly but not fully modeled by local structured mutation/render semantics (for example flowchart edge IDs, edge metadata, click/href directives, or markdown strings). Payload includes `syntax`, optional `line`, and `message`. |
134
+ | `CONTENT_DROPPED_ON_ROUNDTRIP` | warning | The structured `{nodes, edges, groups}` tally changed across a parse → serialize → re-parse cycle, so canonical serialization is silently dropping or duplicating content even though the bytes may re-parse (payload carries `before`/`after` counts). The faithfulness analogue of `COMMENT_DROPPED` — "100% parse success is not faithfulness". Runs on every verify, for every family; opaque bodies (byte-verbatim) are exempt. |
135
+ | `INEFFECTIVE_CONFIG` | warning | A Mermaid config field was accepted (for config-shape compatibility) but has no effect on this family's geometry or paint — e.g. Journey's sequence-era fields (`boxMargin`, `rightAngles`, …). Payload names the `field`. Accepting-and-ignoring silently misleads migrating users; this lint says so. |
136
+ | `LOW_CONTRAST` | warning | A concrete authored paint remains authoritative but misses a measurable contrast threshold against the final resolved opaque background. Payload names the `field`, `foreground`, `background`, measured `ratio`, and required `minimum`; verification diagnoses without repainting authored intent. Transparent output is not measured because its host backdrop is unknown. |
137
+ | `BRAND_CONSTRAINT_WARNING` | warning | A caller-selected inspect-only `contrast`, `accent-area`, or `mono-role` Brand constraint failed or could not be measured. Payload reports the constraint, `measurement`, applicable role/mark, and concrete evidence without repainting or relayout. |
138
+ | `BRAND_CONSTRAINT_ERROR` | error | The same inspect-only Brand constraint contract with `action: "error"`; it preserves authored output but flips `verify.ok` so a caller can enforce its declared brand policy. |
139
+
140
+ `FamilyDescriptor.verify` hooks are wired and run today; built-ins use them for Tier 1 structural warnings for class/ER and the central flowchart verifier emits the initial Tier 3 lint catalogue. Future lint codes should be added deliberately to `WARNING_TIER`, documented here, and covered by doc-sync tests.
141
+
142
+ **Branded coordinate types** (`Finite`) prevent NaN / Infinity from reaching the renderer. `toFinite()` is the only constructor; it throws on invalid input.
143
+
144
+ **Model-gap property test**: for every generated `D` that parses successfully, `verify(D).warnings` filtered to Tier-1 `error` codes must be empty. Counterexamples are renderer bugs. Tier-2 warnings are excluded from this property because a `NODE_OVERLAP` or `ROUTE_SELF_CROSS` can be a legitimate property of a valid diagram, not a bug. Tier-3 lint warnings are excluded because they are maintainability hints, not render failures.
145
+
146
+ ---
147
+
148
+ ## (3) Round-trippable
149
+
150
+ A sealed `ValidDiagram` that preserves everything the source had — and carries the canonical source verbatim as the round-trip pillar:
151
+
152
+ ```ts
153
+ interface ValidDiagram {
154
+ readonly kind: DiagramKind
155
+ readonly meta: {
156
+ frontmatter?: Frontmatter
157
+ initDirectives: InitDirective[]
158
+ comments: Comment[]
159
+ accessibility: { title?: string, descr?: string }
160
+ }
161
+ readonly body: DiagramBody
162
+ readonly source: SourceMap
163
+ /**
164
+ * The canonical preprocessed source — frontmatter, init directives, and
165
+ * comments stripped; line breaks normalized. Structured renderers use this as
166
+ * input. Opaque/source-level fidelity relies on `body.source`, not this field.
167
+ */
168
+ readonly canonicalSource: string
169
+ }
170
+
171
+ type DiagramBody =
172
+ | { kind: 'flowchart'; graph: MermaidGraph }
173
+ | StateBody | SequenceBody | TimelineBody | ClassBody | ErBody
174
+ | JourneyBody | ArchitectureBody | XyChartBody | PieBody | QuadrantBody | GanttBody
175
+ | MindmapBody | GitGraphBody | RadarBody
176
+ | { kind: 'opaque'; family: DiagramKind; source: string }
177
+
178
+ type MutableValidDiagram = import('agentic-mermaid/agent').MutableValidDiagram
179
+ ```
180
+
181
+ ```ts
182
+ parseRegisteredMermaid(source: string): Result<ValidDiagram, ParseError[]>
183
+ serializeMermaid(d: ValidDiagram): string
184
+ synthesizeFromGraph(payload): Result<ValidDiagram, ParseError[]>
185
+ createMermaid(kind, opts?): MutableValidDiagram // empty structured diagram; overloads narrow per kind
186
+ buildMermaid(kind, ops, opts?): Result<MutableValidDiagram, MutationError & { opIndex: number }>
187
+ mutate(d, op): Result<MutableValidDiagram, MutationError>
188
+ // Concrete overloads preserve the narrowed family. Overloads and narrowers are
189
+ // registry-derived; discover them with am capabilities --json and describeOps(family).
190
+ ```
191
+
192
+ **`mutate` is overloaded by family.** A registry-declared narrower returns the family-specific mutable type or `null`; the generated capability entry and `describeOps(family)` own the live operation contract. Opaque-fallback diagrams are not typed for mutation, so agents get a compile-time/null-narrower stop rather than a lossy edit path.
193
+
194
+ Two contracts:
195
+
196
+ - `serializeMermaid(parseRegisteredMermaid(s)) ≡ normalize(s)` for canonical input. For structured families this emits a fresh canonical form; for opaque families it emits preserved source with `meta` re-attached.
197
+ - `parseRegisteredMermaid(serializeMermaid(d)) ≡ d` for every `d` produced by `parseRegisteredMermaid` or `mutate`.
198
+
199
+ **Flowchart MutationOp kinds** (the authoritative menu is generated from the registry; `shape` fields accept geometry names and documented v11 names/aliases, and edge targets accept authored edge IDs or endpoint forms):
200
+
201
+ | Kind | Required | Optional | Inverse |
202
+ |---|---|---|---|
203
+ | `add_node` | `id`, `label` | `shape`, `parent` | `remove_node(id)` |
204
+ | `remove_node` | `id` | — | `add_node(id, label, shape, parent)` |
205
+ | `rename_node` | `from`, `to` | — | `rename_node(to, from)` |
206
+ | `set_label` | `target`, `label` | — | `set_label(target, prev_label)` |
207
+ | `add_edge` | `from`, `to` | `label`, `style` | `remove_edge(id)` |
208
+ | `remove_edge` | `id` | — | `add_edge(from, to, label, style)` |
209
+ | `set_shape` | `id`, `shape` | — | `set_shape(id, prev_shape)` |
210
+ | `set_direction` | `direction` | `subgraph` (sets that subgraph's override; omit = diagram) | `set_direction(prev_direction, subgraph)` |
211
+ | `add_subgraph` | `id` | `label`, `parent`, `members` (existing nodes MOVED in — the state `make_composite` precedent) | `remove_subgraph(id)` |
212
+ | `remove_subgraph` | `id` | `removeMembers` (default dissolves the box — members move to the parent scope; `true` deletes member nodes and their edges) | `add_subgraph(...)` |
213
+ | `move_node` | `id`, `subgraph \| null` (null = top level) | — | `move_node(id, prev_subgraph)` |
214
+ | `define_class` | `name`, `style` (CSS-like pairs, parsed by the parser's own style grammar) | — | `define_class(name, prev_style)` |
215
+ | `set_node_class` | `id`, `className \| null` | — | `set_node_class(id, prev_class)` |
216
+ | `set_node_style` | `id`, `style \| null` | — | `set_node_style(id, prev_style)` |
217
+
218
+ **State MutationOp kinds** (18, BUILD-19 + the family elevation — promoting state from a "parses AS flowchart" projection to a dedicated `StateBody` IR with state-shaped ops and a real `asState` narrower). Modeled grammar: simple states (`state "Label" as id`, `id : Label`), transitions `from --> to [: label]` where `from`/`to` may be the reserved pseudostate `[*]` (source = start, target = end, scoped per composite level) or a history reference (`[H]`, `Base[H*]`), composite blocks `state X { … }` (nestable), `direction`, pseudostate stereotypes (`state id <<fork|join|choice>>` plus the history forms `<<history>>`/`<<H>>`/`<<deephistory>>`/`<<H*>>`), and notes (`note left|right of X : text`, block form included). Concurrency `--`, `classDef`/`class`/`:::` styling, bare state lines, and hyphenated composite ids are modeled structurally. `[*]` is contextual, not a state node:
219
+
220
+ | Kind | Required | Optional | Inverse |
221
+ |---|---|---|---|
222
+ | `add_state` | `id` | `label`, `parent` (composite) | `remove_state(id)` |
223
+ | `remove_state` | `id` (refused on a non-empty composite unless `recursive`) | `recursive` (`true` removes the whole subtree; touching transitions/notes cascade, history refs `X[H]` included) | `add_state(...)` |
224
+ | `rename_state` | `from`, `to` | — (rewrites transitions) | `rename_state(to, from)` |
225
+ | `set_state_label` | `id`, `label \| null` | — | `set_state_label(id, prev_label)` |
226
+ | `add_transition` | `from`, `to` (`[*]` allowed) | `label`, `parent` | `remove_transition(from->to)` |
227
+ | `remove_transition` | `index` or `from`/`to` pair | `parent` | `add_transition(...)` |
228
+ | `set_transition_label` | `label \| null` + (`index` or `from`/`to`) | `parent` | `set_transition_label(..., prev_label)` |
229
+ | `make_composite` | `id`, `members: string[]` | `label` | `dissolve_composite(id)` |
230
+ | `set_direction` | `direction` | `state` (composite override; omit/null = diagram) | `set_direction(prev, state)` |
231
+ | `move_state` | `id`, `parent \| null` (null = top level; a simple parent promotes to a composite) | — | `move_state(id, prev_parent)` |
232
+ | `dissolve_composite` | `id` (hoists children + inner transitions into the parent scope; rejects while transitions/notes still reference it) | — | `make_composite(id, members, label)` |
233
+ | `add_note` | `target`, `text` | `side` (`left`/`right`, default `right`) | `remove_note(index)` |
234
+ | `remove_note` | `index` | — | `add_note(target, side, text)` |
235
+ | `set_note_text` | `index`, `text` | — | `set_note_text(index, prev_text)` |
236
+ | `define_class` | `name`, `style` | — | restore previous definition |
237
+ | `set_state_class` | `id`, `className \| null` | — | restore previous class |
238
+ | `set_state_style` | `id`, `style \| null` | — | restore previous style |
239
+ | `set_transition_style` | selector + `style \| null` | `parent`, `region` | restore previous style |
240
+
241
+ **Sequence MutationOp kinds** (top-level message indices remain separate from typed fragment-message indices):
242
+
243
+ | Kind | Required | Optional | Inverse |
244
+ |---|---|---|---|
245
+ | `add_participant` | `id` | `label`, `participantKind` (`participant`/`actor`) | `remove_participant(id)` |
246
+ | `remove_participant` | `id` | — | `add_participant(id, label)` |
247
+ | `add_message` | `from`, `to`, `text` | `style` (sync/async), insert `index` (omitted = append) | `remove_message(index)` |
248
+ | `remove_message` | `index` | — | `add_message(...)` |
249
+ | `set_message_text` | `index`, `text` | — | `set_message_text(index, prev_text)` |
250
+ | `move_message` | `from`, `to` (source order IS the interaction timeline, so reorder is a first-class edit) | — | `move_message(to, from)` |
251
+ | `set_participant_label` | `id`, `label` | — | `set_participant_label(id, prev_label)` |
252
+ | `add_fragment` / `remove_fragment` / `set_fragment_label` | fragment kind or index | label/index | matching inverse |
253
+ | `add_fragment_branch` / `set_fragment_branch_label` | fragment/branch indices | label | matching inverse |
254
+ | `add_fragment_message` / `remove_fragment_message` / `set_fragment_message_text` | fragment index + message fields/index | branch/index/style | matching inverse |
255
+
256
+ **Timeline MutationOp kinds** (15 — the journey conventions from PR #141: `index?` on add ops is an insert position, move ops take the insert position after removal, and errors name the legal range):
257
+
258
+ | Kind | Required | Inverse |
259
+ |---|---|---|
260
+ | `set_title` | `title \| null` | `set_title(prev_title)` |
261
+ | `add_section` | `label` (+ optional insert `index`) | `remove_section(index)` |
262
+ | `remove_section` | `index` | `add_section(label)` |
263
+ | `set_section_label` | `index`, `label` | `set_section_label(index, prev_label)` |
264
+ | `add_period` | `sectionIndex`, `label` (+ optional `events: string[]`, insert `index`) | `remove_period(sectionIndex, periodIndex)` |
265
+ | `remove_period` | `sectionIndex`, `periodIndex` | `add_period(...)` |
266
+ | `set_period_label` | `sectionIndex`, `periodIndex`, `label` | `set_period_label(... prev_label)` |
267
+ | `add_event` | `sectionIndex`, `periodIndex`, `text` (+ optional insert `index`) | `remove_event(... eventIndex)` |
268
+ | `remove_event` | `sectionIndex`, `periodIndex`, `eventIndex` | `add_event(...)` |
269
+ | `set_event_text` | `sectionIndex`, `periodIndex`, `eventIndex`, `text` | `set_event_text(... prev_text)` |
270
+ | `move_period` | `fromSection`, `fromIndex`, `toSection`, `toIndex` (timeline order IS the chronology, so reorder is a first-class edit) | `move_period(toSection, toIndex, fromSection, fromIndex)` |
271
+ | `move_event` | `fromSection`, `fromPeriod`, `fromIndex`, `toSection`, `toPeriod`, `toIndex` | `move_event(...)` |
272
+ | `move_section` | `from`, `to` | `move_section(to, from)` |
273
+ | `set_accessibility_title` | `title \| null` | `set_accessibility_title(prev)` |
274
+ | `set_accessibility_description` | `description \| null` | `set_accessibility_description(prev)` |
275
+
276
+ **Class MutationOp kinds** (15):
277
+
278
+ | Kind | Required | Inverse |
279
+ |---|---|---|
280
+ | `set_title` | `title \| null` | `set_title(prev_title)` |
281
+ | `add_class` | `id` (+ optional `label`, `generic`, `members: string[]`, `namespace` dot path) | `remove_class(id)` |
282
+ | `remove_class` | `id` | `add_class(id, label, members)` |
283
+ | `rename_class` | `from`, `to` | `rename_class(to, from)` |
284
+ | `set_class_generic` | `class`, `generic \| null` | `set_class_generic(class, prev_generic)` |
285
+ | `add_member` | `class`, `text` | `remove_member(class, index)` |
286
+ | `remove_member` | `class`, `index` | `add_member(class, text)` |
287
+ | `add_relation` | `from`, `to`, `relKind` (+ optional `label`) | `remove_relation(index)` |
288
+ | `remove_relation` | `index` | `add_relation(...)` |
289
+ | `add_note` | `text` (+ optional `for: class`) | `remove_note(index)` |
290
+ | `remove_note` | `index` | `add_note(text, for)` |
291
+ | `set_class_namespace` | `class`, `namespace \| null` (a dot path like `Platform.Auth`, declared on demand; null = top level) | `set_class_namespace(class, prev_namespace)` |
292
+ | `define_class` | `name`, `style` | restore previous definition |
293
+ | `set_css_class` | `class`, `className \| null` | restore previous assignment |
294
+ | `set_class_style` | `class`, `style \| null` | restore previous style |
295
+
296
+ **ER MutationOp kinds** (12):
297
+
298
+ | Kind | Required | Inverse |
299
+ |---|---|---|
300
+ | `add_entity` | `id` (+ optional `label`, `attributes: string[]`) | `remove_entity(id)` |
301
+ | `remove_entity` | `id` | `add_entity(id, label, attributes)` |
302
+ | `rename_entity` | `from`, `to` | `rename_entity(to, from)` |
303
+ | `set_entity_label` | `entity`, `label \| null` | `set_entity_label(entity, previous_label)` |
304
+ | `add_attribute` | `entity`, `text` | `remove_attribute(entity, index)` |
305
+ | `remove_attribute` | `entity`, `index` | `add_attribute(entity, text)` |
306
+ | `add_relation` | `from`, `to`, `leftCard`, `rightCard` (+ `dashed`, `label`) | `remove_relation(index)` |
307
+ | `remove_relation` | `index` | `add_relation(...)` |
308
+ | `set_direction` | `direction` | restore previous direction |
309
+ | `define_class` | `name`, `style` | restore previous definition |
310
+ | `set_entity_class` | `entity`, `className \| null` | restore previous assignment |
311
+ | `set_entity_style` | `entity`, `style \| null` | restore previous style |
312
+
313
+ **Journey MutationOp kinds** (promoted from opaque-only fallback semantics through the family-descriptor registry; ordering and accessibility are typed because Journey order is the timeline):
314
+
315
+ | Kind | Required | Inverse |
316
+ |---|---|---|
317
+ | `set_title` | `title \| null` | `set_title(prev_title)` |
318
+ | `add_section` | `label` (+ optional insert `index`) | `remove_section(index)` |
319
+ | `remove_section` | `index` | `add_section(label)` |
320
+ | `set_section_label` | `index`, `label` | `set_section_label(index, prev_label)` |
321
+ | `add_task` | `sectionIndex`, `text`, `score` (+ optional `actors`, insert `index`) | `remove_task(sectionIndex, taskIndex)` |
322
+ | `remove_task` | `sectionIndex`, `taskIndex` | `add_task(...)` |
323
+ | `set_task_text` | `sectionIndex`, `taskIndex`, `text` | `set_task_text(... prev_text)` |
324
+ | `set_task_score` | `sectionIndex`, `taskIndex`, `score` (integer 1..5) | `set_task_score(... prev_score)` |
325
+ | `set_task_actors` | `sectionIndex`, `taskIndex`, `actors: string[]` | `set_task_actors(... prev_actors)` |
326
+ | `rename_actor` | `from`, `to` | `rename_actor(to, from)` |
327
+ | `move_task` | `fromSection`, `fromIndex`, `toSection`, `toIndex` | `move_task(toSection, toIndex, fromSection, fromIndex)` |
328
+ | `move_section` | `from`, `to` | `move_section(to, from)` |
329
+ | `set_accessibility_title` | `title \| null` | `set_accessibility_title(prev)` |
330
+ | `set_accessibility_description` | `description \| null` | `set_accessibility_description(prev)` |
331
+
332
+ **Architecture MutationOp kinds** (19):
333
+
334
+ | Kind | Required | Inverse |
335
+ |---|---|---|
336
+ | `set_title` | `title \| null` | `set_title(prev_title)` |
337
+ | `set_accessibility_title` | `title \| null` | `set_accessibility_title(prev)` |
338
+ | `set_accessibility_description` | `description \| null` | `set_accessibility_description(prev)` |
339
+ | `add_service` | `id` (+ optional `label`, `icon`, `group`) | `remove_service(id)` |
340
+ | `remove_service` | `id` (cascades edges/alignments) | `add_service(...)` + re-add edges |
341
+ | `rename_service` | `from`, `to` (rewrites edges/alignments) | `rename_service(to, from)` |
342
+ | `set_service_label` | `id`, `label` | `set_service_label(id, prev_label)` |
343
+ | `set_service_icon` | `id`, `icon: string \| null` | `set_service_icon(id, prev_icon)` |
344
+ | `move_service` | `id`, `group: string \| null` | `move_service(id, prev_group)` |
345
+ | `add_junction` | `id` (+ optional `group`) | `remove_junction(id)` |
346
+ | `remove_junction` | `id` (cascades edges/alignments) | `add_junction(...)` + re-add edges |
347
+ | `rename_junction` | `from`, `to` (rewrites edges/alignments) | `rename_junction(to, from)` |
348
+ | `move_junction` | `id`, `group: string \| null` | `move_junction(id, prev_group)` |
349
+ | `add_group` | `id` (+ optional `label`, `icon`, `parent`) | `remove_group(id)` |
350
+ | `set_group_label` | `id`, `label` | `set_group_label(id, prev_label)` |
351
+ | `remove_group` | `id` (refused if non-empty) | `add_group(...)` |
352
+ | `add_edge` | `from`, `to`, `fromSide`, `toSide` (+ optional boundaries/label/arrows) | `remove_edge(id)` |
353
+ | `update_edge` | `index` (+ any endpoint/side/boundary/label/arrow fields) | `update_edge(index, previous fields)` |
354
+ | `remove_edge` | `index` or `id` (`from->to`) | `add_edge(...)` |
355
+
356
+ **XY chart MutationOp kinds** (10, BUILD-16 — promoting the xychart family to structured mutation via the family-descriptor registry, following the BUILD-15 journey and BUILD-17 architecture pilots). Canonical number format is `String(n)` (shortest round-tripping decimal); all values must be finite. Modeled grammar covers bare titles/axis-names/series-names/categories — quoted text whose content is bare parses and canonicalizes to unquoted form; embedded quotes/brackets, multi-statement `;` lines, accTitle/accDescr, and any other unmodeled syntax fall back to opaque:
357
+
358
+ | Kind | Required | Inverse |
359
+ |---|---|---|
360
+ | `set_title` | `title \| null` | `set_title(prev_title)` |
361
+ | `set_x_axis` | `axis: { name?, categories?, range? } \| null` (categories XOR range) | `set_x_axis(prev_axis)` |
362
+ | `set_y_axis` | `axis: { name?, range? } \| null` (y-axis is never categorical) | `set_y_axis(prev_axis)` |
363
+ | `add_series` | `kind2: 'bar' \| 'line'`, `values: number[]` (+ optional `name`) | `remove_series(index)` |
364
+ | `remove_series` | `index` | `add_series(...)` |
365
+ | `set_series_values` | `index`, `values: number[]` | `set_series_values(index, prev_values)` |
366
+ | `set_series_name` | `index`, `name: string \| null` | `set_series_name(index, prev_name)` |
367
+ | `reorder_series` | `from`, `to` | `reorder_series(to, from)` |
368
+ | `set_orientation` | `horizontal: boolean` (`true` adds the `horizontal` header suffix; `false` drops it — vertical is the serialized default) | `set_orientation(prev)` |
369
+ | `set_data_point` | `seriesIndex`, `index`, `value` (finite; out-of-range indices name the valid ranges) | `set_data_point(seriesIndex, index, prev_value)` |
370
+
371
+ **Pie MutationOp kinds** (7 — promoting the pie family to structured mutation via the family-descriptor registry, following the journey/architecture/xychart pilots). Slices are addressed by their (unique) label; values must be positive finite numbers. The header's `showData` flag and an optional `title` are modeled; any unmodeled line (accTitle/accDescr, malformed entry) falls back to opaque losslessly:
372
+
373
+ | Kind | Required | Inverse |
374
+ |---|---|---|
375
+ | `set_title` | `title \| null` | `set_title(prev_title)` |
376
+ | `set_show_data` | `showData: boolean` | `set_show_data(prev_flag)` |
377
+ | `add_slice` | `label`, `value` (> 0) | `remove_slice(label)` |
378
+ | `remove_slice` | `label` | `add_slice(...)` |
379
+ | `rename_slice` | `from`, `to` | `rename_slice(to, from)` |
380
+ | `set_slice_value` | `label`, `value` (> 0) | `set_slice_value(label, prev_value)` |
381
+ | `reorder_slice` | `from`, `to` | `reorder_slice(to, from)` |
382
+
383
+ **Quadrant MutationOp kinds** (7 — promoting the quadrantChart family to structured mutation via the family-descriptor registry). Points are addressed by their (unique) label; coordinates must be in `[0, 1]`. Quadrant numbering follows Mermaid core (1=top-right, 2=top-left, 3=bottom-left, 4=bottom-right). Styling (`classDef`, `:::`), out-of-range coordinates, and any unmodeled line fall back to opaque losslessly:
384
+
385
+ | Kind | Required | Inverse |
386
+ |---|---|---|
387
+ | `set_title` | `title \| null` | `set_title(prev_title)` |
388
+ | `set_axis_labels` | `axis: 'x' \| 'y'`, `near: string \| null` (+ optional `far`) | `set_axis_labels(axis, prev_near, prev_far)` |
389
+ | `set_quadrant_label` | `quadrant: 1..4`, `label: string \| null` | `set_quadrant_label(quadrant, prev_label)` |
390
+ | `add_point` | `label`, `x`, `y` (in `[0,1]`) | `remove_point(label)` |
391
+ | `remove_point` | `label` | `add_point(...)` |
392
+ | `move_point` | `label`, `x`, `y` (in `[0,1]`) | `move_point(label, prev_x, prev_y)` |
393
+ | `rename_point` | `from`, `to` | `rename_point(to, from)` |
394
+
395
+ **Gantt MutationOp kinds** (13 — segment-preserving from day one per [`docs/design/families/gantt.md`](./docs/design/families/gantt.md)). Sections and tasks are addressed by index (`sectionIndex`, `taskIndex`); `taskId` is the Mermaid id used by `after`/`until`/`click`. Calendar directives (`dateFormat`, `excludes`, `includes`, `weekend`, `weekday`, `todayMarker`, `tickInterval`, `inclusiveEndDates`, `topAxis`), `click` lines, accTitle/accDescr, and comments ride along VERBATIM as opaque-block segments — preserved, source-level-editable, never typed-editable in v1. Every value an op writes is validated by rendering its canonical line and re-parsing it (correctness by construction), so labels with `:` or values with `,` are rejected rather than corrupting the source. Gantt source order IS scheduling semantics (a task with an implicit start chains from the previous task in flat source order), so the move ops REJECT prescriptively whenever they would change an implicit-start task's predecessor — materialize an explicit start (`set_task_dates` or an `after` dependency) and retry:
396
+
397
+ | Kind | Required | Inverse |
398
+ |---|---|---|
399
+ | `set_title` | `title \| null` | `set_title(prev_title)` |
400
+ | `add_section` | `label` | `remove_section(index)` |
401
+ | `rename_section` | `index`, `label` | `rename_section(index, prev_label)` |
402
+ | `remove_section` | `index` (drops its tasks too) | `add_section(...)` + `add_task(...)` |
403
+ | `add_task` | `sectionIndex`, `label`, `end` (+ optional `taskId`, `tags`, `start`, insert `index` — a mid-chain insert deliberately re-chains the follower) | `remove_task(sectionIndex, taskIndex)` |
404
+ | `remove_task` | `sectionIndex`, `taskIndex` | `add_task(...)` |
405
+ | `rename_task` | `sectionIndex`, `taskIndex`, `label` | `rename_task(..., prev_label)` |
406
+ | `set_task_status` | `sectionIndex`, `taskIndex`, `status: 'active' \| 'done' \| 'crit' \| null` (milestone/vert tags are never disturbed) | `set_task_status(..., prev_status)` |
407
+ | `set_task_dates` | `sectionIndex`, `taskIndex`, `start?: string \| null`, `end?: string` | `set_task_dates(..., prev_start, prev_end)` |
408
+ | `set_task_flags` | `sectionIndex`, `taskIndex`, `milestone?`, `vert?` (structural-tag toggles; `set_task_status` never touches them) | `set_task_flags(..., prev_flags)` |
409
+ | `set_task_id` | `sectionIndex`, `taskIndex`, `taskId \| null` (a rename REWRITES structured `after`/`until` references; REJECTS while opaque segments reference the id, and `null` rejects while ANY reference exists) | `set_task_id(..., prev_id)` |
410
+ | `move_task` | `fromSection`, `fromIndex`, `toSection`, `toIndex` (implicit-start guard above) | `move_task(toSection, toIndex, fromSection, fromIndex)` |
411
+ | `move_section` | `from`, `to` (implicit-start guard above) | `move_section(to, from)` |
412
+
413
+ **Mindmap MutationOp kinds** (10; see [`docs/design/families/mindmap.md`](./docs/design/families/mindmap.md)):
414
+
415
+ `add_node`, `remove_node`, `rename_node`, `set_label`, `move_node`, `set_shape`, `set_icon`, `set_node_class`, `set_accessibility_title`, `set_accessibility_description`.
416
+
417
+ **GitGraph MutationOp kinds** (11; see [`docs/design/families/gitgraph.md`](./docs/design/families/gitgraph.md)):
418
+
419
+ `append_commit`, `create_branch`, `checkout_branch`, `merge_branch`, `cherry_pick`, `set_commit_message`, `set_commit_type`, `set_commit_tags`, `rename_branch`, `set_accessibility_title`, `set_accessibility_description`.
420
+
421
+ **Radar MutationOp kinds** (14 — promoting the `radar-beta` family to structured mutation via the FamilyPlugin registry). Axes and curves are addressed by unique ids; each curve carries exactly one finite value per axis, in axis order. The header keyword is `radar-beta`; order-independent and multiline `title`, `axis`/`curve`, and option declarations are modeled. Duplicate identities, vector mismatches, accessibility directives, and any unmodeled line fall back to opaque losslessly rather than weakening the typed invariant:
422
+
423
+ | Kind | Required | Inverse |
424
+ |---|---|---|
425
+ | `set_title` | `title \| null` | `set_title(prev_title)` |
426
+ | `add_axis` | `id` (+ optional `label`, insert `index`, `fill`) | `remove_axis(id)` |
427
+ | `remove_axis` | `id` | `add_axis(...)` |
428
+ | `rename_axis` | `from`, `to` | `rename_axis(to, from)` |
429
+ | `set_axis_label` | `id`, `label \| null` | `set_axis_label(id, prev_label)` |
430
+ | `reorder_axis` | `from`, `to` (permutes every curve's value vector) | `reorder_axis(to, from)` |
431
+ | `add_curve` | `id`, `values` (one per axis) (+ optional `label`, insert `index`) | `remove_curve(id)` |
432
+ | `remove_curve` | `id` | `add_curve(...)` |
433
+ | `set_curve_values` | `id`, `values` (one per axis) | `set_curve_values(id, prev_values)` |
434
+ | `set_curve_value` | `curve`, `axis`, `value` (finite) | `set_curve_value(curve, axis, prev_value)` |
435
+ | `set_curve_label` | `id`, `label \| null` | `set_curve_label(id, prev_label)` |
436
+ | `rename_curve` | `from`, `to` | `rename_curve(to, from)` |
437
+ | `reorder_curve` | `from`, `to` | `reorder_curve(to, from)` |
438
+ | `set_config` | optional `max`, `min`, `ticks` (`1..64`), `graticule`, `showLegend`; `null` resets each field | `set_config(prev_fields)` |
439
+
440
+ **Structured-or-opaque rule (v4): never lossy.** The parser only produces a structured body when it fully understands every non-blank, non-comment line for most structured families. If the source contains *any* construct the parser doesn't model — `direction TB` in class, out-of-range scores in journey, duplicate Architecture title declarations, a `;`-joined multi-statement line in xychart, a malformed entry in pie, or out-of-range coordinates in quadrant, etc. — parsing **falls back to an opaque body**. The diagram still parses, renders, verifies (structurally), and round-trips losslessly via preserved `body.source`; it simply isn't offered for structured mutation (structured-family narrowers return `null` on opaque fallbacks). This guarantees the parser never silently drops information. Earlier drafts dropped unrecognized lines on the floor; v4 does not.
441
+
442
+ **Segment-preserving bodies (BUILD-18) — sequence ends the all-or-nothing cliff.** Sequence carries an ordered `statements: SequenceStatement[]` list. `alt|opt|loop|par` are typed fragments: their branches/messages participate in describe/facts/verify and have dedicated mutation ops. Other block constructs (`critical|break|rect|box … end`) and `Note`/`activate`/`deactivate`/`create`/`destroy`/`autonumber`/`title` remain VERBATIM opaque segments. Top-level message indices stay backward-compatible and distinct from fragment indices. Only an un-segmentable body (a stray `end`, an unclosed block) falls back to whole-body opaque. Gantt adopts the same pattern for calendar directives/click/comments. ER also uses ordered typed and opaque-block segments; Class and Timeline are the remaining follow-up work.
443
+
444
+ For any opaque fallback, cross-cutting edits are source-level only: operate against preserved source intentionally, then re-parse and verify before returning. Every renderable family now ships structured mutation; a new family follows the same pattern as its definition of done: narrowed type + body parser + serializer + per-family ops + verify hook + round-trip property tests + doc sync (most recently Mindmap and GitGraph). See `docs/contributing/adding-diagram-types.md`.
445
+
446
+ Convention bans constructing `ValidDiagram` outside `parseRegisteredMermaid`, `mutate`, `synthesizeFromGraph`, and `createMermaid`/`buildMermaid`.
447
+
448
+ ---
449
+
450
+ ## Public API
451
+
452
+ ```ts
453
+ parseRegisteredMermaid(source: string): Result<ValidDiagram, ParseError[]>
454
+ layoutMermaid(d: ValidDiagram): RenderedLayout
455
+ renderMermaidASCII(input: ValidDiagram | string, opts?): string
456
+ renderMermaidPNG(input: ValidDiagram | string, opts?): Uint8Array
457
+ renderMermaidSVG(input: ValidDiagram | string, opts?): string
458
+ verifyMermaid(input: ValidDiagram | string, opts?: VerifyOptions): VerifyResult
459
+ serializeMermaid(d: ValidDiagram): string
460
+ // ASCII opts.targetWidth is a hard display-cell bound; impossible bounds throw
461
+ // AsciiWidthError { code, requestedWidth, requiredWidth, family, reason }.
462
+
463
+ // Mutation is overloaded by family. Opaque/source-only families don't typecheck.
464
+ mutate(d: FlowchartValidDiagram, op: FlowchartMutationOp): Result<FlowchartValidDiagram, MutationError>
465
+ mutate(d: StateValidDiagram, op: StateMutationOp): Result<StateValidDiagram, MutationError>
466
+ mutate(d: SequenceValidDiagram, op: SequenceMutationOp): Result<SequenceValidDiagram, MutationError>
467
+ mutate(d: TimelineValidDiagram, op: TimelineMutationOp): Result<TimelineValidDiagram, MutationError>
468
+ mutate(d: ClassValidDiagram, op: ClassMutationOp): Result<ClassValidDiagram, MutationError>
469
+ mutate(d: ErValidDiagram, op: ErMutationOp): Result<ErValidDiagram, MutationError>
470
+ mutate(d: JourneyValidDiagram, op: JourneyMutationOp): Result<JourneyValidDiagram, MutationError>
471
+ mutate(d: ArchitectureValidDiagram, op: ArchitectureMutationOp): Result<ArchitectureValidDiagram, MutationError>
472
+ mutate(d: XyChartValidDiagram, op: XyChartMutationOp): Result<XyChartValidDiagram, MutationError>
473
+ mutate(d: PieValidDiagram, op: PieMutationOp): Result<PieValidDiagram, MutationError>
474
+ mutate(d: QuadrantValidDiagram, op: QuadrantMutationOp): Result<QuadrantValidDiagram, MutationError>
475
+ mutate(d: GanttValidDiagram, op: GanttMutationOp): Result<GanttValidDiagram, MutationError>
476
+ mutate(d: MindmapValidDiagram, op: MindmapMutationOp): Result<MindmapValidDiagram, MutationError>
477
+ mutate(d: GitGraphValidDiagram, op: GitGraphMutationOp): Result<GitGraphValidDiagram, MutationError>
478
+ mutate(d: RadarValidDiagram, op: RadarMutationOp): Result<RadarValidDiagram, MutationError>
479
+
480
+ // Narrowing helpers; null when the diagram isn't of that family or is opaque/source-level.
481
+ asFlowchart(d: ValidDiagram): FlowchartValidDiagram | null
482
+ asState(d: ValidDiagram): StateValidDiagram | null
483
+ asSequence(d: ValidDiagram): SequenceValidDiagram | null
484
+ asTimeline(d: ValidDiagram): TimelineValidDiagram | null
485
+ asClass(d: ValidDiagram): ClassValidDiagram | null
486
+ asEr(d: ValidDiagram): ErValidDiagram | null
487
+ asJourney(d: ValidDiagram): JourneyValidDiagram | null
488
+ asArchitecture(d: ValidDiagram): ArchitectureValidDiagram | null
489
+ asXyChart(d: ValidDiagram): XyChartValidDiagram | null
490
+ asPie(d: ValidDiagram): PieValidDiagram | null
491
+ asQuadrant(d: ValidDiagram): QuadrantValidDiagram | null
492
+ asGantt(d: ValidDiagram): GanttValidDiagram | null
493
+ asMindmap(d: ValidDiagram): MindmapValidDiagram | null
494
+ asGitGraph(d: ValidDiagram): GitGraphValidDiagram | null
495
+ asRadar(d: ValidDiagram): RadarValidDiagram | null
496
+
497
+ // Build a ValidDiagram from a JSON-safe graph payload without re-parsing
498
+ // source. Used by `am parse | am serialize` shell pipelines.
499
+ synthesizeFromGraph(payload: ValidDiagramPayload): Result<ValidDiagram, ParseError[]>
500
+
501
+ // Blank-slate authoring on the typed path: an empty structured diagram for
502
+ // any built-in family (overloads narrow the return per kind), and a fold of
503
+ // typed ops over that empty diagram. Errors carry the failing op's index.
504
+ createMermaid(kind: DiagramKind, opts?: { direction?: Direction }): MutableValidDiagram
505
+ buildMermaid(kind: DiagramKind, ops: AnyMutationOp[], opts?: { direction?: Direction }): Result<MutableValidDiagram, MutationError & { opIndex: number }>
506
+
507
+ // VerifyOptions carries the only real knob: the label character cap.
508
+ interface VerifyOptions { suppress?: WarningCode[]; labelCharCap?: number } // default cap 40
509
+ ```
510
+
511
+ There is no `LayoutContext`, no `SeededRNG`, no `Clock`, no font-metric table in the public surface. Those existed to support a seed apparatus that did nothing (see § (1)). The only verification knob is `labelCharCap`.
512
+
513
+ **CLI** (`am <verb>`) with JSON where useful: `render`, `preview`, `verify`, `parse`, `serialize`, `mutate`, `format`, `describe`, `capabilities`, `batch`, `render-markdown`, `llms-txt`. `am capabilities --json` reports each family's `editPolicy` (`structured-when-narrowed` vs `source-level-only`) plus `mutationOps`, so agents can route without trial-and-error. Plus `am --agent-instructions` printing the canonical agent-use guide embedded in the binary at build time — agents read the doc that ships with the tool, not whatever their training set indexed. The CLI's role is one-shot operations, shell-only contexts (CI, Bash-tool agents), and human inspection; multi-step editing belongs in the library or Code Mode, not in shell pipelines.
514
+
515
+ ---
516
+
517
+ ## Compatibility choices from the Beautiful Mermaid foundation
518
+
519
+ - **Agent surface exposed via the `./agent` subpath export** on the `agentic-mermaid` package. Agentic Mermaid is the product/docs name, npm identity, and repository path (`adewale/agentic-mermaid`). The subpath export keeps renderer and agent surfaces side by side.
520
+ - **Deterministic layout, verified.** Layout JSON is byte-identical across processes because ELK is configured for model-order layout with no random seed (see § (1)). No seed parameter is exposed because none affects output. Cross-machine byte equality across different CPU float behavior is not claimed; structural determinism within an ELK version is guaranteed and tested cross-process.
521
+ - **IDs are content-hashed and stable** across runs (within an ELK version).
522
+ - **`MermaidGraph` is kept** as an exported type — `ValidDiagram` wraps it in `body.graph` for flowchart, rather than replacing it. The original spec called for removal; characterization showed it would break existing tests and the Craft Agents consumer. The wrapping shape costs nothing.
523
+ - **`renderMermaidSVGAsync` is kept**. Removing it was a v1-aspiration that turned out to break consumers for no win at the agent surface.
524
+ - **`mermaidConfig` precedence** is source frontmatter < init < explicit render options for supported family fields. Normalization merges source wrappers; family resolvers then reapply the original explicit section last. State has direct public-path precedence and geometry probes in `state-config.test.ts`.
525
+
526
+ ---
527
+
528
+ ## Distribution
529
+
530
+ Five artifacts, all derived from this doc:
531
+
532
+ - **npm package** `agentic-mermaid` with the `agentic-mermaid/agent` subpath. The full TypeScript API, including ASCII, PNG, and SVG output helpers. Agents with shell access import the library directly and compose verbs in their own JS/TS runtime; no MCP wrapper required.
533
+ - **`skills/agentic-mermaid-diagram-workflow/`** agent-agnostic skill bundle. Master `SKILL.md` routes by *both* diagram family and composition channel: it picks Code Mode when the MCP is connected, library import when the agent can run JS/TS with imports, the CLI for shell-only contexts. Per-family references (`flowchart.md`, `sequence.md`, etc.) describe syntax. Two channel references — `code-mode.md` (the canonical multi-step pattern) and `cli.md` (shell-only) — describe composition. Progressive disclosure means the LLM loads only what it needs. Family references sync from upstream Mermaid docs weekly via the shipped GitHub Action at `.github/workflows/sync-mermaid-docs.yml`, alongside our additions (LayoutWarning codes, MutationOp taxonomy).
534
+ - **Substrate grep-lint** runs under `bun test` (not an uninstalled ESLint): `src/__tests__/agent-substrate-lint.test.ts` fails the build if `Math.random`, `Date.now`, or `performance.now` appear in `src/agent/**` or `src/layout-engine.ts`. This is real enforcement, executed in CI, not an aspirational config file.
535
+ - **`agentic-mermaid-mcp`** Code Mode-style MCP server. The primary tool is `execute(code: string)`: the model writes JavaScript against the typed `mermaid.*` SDK declaration embedded in the system prompt. Local stdio/HTTP runs synchronous code in a `node:vm` sandbox. The hosted `/mcp` endpoint runs the same hardened facade in per-request Cloudflare Dynamic Worker isolates with bounded CPU/subrequests and no outbound network. Pure render/verify/describe helpers share transport-neutral behavior. Differential tests pin the common contract and explicitly name strict-module and CPU-time divergences; neither runtime depends on `@cloudflare/codemode`.
536
+ - **`Instructions_for_agents.md`** at repo root, hard-capped under 100 lines. `am --agent-instructions` prints the same content at runtime; a doc-sync test asserts the two are byte-identical.
537
+
538
+ No HTTP endpoint or editor WebSocket watch in v1. The skill teaches Code Mode for both paths: agents-with-shell write JS/TS against the imported library; agents-without-shell write JavaScript against the MCP's `mermaid.*` SDK. Same surface in both cases.
539
+
540
+ ---
541
+
542
+ ## Agent-contract verbs (CLI)
543
+
544
+ Agent-contract CLI verbs for explicit self-discovery, summaries, and batch operation:
545
+
546
+ - `am capabilities [--json]` — emit `{ sdkVersion, families: [{ id, editPolicy, hasParse, hasSerialize, hasMutate, hasVerify, hasExtractLabels, mutationOps }], warningCodes: [{ code, tier, severity }], outputFormats: ["svg","ascii","unicode","png","json"] }`. Sourced from the public dispatch surface, family-plugin registry, and `WARNING_SEVERITY` / `WARNING_TIER` tables — so the contract is self-describing, not hand-maintained. A JSON Schema is committed at `src/__tests__/__fixtures__/capabilities.schema.json`; any shape drift fails the test loudly.
547
+ - `am batch --jsonl` — read JSONL from stdin, dispatch per-line to render/verify/parse/serialize/mutate handlers, emit one JSON envelope per result. Malformed lines surface `{ ok: false, error: { code: 'INVALID_JSON' } }` and do **not** abort the stream.
548
+ - `am preview <file|-> [--output file.html] [--open] [--json] [--security strict]` — write a standalone strict-mode HTML preview for human inspection without hand-building wrapper files.
549
+ - `am mutate <file|-> (--op '<json>' | --ops '<json array|file>') [--json]` — apply typed mutations, verify once at the commit point, and omit source on verify failure.
550
+ - `am describe <file|-> [--format text|json] [--json]` — emit a prose summary or structured AX tree (`{kind,nodes,edges,entryPoints,sinks}`) for screen readers, doc generation, and agent context compaction.
551
+ - **Exit codes** are widened to 4: `EXIT_OK=0`, `EXIT_ARG_ERROR=2`, `EXIT_VERIFY_FAILED=3`, `EXIT_INTERNAL=4` (in `src/cli/exit-codes.ts`). The CLI was previously `0` or `2` only. `EXIT_VERIFY_FAILED=3` is the new code for "valid args, but the diagram failed verify" — important for agents wrapping `am verify` in batch.
552
+
553
+ **Counter-example, documented.** manuareraa PR #42 on `lukilabs/beautiful-mermaid` ships an MCP server with 4 render-only tools (`render_svg` / `render_ascii` / `list_themes` / `parse`). We rejected this design: a render-tool-per-format MCP forces the agent to chain calls and loses ValidDiagram context. Our Code Mode design keeps one primary `execute()` surface for multi-step edits; `render_png` and `describe` are narrow helpers, not a render-tool-per-format API. PR #42 is preserved here as the documented counter-example so future contributors understand why we chose Code Mode.
554
+
555
+ ---
556
+
557
+ ## Agent workflow
558
+
559
+ The canonical runtime guide lives in `Instructions_for_agents.md` and is emitted byte-for-byte by `am --agent-instructions`; this spec intentionally does not duplicate the full snippet. The stable contract is:
560
+
561
+ 1. For new diagrams, `buildMermaid(kind, ops)` — or `createMermaid(kind)` then typed mutations — then `verifyMermaid` / render or return it. Author Mermaid source directly only for syntax the typed ops do not model.
562
+ 2. For existing diagrams, `parseRegisteredMermaid(source)` → `ValidDiagram`.
563
+ 3. Use the family entry's registry-advertised narrower; `null` means no structured mutation for that body.
564
+ 4. Apply typed `mutate` ops only to narrowed mutable bodies; Code Mode SDK-returned diagrams are read-only to block direct IR edits.
565
+ 5. Run `verifyMermaid(d)` at every commit point and inspect `ok` / `warnings` / `layout`.
566
+ 6. Only then `serializeMermaid(d)`.
567
+
568
+ Anti-patterns: whole-source regeneration of an existing parsed diagram, string concatenation to edit an existing structured diagram where a typed op exists, serializing before inspecting verify, and calling `mutate` on opaque/source-only bodies.
569
+
570
+ ---
571
+
572
+ ## Release discipline
573
+
574
+ This section records the design discipline for the branch, not an active roadmap. Active work lives only in `TODO.md`.
575
+
576
+ | Principle | Meaning |
577
+ |---|---|
578
+ | **Ship** | Keep the minimum lethal surface together: substrate + `verify` + `mutate` + `serialize` + typed MutationOps + CLI + skill + Code Mode MCP + `Instructions_for_agents.md`. |
579
+ | **Learn** | Use MermaidSeqBench, stored Code Mode evals, live model transcripts, and real consumers to decide what is missing. |
580
+ | **Expand by evidence** | Promote more MutationOps, composition primitives, HTTP/SSE MCP transport, or additional structured families only when evidence justifies them. |
581
+
582
+ ---
583
+
584
+ ## Measurement
585
+
586
+ | What | How | Target |
587
+ |---|---|---|
588
+ | Layout JSON byte-equality across runs | Determinism grid (4 directions × node-counts 2..12 × {sparse, dense, star}) | 100% within one ELK version on one machine |
589
+ | Drift sentinel | 8 hand-picked canonical layout JSONs as snapshots; any change without explicit acknowledgment fails CI | — |
590
+ | Cross-process determinism | Test spawns child `bun` processes; layout JSON byte-identical across them | 100% |
591
+ | Grep-lint substrate | Test fails if `Math.random`/`Date.now`/`performance.now` appear in `src/agent/**` or `src/layout-engine.ts` | 0 hits |
592
+ | Tier-1 verifier recall on broken-fixture cases | Inline tests per Tier-1 code | high |
593
+ | Round-trip identity | Golden corpus + property test | 100% on canonical input |
594
+ | Round-trip property | Property test (fast-check) | 100% on parseable input |
595
+ | Sequence fidelity | Property test (BUILD-18): any sequence source is segments-or-opaque and always lossless — interleaved structured + opaque lines round-trip in order; `remove_message` never touches opaque-block bytes | 100% lossless |
596
+ | Sad-path coverage | CLI mutate on opaque family; malformed JSON-RPC; broken Code Mode script; N-round format idempotence | explicit tests |
597
+ | Fault-injection (poor-man's mutation testing) | Inject a known bug into each core function, confirm a test catches it, revert | every core fn covered |
598
+ | Code Mode sandbox isolation | Tests assert `execute()` cannot reach `process`, `require`, `fetch`, `eval`, `Function`, or host-constructor escape paths; dynamic code generation is disabled | explicit tests |
599
+
600
+ Cross-cutting:
601
+
602
+ - **Doc-sync** (both directions): every `LayoutWarning` code and `MutationOp` kind must appear in this spec. `am --agent-instructions` output must equal `Instructions_for_agents.md` byte-for-byte.
603
+ - **Test honesty:** no tautological assertions (`expect(typeof x).toBe('boolean')` is banned by review). Every test must be able to fail for the regression it names. Verified by the fault-injection pass.
604
+
605
+ MermaidSeqBench is wired as an external corpus signal; live model transcript evaluation remains periodic / pre-release rather than PR CI.
606
+
607
+ ---
608
+
609
+ ## Risks (honest, v4)
610
+
611
+ - **Determinism is empirical, not proven.** It's established by cross-process test over a corpus + the drift sentinel, plus reading ELK's config (`considerModelOrder: NODES_AND_EDGES`, no `randomSeed`). An ELK upgrade could in principle change this; the cross-process test and sentinel would catch it. There is no layout seed to fall back on because seeding never affected geometry. (The render option `seed` is a *style* seed: it re-rolls the ink wobble of styled looks and never moves layout.)
612
+ - **Determinism claim, precisely.** Layout JSON is byte-identical (after structural parse) across processes AND across JS runtimes (bun, node) on the same machine and same ELK version; this is verified on same-machine x86_64 and ARM64 when Node + built `dist/` are present. Direct cross-architecture byte equality (x86_64 output compared to ARM64 output) is still not a separate claim.
613
+ - **Sequence structured coverage is segment-preserving (BUILD-18).** Participant declarations, top-level messages, and direct-message `alt`/`opt`/`loop`/`par` fragments are typed; fragment messages participate in describe/facts/verify and dedicated mutation ops. Note/critical/break/rect/box/activate/autonumber/title and nested or otherwise unmodeled fragment content ride along as verbatim opaque-block *segments*, so the structured ops survive instead of going whole-body opaque. Messages inside those remaining opaque blocks are deliberately not modeled. Only un-segmentable input (unbalanced `end`, unclosed block) falls back to whole-body opaque. The honest tradeoff is unchanged — never lossy — it just no longer sacrifices the structured ops at the first unmodeled line. ER also has ordered typed and opaque-block segments; Class and Timeline are the remaining follow-up work.
614
+ - **Every registered renderable family ships structured mutation; opaque fallbacks stay source-level only.** A typed narrower exists for every family kind; only opaque-fallback bodies for unmodeled syntax remain deliberate, lossless source-level paths.
615
+ - **Live-model agent-usage eval is periodic, not PR CI.** Stored Code Mode scripts, sandbox traces, task oracles, and the committed pi-subagent transcript replay run in CI; API-backed release-model transcripts remain in `TODO.md` because they need model access and selected release tasks.
616
+ - **Bloat in agent-facing docs.** `Instructions_for_agents.md` is hard-capped under 100 lines; doc-sync test enforces.
617
+
618
+ ## What v4 delivers (vs. earlier drafts)
619
+
620
+ - **Determinism is structural and verified cross-process** — not a seed apparatus. The seed/RNG/clock machinery and the font-metric table are *removed* (they did nothing).
621
+ - **Mutation for every registered renderable family**, with family-narrowed overloads and compile-time rejection of opaque/source-only fallback bodies.
622
+ - **Sequence parsing is lossless** — segment-preserving structured body (BUILD-18): Note/alt/loop/etc. ride along verbatim as opaque-block segments while the structured ops stay live; only un-segmentable input falls back to whole-body opaque; never silently drops constructs.
623
+ - **Substrate enforcement is a real grep test** that runs under `bun test`, not an ESLint config that was never installed.
624
+ - **`synthesizeFromGraph`** lets `am parse | am serialize` round-trip without `canonicalSource` on the wire.
625
+ - **`LABEL_OVERFLOW` is a rendered-line char-count check** (Tier 1, reliable), not a font-metric heuristic: the cap applies to the longest displayed line (XML entities decode, `<br>` splits lines, formatting tags strip), not raw source chars.
626
+ - **`Finite` branded type** enforced at every coordinate emission.
627
+ - **Deliverable completeness:** CHANGELOG entry, README section, an `examples/` script, per-verb CLI `--help`, and a [`docs/fork-differences.md`](./docs/fork-differences.md) mention all ship with the code.
628
+ - **Test honesty:** the tautological seed-variance test is gone; a fault-injection pass proves the suite has teeth.
629
+
630
+ ---
631
+
632
+ ## What Code Mode unlocks
633
+
634
+ The Code Mode MCP raises the ceiling on what an agent can do with the library in one call. We initially framed the agent surface as a fixed set of verbs (parse, verify, mutate, serialize) where each verb is one round-trip. Code Mode reframes the surface as a typed library that the agent composes against — and the only thing we ship is the small set of primitives plus a sandbox. The agent supplies the algorithm.
635
+
636
+ Concrete consequences, in roughly descending impact:
637
+
638
+ 1. **Composition without shipping composition.** `@include`, `@template`, vars/`${}` are deferred indefinitely. An agent can implement its own splice or template flavor in JavaScript over source strings and SDK-returned parsed diagrams — parse trusted inputs, use supported `mutate` operations, verify, serialize — in one round-trip. Code Mode does not bless hand-fabricated `ValidDiagram` clones; structured edits stay on SDK lineage.
639
+ 2. **Multi-diagram repo operations.** "Rename `AuthService` across every architecture diagram in this repo, verify each, and report which now have warnings." With verb-per-tool MCP that's `N × 3` round-trips. With Code Mode: host/agent code supplies the file list and source strings, and one `execute()` can process them together. The sandbox itself does not expose filesystem access.
640
+ 3. **Auto-fix loops in one round-trip.** `verify` → identify mechanically fixable warnings → apply fixes → `verify` again, as a `while (!result.ok)` block. The agent only sees the final state plus a structured audit trail.
641
+ 4. **Diagram-as-tests / CI gate.** A repo installs `agentic-mermaid`, runs the `agentic-mermaid-mcp` binary, and writes a Code Mode snippet that verifies every `.mmd` on push. Diagrams become test artifacts that fail CI when they break.
642
+ 5. **No need to ship `diffDiagrams` or `explainDiagram`.** Already cut. Code Mode confirms the cut: an agent that wants structural diff writes it from `parse` + `ValidDiagram` inspection in JavaScript. Every "would be nice to have a verb for" becomes "write the code for it in `execute()`."
643
+ 6. **Library as the cross-tool agent interface for diagrams.** A Mermaid linter, a `mermaid → d2` converter, a `graphviz → mermaid` importer can each expose the same Code Mode shape. Any agent then writes one JavaScript snippet that composes across libraries. We've effectively defined the agent interface for diagrams in this language.
644
+ 7. **Benchmark eval at speed.** MermaidSeqBench (and any future eval) runs as one `execute()` per case rather than N round-trips. Internal velocity multiplier.
645
+ 8. **Hosted Worker path.** The shipped `/mcp` endpoint uses Cloudflare Dynamic Worker isolates through the platform loader binding, not `@cloudflare/codemode`. The runtime has explicit security/resource limits and differential parity with local Code Mode; remaining WAF and abuse-control operations are tracked separately in `TODO.md`.
646
+ 9. **The skill becomes runnable, not just descriptive.** `references/code-mode.md` ships canonical executable JavaScript snippets the agent copy-pastes into `execute()`. Skill stops being prose; starts being a library of executable patterns.
647
+ 10. **A future diagram REPL would be a thin transport.** An `am repl` could become an interactive Code Mode shell — paste JavaScript, get structured results, iterate. Same sandbox, different transport. It is not shipped and would need promotion to `TODO.md` before implementation.
648
+
649
+ The biggest single consequence is #1: it gives us permission to never grow the spec for composition, queries, diffing, explaining, or any "we should probably have a verb for that" feature. **The verb set is intentionally small; Code Mode makes it sufficient.**
650
+
651
+ ### Counter-example: why we did not ship a `render-tool-per-format` MCP
652
+
653
+ Upstream [manuareraa PR #42](https://github.com/lukilabs/beautiful-mermaid/pull/42) takes the opposite design: an MCP server with one tool per output format (`render_svg`, `render_ascii`, `render_png`, etc.), each pinned to a specific renderer call. We considered cloning that surface and decided against it for two reasons. First, the verb explosion is unbounded — every new output format means a new tool, every option flag means a new tool variant, and every cross-cutting workflow (parse → verify → render → write back) requires the agent to glue tools together via the host's tool-call protocol, multiplying round-trips. Second, the per-tool typing pushes structure into the tool schema where the agent can only see what the schema declares; it can't introspect `ValidDiagram`, can't compose `mutate` with `verify`, and can't write the algorithm that uses the answer. Code Mode inverts both: one `execute` tool with a typed `mermaid.*` SDK declaration in scope. Read-only inspection, cross-diagram lookups, and custom transforms are ordinary JavaScript inside that execution context rather than additional MCP tools. Anything PR #42's surface could do — render-to-format with options — is one line of JavaScript inside `execute`; anything our surface can do that PR #42's can't includes every multi-step workflow that touches more than one verb. The asymmetry justifies the cut.
654
+
655
+ ---
656
+
657
+ ## Why totality matters
658
+
659
+ Each property alone leaves a gap. Determinism alone — agents can diff outputs but can't edit without regenerating. Round-trip alone — every edit shuffles the layout, drowning the signal. Verify alone — outputs aren't reproducible across runs, so a warning fixed in one run silently returns in the next.
660
+
661
+ Together they close a loop:
662
+
663
+ 1. Parse the source into `ValidDiagram`.
664
+ 2. `mutate` one node — the IR guarantees the edit produces valid Mermaid.
665
+ 3. `verify` — structured warnings tell the agent whether anything broke.
666
+ 4. If broken, back up to the previous `ValidDiagram` and try a different op.
667
+ 5. `serializeMermaid` only after verify passes.
668
+
669
+ That loop is the agent-native claim. Replacing several rounds of vision-on-PNG with one structured verification pass is what makes a coding agent reach for this fork before any other Mermaid library.
670
+
671
+ The API makes the loop possible. `Instructions_for_agents.md` and `am --agent-instructions` make it a habit. Both ship together.