agentic-mermaid 0.1.2 → 0.2.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 (52) hide show
  1. package/AGENT_NATIVE.md +671 -0
  2. package/CHANGELOG.md +19 -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/{chunk-QKDLMOJJ.js → chunk-2P5FKUQC.js} +7 -9
  13. package/dist/{chunk-WN3YDOBG.js → chunk-2S3VSS6I.js} +4 -4
  14. package/dist/{chunk-CDPKJ2BQ.js → chunk-2SILJDW2.js} +2 -2
  15. package/dist/{chunk-ZYMMKQ3I.js → chunk-BJYMBT4I.js} +299 -973
  16. package/dist/{chunk-WI35L7IR.js → chunk-FDM43XU7.js} +2 -2
  17. package/dist/{chunk-4QECACIA.js → chunk-MYPDVZCO.js} +19 -19
  18. package/dist/{index-D54dqSdC.d.ts → index-DWPa2MJs.d.ts} +7 -4
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/mcp-cli-5J47WEY3.js +17 -0
  22. package/docs/README.md +34 -34
  23. package/docs/agent-api-cookbook.md +317 -0
  24. package/docs/agent-mutation-policy.md +104 -0
  25. package/docs/agent-workflow-examples.md +46 -0
  26. package/docs/api.md +418 -0
  27. package/docs/ascii.md +113 -0
  28. package/docs/code-mode-impact.md +214 -0
  29. package/docs/comparison.md +106 -0
  30. package/docs/computational-aesthetics-prototype-plan.md +252 -0
  31. package/docs/config.md +121 -0
  32. package/docs/custom-fonts.md +265 -0
  33. package/docs/custom-style-cookbook.md +275 -0
  34. package/docs/diagram-families.md +218 -0
  35. package/docs/features.md +154 -0
  36. package/docs/fork-differences.md +133 -0
  37. package/docs/getting-started.md +182 -0
  38. package/docs/issue-derived-test-cases.md +163 -0
  39. package/docs/mcp-code-mode-rationale.md +108 -0
  40. package/docs/mcp-http-transport.md +230 -0
  41. package/docs/mermaid-layout-complaints.md +851 -0
  42. package/docs/mutation-testing.md +285 -0
  43. package/docs/quality.md +224 -0
  44. package/docs/react.md +97 -0
  45. package/docs/style-authoring.md +336 -0
  46. package/docs/svg-semantic-contract.md +49 -0
  47. package/docs/testing-strategy.md +443 -0
  48. package/docs/theming.md +149 -0
  49. package/package.json +21 -6
  50. package/server.json +2 -2
  51. package/skills/agentic-mermaid-live-editor/SKILL.md +1 -1
  52. package/dist/mcp-cli-SBQSTJFC.js +0 -17
@@ -0,0 +1,285 @@
1
+ # Mutation testing
2
+
3
+ The original ASCII Stryker lane is scoped to the five layout modules where a
4
+ silent logic regression is most expensive: `src/ascii/pathfinder.ts`,
5
+ `src/ascii/edge-routing.ts`, `src/ascii/converter.ts`, `src/ascii/grid.ts`,
6
+ `src/ascii/draw.ts`.
7
+ The `ascii` profile in the single `stryker.config.mjs` authority executes the
8
+ ASCII test files (goldens, invariants, properties, unit tests) per mutant.
9
+
10
+ ## Running
11
+
12
+ ```bash
13
+ # One module (preferred — a full module takes 5–15 minutes):
14
+ bun run mutation-test -- ascii --mutate 'src/ascii/pathfinder.ts'
15
+
16
+ # All five:
17
+ bun run mutation-test -- ascii
18
+
19
+ # Discover every available profile:
20
+ bun run mutation-test -- --list
21
+ ```
22
+
23
+ (`bun run mutation-test -- families` covers the
24
+ shared Architecture, XYChart, Mindmap, GitGraph, and Radar parser/layout/renderer
25
+ cores plus Radar's structured editing body, and `bun run mutation-test -- routes` covers
26
+ the route-contracts module
27
+ (`docs/design/system/route-contracts.md`), all with the same policy.)
28
+
29
+ Narrow lanes for PR-scale survivor harvests:
30
+
31
+ ```bash
32
+ bun run mutation-test -- links # text-embedded link-length parsing
33
+ bun run mutation-test -- routes:certs # route-certificate finality + stale-route audit
34
+ bun run mutation-test -- routes:subgraph # subgraph endpoint/LCA routing
35
+ bun run sabotage:routes # one-line revert checks against committed HEAD; expects focused tests to fail
36
+ ```
37
+
38
+ JSON reports land in `reports/mutation/` (gitignored). The single executable
39
+ config resolves route-certificate, subgraph-routing, and link-grammar ranges
40
+ from source-adjacent markers; inserting code above the behavior cannot silently
41
+ move those lanes onto unrelated lines.
42
+
43
+ Beyond the lanes documented here, configs exist for every built-in renderable
44
+ family through named profiles: flowchart uses `mutation-test -- routes`;
45
+ XYChart, Architecture, and Radar share `mutation-test -- families`; State, Sequence,
46
+ Timeline, Class, ER, Journey, Pie, Quadrant, Gantt, Mindmap, and GitGraph each
47
+ have a focused command. These are **opt-in diagnostic survivor harvests**. They
48
+ emit scores and JSON reports, but have no `thresholds.break` and are neither
49
+ scheduled nor acceptance gates.
50
+
51
+ Two bounded checks run automatically on each PR:
52
+
53
+ - `mutation-test -- incremental` mutates one small, pure module in about one minute
54
+ and enforces its measured score.
55
+ - `sabotage:routes` injects five named one-line regressions and requires the
56
+ focused behavioral tests to fail.
57
+
58
+ This distinction is deliberate. Mutation testing is useful when it challenges
59
+ a specific test or design assumption; family membership alone is not a reason
60
+ to enroll code in an expensive recurring sweep. A local mutation score without
61
+ a retained report is diagnostic evidence, not a repository acceptance claim.
62
+
63
+ ## Why broad scheduled mutation was retired (2026-07-14)
64
+
65
+ The nightly workflow, its sharder, mutant-set oracle, aggregate verifier, and
66
+ orchestration tests were retired and deleted. The operational evidence did
67
+ not support maintaining them:
68
+
69
+ - all 26 scheduled runs from 19 June through 14 July failed or were cancelled;
70
+ - the workflow expanded from four route checks to every family while it was
71
+ still unable to produce a successful retained run;
72
+ - the final repair grew to 39 coverage workers (41 jobs total), yet long lanes
73
+ still timed out and hosted runners disappeared before producing reports; and
74
+ - rerunning unchanged mutation-relevant code consumed runner-hours without
75
+ adding information.
76
+
77
+ The [last scheduled baseline](https://github.com/adewale/agentic-mermaid/actions/runs/29309167850),
78
+ [calibration diagnostic](https://github.com/adewale/agentic-mermaid/actions/runs/29327418603),
79
+ and [cancelled 41-job repair](https://github.com/adewale/agentic-mermaid/actions/runs/29340333016)
80
+ are retained as historical evidence, not as acceptance authority. The focused
81
+ configs, marker-based scopes, survivor classifications, incremental gate, and
82
+ sabotage probes remain because they are independently useful.
83
+
84
+ The causal record is in
85
+ [`project/archive/mutation-infrastructure-postmortem-2026-07.md`](./project/archive/mutation-infrastructure-postmortem-2026-07.md).
86
+ The durable stop rule is: after three consecutive scheduled failures, an owner
87
+ must fix, narrow, disable, or delete the diagnostic before adding scope. Any new
88
+ recurring lane also needs a demonstrated fault class, one complete retained
89
+ baseline, a runner-minute budget, an owner, and a removal criterion. No single
90
+ proxy—coverage, mutation score, assertion count, or matrix enrollment—is “the”
91
+ adequacy signal.
92
+
93
+ ## Focused Mindmap/GitGraph historical local measurement (2026-07-10)
94
+
95
+ These figures came from local runs. Their JSON reports are gitignored and no
96
+ immutable CI artifact URL is committed, so they are diagnostic history—not a
97
+ PR acceptance gate or a current break floor.
98
+
99
+ | Lane | Mutants | Killed | Survived | Score |
100
+ |---|---:|---:|---:|---:|
101
+ | `bun run mutation-test -- mindmap` (`src/agent/mindmap-body.ts`) | 405 | 400 | 5 | **98.77%** |
102
+ | `bun run mutation-test -- gitgraph` (`src/agent/gitgraph-body.ts`) | 303 | 294 | 9 | **97.03%** |
103
+
104
+ The operation suites exercise every happy path, validation branch, source-order
105
+ rewrite, recursive/cycle guard, null-clearing path, and verification warning.
106
+ Mindmap's five survivors are two correlated node-syntax tuple guards plus
107
+ three exception-path mutations around the node/body stability probes (two
108
+ empty `catch` forms remain falsy; one `false`→`true` mutation is retained as a
109
+ non-gating test gap). GitGraph's nine
110
+ survivors are canonical serialization equivalents (trimming an already
111
+ canonical header, explicit `NORMAL`, and optional attribute whitespace),
112
+ merge-field guards implied by the discriminated model, or widened
113
+ statement-kind guards that serialize identically on valid replay state. Reports
114
+ are generated at `reports/mutation/{mindmap,gitgraph}-mutation.json`
115
+ (gitignored); the committed configs make the runs reproducible. The measured
116
+ scores above are not acceptance evidence: any separately owned acceptance claim
117
+ must cite retained CI artifacts or a content-addressed report.
118
+
119
+ ## Policy
120
+
121
+ A SURVIVED mutant is a test gap until shown otherwise. Either kill it with a
122
+ test (and sabotage-verify the test), or classify it below with a reason.
123
+ Don't chase 100%: performance-guard and unreachable-branch mutants are
124
+ documented, not killed with synthetic inputs.
125
+
126
+ ## Baseline (June 2026, first scoped run)
127
+
128
+ | File | Score | Killed+timeout | Survived |
129
+ | --- | --- | --- | --- |
130
+ | pathfinder.ts | 60.3% → 70.8% after `ascii-pathfinder-units.test.ts` | 184 → 216 | 121 → 89 |
131
+ | edge-routing.ts | 55.1% | 163 | 133 |
132
+ | converter.ts | 38.4% | 94 | 151 |
133
+ | grid.ts | 70.1% | 421 | 180 |
134
+ | layout/passes/index.ts:931-1165 (linkrank packing repair) | 49.86% → 65.07% after the geometry-characterization harvest (355 mutants; killed 176 → 230+1 timeout). The +54 kills were the repair's candidate ORDERING / ahead-node selection / push-delta arithmetic — invisible to HARD-metric assertions (a mutant picking a different clear lane still yields zero violations), pinned instead by per-repro geometry snapshots in `linkrank-packing.test.ts` (layout determinism makes them stable). | 355 | 124 — see classification below |
135
+ | route-contracts.ts | 58.1% → 72.3% → 75.6% → 73.1% across the first three batches; 54.31% at 0b43c90, then 50.69% at 526d9cf after the slanted-family batch (shapePorts generalization, convex-polygon geometry: 2659 → 2740 mutants — absolute kills 1444 → 1381). The percentage fell on denominator growth (~3× since batch 3); residuals from that run are classified below. | 882 → 2740 | 50.69% (1381 killed) — historical snapshot |
136
+
137
+ (Numbers from the June 2026 runs; regenerate rather than trusting this
138
+ table — the report lands in `reports/mutation/`.)
139
+
140
+ ## Historical survivor classification
141
+
142
+ This section explains the recorded run; it is not a secondary test backlog.
143
+ Only root `TODO.md` can schedule a new fixture or survivor-harvest package.
144
+
145
+ **linkrank lane residual (124 at the 65.07% run)**, classified:
146
+
147
+ - **Unexercised branch in the recorded corpus**: the subgraph-scoped
148
+ `separationUnit` branch (walk the overlapped node's scope chain, push its
149
+ outermost subgraph unit) did not fire because the degenerate repro set had no
150
+ subgraphs. A subgraph + long-link shove-overlap fixture was not part of that run.
151
+ - **Unexercised defensive fallbacks**: the `rungSource` feedback family and some
152
+ min-lane branches fire only on inputs the deterministic enumeration no longer
153
+ produces (0 residual signatures); every adopted route is still runtime-validated by
154
+ `routeClearOfNodes`, so a wrong fallback cannot ship a through-node route.
155
+ - **Equivalent on the real input domain**: exact-tie tiebreaks (`flowGap !== 0` id
156
+ fallback), the `pairKey` separator literal (any injective separator), the defensive
157
+ round cap (`nodes.length * 4` — forward-only motion terminates before it), and the
158
+ candidate-list `slice(0, 16)` caps (lists are shorter in practice).
159
+
160
+ **Killed by `src/__tests__/ascii-pathfinder-units.test.ts`** (32 mutants):
161
+ `buildMoveDirs` preferred-direction ordering (it only decides ties between
162
+ equal-cost A* paths, which the golden corpus never isolates; the unit tests
163
+ pin `path[1]` per direction), `heuristic` corner-penalty logic, and
164
+ `mergePath` straight-run detection (backtracks and mixed-delta steps must
165
+ be preserved).
166
+
167
+ **Accepted — performance guards, not behavior** (pathfinder): the #66
168
+ bounded-search mutants (`next.x > boundX || next.y > boundY` weakenings,
169
+ `maxIterations` arithmetic). They change how much work an unreachable-target
170
+ search does before returning null, not what any reachable search returns.
171
+ Timing the search was judged less reliable than the risk warranted.
172
+
173
+ **Accepted — unreachable via convention** (pathfinder): the `dy`-dominant
174
+ fallback in `buildMoveDirs` (lines ~157–159). All nine 3×3 `Direction`
175
+ constants either match an explicit cardinal branch or have `|dx| >= |dy|`,
176
+ so the branch only runs for synthetic Direction values. Candidate for
177
+ simplification, not for synthetic-input tests.
178
+
179
+ **Found dead code**: the first mutation run showed the audit-suggested sort
180
+ tie-breaks on the two `b.index - a.index` comparators in
181
+ `determineLabelLine` could never fire — segment `index` is unique by
182
+ construction — so they were reverted. Only the width-sort tie-break (where
183
+ ties are real) remains. Mutation testing earning its keep: it falsified an
184
+ audit assumption the test suite couldn't.
185
+
186
+ **Killed by `route-contracts.test.ts` survivor harvests** (170 mutants
187
+ across five passes): RL/BT axis orientation (reciprocal-pair regressions in
188
+ both reversed directions), every `directLaneBlockers` blocker kind at its
189
+ exact ±4px clearance boundary on both axes (non-square obstacles so a
190
+ swapped width/height changes the verdict), own-label lane capacity in
191
+ rendered-pill units, the same-edgeIndex self-exclusion, sub-epsilon
192
+ polyline simplification, the hitch deviation rounding, `findLabelSlot`
193
+ stagger and pill-boundary arithmetic, the primary-over-feedback exemption
194
+ asymmetry, the feedback path of `findRouteHitches`, shared-trunk detection
195
+ on both axes at its collinearity/pill-extent boundaries, the ±2px line
196
+ between the stale and shape-misanchor tripwires, container perimeter
197
+ tolerance, and the container repair's horizontal-gap axis selection.
198
+
199
+ **Accepted — equivalent on the real input domain** (route-contracts): the
200
+ collinearity cross-product variants in `simplifyPolyline` behave identically
201
+ for the orthogonal polylines ELK produces (several algebraic rearrangements
202
+ preserve the zero/non-zero verdict for axis-aligned triples), and the
203
+ `isMonotoneStaircase` diagonal/backward guards are defensive against passes
204
+ that do not currently exist — real extracted routes are orthogonal and the
205
+ backward case is already excluded by feedback classification upstream.
206
+
207
+ **Accepted — performance guards** (route-contracts): the `seen`-set and
208
+ stack mechanics inside `classifyRoutes`' DFS change traversal cost, not
209
+ reachability verdicts, on the small graphs flowcharts produce.
210
+
211
+ **Accepted — bounded-iteration guard** (route-contracts): the fixed-point
212
+ loop's `round < 4` weakenings only change how many re-prove rounds run;
213
+ the duplicate-edge regression pins one unblocking round, while three chained
214
+ rounds were outside the recorded real-input corpus.
215
+
216
+ **Residual observations from that run** (not scheduled):
217
+ - `directLaneBlockers` label-rect axis ternaries retain a few survivors on
218
+ rect width/height swaps for near-square pills; the corpus lacked a multi-line
219
+ (taller than wide) label obstacle.
220
+ - `findLabelSlot`'s rect-overlap conjunction retains sign-variant survivors
221
+ for rects that only graze on one axis; corner-touching fixtures were absent.
222
+ - The audit's pill-hit prefilter (`hitsPill`) retains inclusive-vs-strict
223
+ comparison survivors for segments that exactly graze the pill border;
224
+ they only widen the prefilter the collinearity check still gates.
225
+ - The container repair's reversed-order gaps (target above / left of source)
226
+ were absent; the recorded classification relied on symmetry with the two
227
+ covered directions.
228
+ - `grid.ts` `ensureSubgraphSpacing` (~48 survivors): the overlap/min-spacing
229
+ resolution between root subgraphs is never triggered by the corpus —
230
+ placement upstream appeared to avoid overlaps, so the run could not
231
+ distinguish reachable behavior from defensive dead code.
232
+ - `converter.ts` entry/exit attachment roles and phantom still-used checks
233
+ (`resolveSubgraphEdges` area): rendered-output invariants tolerate several
234
+ mutants because other mechanisms compensate; the run asserted rendered text,
235
+ not the intermediate resolved edge structure.
236
+ - `edge-routing.ts` label segment filter (`width >= lenLabel && index > 1 &&
237
+ orientationMatches`): the labeled-fanout invariants pin the TD vertical-drop
238
+ case; LR orientation and the index>1 exclusion lack direct coverage.
239
+
240
+ ## Incremental per-PR profile
241
+
242
+ A fast lane gates the small, pure faithfulness counter on every PR (the
243
+ `mutation-incremental` CI job), separate from the opt-in diagnostic configs. It
244
+ mutates `src/agent/structural-count.ts` (the counter + `faithfulnessWarning` +
245
+ `isDrop`), run by the sub-second `structural-count.test.ts` unit runner. A full
246
+ run is ~1 min, so it runs in full each PR.
247
+
248
+ The 2026-07-14 post-Radar run scored **97.01%** (130 killed / 134 valid; regenerate to
249
+ confirm, with the report in `reports/mutation/`), gated by
250
+ `thresholds.break: 90`. The retained floor deliberately remains below the
251
+ measured score; this result for one small pure module is not a repository policy
252
+ or a reason to expand mutation scope.
253
+
254
+ The survivor review found real valid-domain gaps rather than accepting a score:
255
+ the State counter needed both a doubly nested composite and concurrent-region
256
+ fixtures; ER needed a semantic-subgraph fixture and the public empty body with
257
+ its optional `groups` field absent; and `nodes = services + junctions` needed an
258
+ Architecture fixture containing a junction. These cases remain because they
259
+ pin real behavior, not because they improve a metric.
260
+
261
+ Four survivors are classified rather than killed with invalid inputs or by
262
+ weakening the code's forward-compatibility tripwire:
263
+
264
+ - two mutations to `case 'opaque': return null` are equivalent because the
265
+ exhaustive default also returns `null`; and
266
+ - two mutations to the `default` branch are unreachable for the valid
267
+ discriminated union. Its `const _never: never = body` assignment is retained
268
+ so adding a family without a count remains a compile-time error.
269
+
270
+ ### Why `verify.ts`'s faithfulness wrapper is NOT in the gated mutate set
271
+
272
+ Move 2 measured `roundtripFaithfulnessWarnings` (the I/O wrapper around the pure
273
+ `faithfulnessWarning`): **26.67% kill / 11 survivors**. The survivors are all on
274
+ the wrapper's defensive branches — `if (!reparsed.ok) …`, `if (!after) …`, and
275
+ the `catch` — which no *real* diagram exercises: a structured diagram's
276
+ serialization always re-parses, and re-parses structured (not opaque), and never
277
+ throws. Those branches are therefore unkillable through real `verifyMermaid`
278
+ inputs without a synthetic drop, and the *decision logic* they guard already
279
+ lives in `faithfulnessWarning`, which the incremental lane currently measures
280
+ at 97.01%. Adding the wrapper to the gated `mutate` would crater the score to
281
+ ~27% and force either
282
+ a meaningless break threshold or a pile of accepted survivors. So the wrapper
283
+ stays out of the gated set and is recorded here as I/O-glue equivalents; its
284
+ logic is covered where it actually lives, where the current measured score is
285
+ 97.01%.
@@ -0,0 +1,224 @@
1
+ # What "good looking" means in Agentic Mermaid
2
+
3
+ A diagram is considered **good looking** when it satisfies, in order:
4
+
5
+ 1. **All Tier 1 structural checks pass.** No `EMPTY_DIAGRAM`, no
6
+ `EDGE_MISANCHORED`, no `OFF_CANVAS`, no `GROUP_BREACH`, no
7
+ `UNKNOWN_SHAPE`, no `LABEL_OVERFLOW` (default cap: 40 chars).
8
+ This is non-negotiable — fix the source before judging visuals.
9
+
10
+ 2. **Tier 2 geometric checks are within bounds.** `NODE_OVERLAP` count
11
+ and `ROUTE_SELF_CROSS` count are advisory but not catastrophic. A
12
+ diagram with a single intentional overlap (e.g., a self-loop) is
13
+ still considered good. The `ROUTE_*` codes (`ROUTE_HITCH`,
14
+ `ROUTE_UNEXPLAINED_BEND`, `ROUTE_LABEL_ON_SHARED_TRUNK`,
15
+ `ROUTE_CONTAINER_MISANCHOR`, `ROUTE_SHAPE_MISANCHOR`,
16
+ `ROUTE_STALE_AFTER_NODE_MOVE`) should always be zero — they are
17
+ route-contract tripwires (see `docs/design/system/route-contracts.md`), and
18
+ any hit means the layout pipeline regressed, not the diagram.
19
+
20
+ 3. **Perceptual metrics fall in the default `QualityBounds` band:**
21
+
22
+ | metric | meaning | default |
23
+ |---|---|---|
24
+ | `edgeCrossings` | pairs of edge segments that visually intersect | ≤ 5% of edge pairs |
25
+ | `labelLegibility` | fraction of node labels whose rendered length fits the node width | ≥ 85% |
26
+ | `whitespaceBalance` | node area ÷ canvas area | 5%–55% (too sparse AND too dense lose) |
27
+ | `labelEdgeProximity` | min pixel distance between any edge-label box and a non-attached node, another edge-label box, or another edge path (graph families; cross-family label/box occlusion is gated by `eval/overlap-audit` + `label-overlap-gate.test.ts`) | ≥ 4 px |
28
+ | `minimumNodeSpacing` | nearest gap between semantically distinct node/mark boxes; XY and Radar same-coordinate series adjacency/overlays are excluded | ≥ 8 px |
29
+ | `elementDensity` | nodes + edges per 10,000 canvas pixels | 0.02–6 |
30
+ | `minimumTextContrast` | WCAG contrast ratio between foreground and supplied background/surface colors; `null` when a requested paint cannot be resolved | ≥ 4.5:1 |
31
+ | `aspectRatio` | canvas w/h | 0.2–5.0 |
32
+
33
+ These are computed by `measureQuality(layout)` and gated by
34
+ `checkQuality(layout)`. They are **deterministic** and cheap — they
35
+ run on every PR.
36
+
37
+ 4. **LLM-as-judge median ≥ 4.0** on the configured stratified sample of the
38
+ Mermaid-docs corpus across three axes. Since QUAL-1, perceptual metrics and
39
+ judge sampling cover every registered renderer rather than only graph families:
40
+ - **Readability** — labels legible, arrows clear, no overlap chaos
41
+ - **Faithfulness** — every node and edge from the source is present
42
+ - **Aesthetics** — balanced layout, professional feel
43
+
44
+ This is **periodic**, not per-PR (model spend + nondeterminism). It
45
+ supports pre-release sign-off when explicitly run. See
46
+ `eval/llm-judge/judge.ts`.
47
+
48
+ ## The rubric (used by both the LLM judge and the perceptual checker)
49
+
50
+ ```
51
+ Rate the rendered Mermaid diagram on three axes, 1 (poor) to 5 (excellent).
52
+
53
+ 1. Readability — Are all labels legible? Do labels overlap nodes or
54
+ edges? Are edges easy to follow? Are arrows pointing the right way?
55
+
56
+ 2. Faithfulness — Does the rendered diagram represent every node and
57
+ edge in the source? Any silently dropped content?
58
+
59
+ 3. Aesthetics — Is the layout balanced? Are nodes evenly spaced?
60
+ Are crossings minimized? Is the overall feel professional?
61
+
62
+ Output strict JSON: { "readability": N, "faithfulness": N, "aesthetics": N, "notes": ["..."] }
63
+ ```
64
+
65
+ ## Why a definition matters
66
+
67
+ Without one, "looks good" is a vibes-check, and vibes drift. The
68
+ combination above gives:
69
+
70
+ - **Verifiability** — every claim has a number behind it.
71
+ - **Repeatability** — the metrics are deterministic; layouts don't
72
+ change between runs (ELK + the verified cross-runtime/cross-process
73
+ determinism we already prove).
74
+ - **Tractability** — the Tier 1 + perceptual checks run in seconds on
75
+ any PR. The LLM judge is reserved for explicit pre-release or periodic
76
+ evaluation runs.
77
+ - **Honest gaps** — `whitespaceBalance` band is rough; aspect-ratio is
78
+ a sanity check, not an aesthetic. We do not claim our metrics match
79
+ a human designer's eye — they catch the worst regressions.
80
+
81
+ ## Why the same Mermaid can look worse here than in Mermaid
82
+
83
+ Agentic Mermaid is not Mermaid's renderer. It parses Mermaid source and
84
+ renders through this project's own layout/style stack: ELK layered layout,
85
+ source-order preservation, conservative node/diamond sizing, and the
86
+ Agentic Mermaid theme system. Mermaid's default flowchart renderer uses
87
+ its own Dagre/ELK configuration, text wrapping, spacing, and CSS. The same
88
+ source can therefore have different rank choices, edge routes, node sizes,
89
+ and aspect ratio.
90
+
91
+ The Auth Flow regression is the concrete example. Mermaid can choose a more
92
+ compact-looking rendering for the feedback loops; Agentic Mermaid now
93
+ prioritizes semantic LR source order (`A → B → C → ... → H`) and routes
94
+ `No` loops backward. That preserves author intent and avoids the earlier
95
+ misordered layout, but it also makes the diagram wider. `verify.ok` still
96
+ only means structurally valid; visual quality needs layout metrics,
97
+ geometry assertions, screenshot/PNG review, or human inspection.
98
+
99
+ ## What we do NOT claim
100
+
101
+ - **No universal Mermaid visual parity.** We aim for faithful, deterministic,
102
+ agent-verifiable output, not pixel/layout equivalence with Mermaid's own
103
+ renderer. When parity matters, compare rendered artifacts and add a
104
+ geometry/screenshot regression for that source.
105
+ - **No pixel comparison for the whole corpus.** We render SVGs but don't compare
106
+ every corpus sample to a golden image. Mermaid-js itself uses an external
107
+ service (Applitools) for visual regression; we don't ship reference images.
108
+ If a glyph changes width by one pixel, our perceptual metrics may move
109
+ slightly, but the bands have headroom.
110
+ - **Perceptual metrics cover every renderable family (QUAL-1).** `layoutMermaid`
111
+ now has `RenderedLayout` adapters for ALL renderable families, so
112
+ `measureQuality` / `checkQuality` and the comparison harness see real geometry
113
+ (not bytes-only) for each. The adapters parse `d.canonicalSource` via the
114
+ legacy per-family parser + layouter — the same geometry the SVG renderer draws
115
+ — so an opaque-but-renderable body is still measured; an invalid opaque body
116
+ degrades to an empty layout instead of throwing. What `nodes` / `edges` /
117
+ `groups` mean per family:
118
+
119
+ | Family | nodes | edges | groups |
120
+ |---------------|------------------------------------|--------------|----------------------|
121
+ | flowchart | graph nodes (ELK) | graph edges | subgraphs |
122
+ | state | states (projected to a graph) | transitions | composites |
123
+ | sequence | actor boxes | messages | — |
124
+ | timeline | period + event boxes | — | — |
125
+ | class | class boxes | relations | — |
126
+ | er | entity boxes | relations | — |
127
+ | journey | task boxes | — | section frames |
128
+ | architecture | services + junctions | edges | groups (flattened) |
129
+ | xychart | bars + line-point markers | — | plot area |
130
+ | pie | slice label boxes (legend anchor + approx bbox) | — | — |
131
+ | quadrant | plotted points | — | quadrant regions |
132
+ | gantt | task bars + milestone diamonds | — | section bands |
133
+ | mindmap | hierarchy nodes | parent links | — |
134
+ | gitgraph | commits | parent links | — |
135
+ | radar | curve vertices (one ring per axis) | — | axis spokes/grid |
136
+
137
+ Families with no structural relations (pie/quadrant/xychart/journey/gantt/radar) carry an
138
+ honestly-empty `edges` array. Bounds is the family layout's canvas size.
139
+ - **No font-substitution check.** Different OSes render different
140
+ default fonts. Our `labelLegibility` heuristic uses a 7 px-per-char
141
+ approximation; under condensed fonts it under-estimates fit.
142
+ - **Contrast needs resolved paints.** `measureQuality(layout)` scores the crisp
143
+ default palette. For a custom style, pass `{ foreground, background,
144
+ surfaces }` as its second argument, or pass the actual rendered text/surface
145
+ associations as `textPairs`. Concrete named, hex, `rgb()`/`rgba()`, and
146
+ `hsl()`/`hsla()` paints—including alpha and `transparent`—are composited over
147
+ the supplied background. Any unresolved requested paint makes contrast
148
+ unmeasurable (`null`), and `checkQuality` reports a violation instead of
149
+ guessing or passing open. The generated visual-quality characterization reads
150
+ text and containing-surface paints from the rendered SVG.
151
+
152
+ ## ASCII determinism
153
+
154
+ Loop 7 added an ASCII determinism guard at
155
+ `src/__tests__/ascii-determinism.test.ts`. It hashes the output of
156
+ `renderMermaidASCII()` over 10 invocations on three multi-edge fixtures
157
+ and asserts a single SHA-256 across all 30 runs per fixture. The probe
158
+ that motivated it (per the Loop 7 plan: "probe FIRST, then act") found
159
+ the existing code already byte-identical across 10 runs — the test
160
+ lands as a regression guard, not a behavior change.
161
+
162
+ What the guard catches:
163
+ - Any future introduction of `Math.random()` / `Date.now()` /
164
+ `performance.now()` into an ASCII rendering or pathfinder code path
165
+ (set size > 1 → fail).
166
+ - Set / Map iteration-order surprises if a code path adopts a hash key
167
+ whose ordering changes between Bun and Node.
168
+
169
+ What the guard covers now:
170
+ - **Representative pathfinder fixtures** over 10 invocations.
171
+ - **Full 271-entry mermaid-js docs corpus**: every entry must have a stable
172
+ ASCII outcome (byte hash or deterministic error) across repeated runs.
173
+
174
+ What the guard does NOT yet cover:
175
+ - **Cross-architecture byte equality** (x86_64 output hash compared directly
176
+ to ARM64 output hash from a recorded manifest).
177
+ - **Input-order independence.** The current same-input tests do not
178
+ stress edge-insertion order. If this becomes release-gating, add a focused
179
+ property/differential test rather than treating corpus determinism as proof.
180
+
181
+ ## PNG determinism
182
+
183
+ Loop 8 added PNG export via `@resvg/resvg-js` (pinned exact `2.6.2`,
184
+ napi-rs native build). The rasterizer choice was hardened by the Loop 8
185
+ critic pass:
186
+
187
+ - `loadSystemFonts: false` is the default — fontconfig differs between
188
+ OSes and CI images, so system fonts would collapse cross-runtime
189
+ parity. (`loadSystemFonts: true` / `--system-fonts` is an explicit
190
+ opt-in for CJK/emoji coverage; determinism is the caller's tradeoff.)
191
+ - Bundled `assets/fonts/Inter-*.ttf` (400/500/600/700) plus
192
+ `DejaVuSans.ttf` + `-Bold.ttf` ship with the package.
193
+ `defaultFontFamily: 'Inter'` — the family the SVG requests and the one
194
+ `src/text-metrics.ts` models, so raster text fits its measured boxes;
195
+ resvg falls back per-glyph to DejaVu for symbols Inter lacks.
196
+ - SVG input passes `embedFontImport: false` so resvg doesn't fetch
197
+ Google Fonts at rasterization time. CSS variable `--font` still
198
+ declares family preference for browser consumers.
199
+
200
+ What's tested:
201
+ - `agent-png.test.ts` — PNG magic bytes, scale proportionality,
202
+ ValidDiagram input, and background variation.
203
+ - `agent-png-determinism.test.ts` — repeated same-input SHA-256 stability
204
+ with a warm-up render to factor out napi initialization differences.
205
+ Plus length-stable defence against partial-buffer truncation
206
+ masquerading as a hash collision.
207
+ - `agent-determinism.test.ts` "cross-runtime PNG" — renders in bun,
208
+ spawns Node on `dist/agent.js`, compares SHA-256 when Node and built `dist/`
209
+ artifacts are present. In bare `bun test` environments it skips rather than
210
+ pretending cross-runtime evidence exists. In the full local verification loop
211
+ after `bun run build`, bun ≡ node on same-machine x86_64 and ARM64 hosts
212
+ where those runtimes and native resvg are installed.
213
+
214
+ What's NOT tested (honest gaps):
215
+ - **Cross-architecture byte equality (x86_64 hash compared to ARM64 hash).**
216
+ The guard verifies Bun ≡ Node on the current machine/architecture, not that
217
+ two different CPU architectures emit identical PNG bytes to each other.
218
+ - **Resvg version drift.** A future bump of `@resvg/resvg-js` may
219
+ change PNG bytes (zlib compression, font hinting). The version is
220
+ pinned exact (no caret) to prevent silent drift on `npm install`,
221
+ but a deliberate bump needs re-baseline.
222
+ - **Different system fonts.** Bundled Inter (+ DejaVu fallback) is what
223
+ we render with; consumers post-processing the output font-substituted
224
+ SVGs will get different pixels. By design.
package/docs/react.md ADDED
@@ -0,0 +1,97 @@
1
+ # React integration
2
+
3
+ Agentic Mermaid renders synchronously to SVG strings, so React apps do not need iframe renderers, hidden browser sessions, or client-side Mermaid hydration.
4
+
5
+ ## Basic component
6
+
7
+ ```tsx
8
+ import { useMemo } from 'react'
9
+ import { renderMermaidSVG } from 'agentic-mermaid'
10
+
11
+ export function MermaidDiagram({ source }: { source: string }) {
12
+ const svg = useMemo(
13
+ () => renderMermaidSVG(source, { security: 'strict' }),
14
+ [source],
15
+ )
16
+
17
+ return <div className="diagram" dangerouslySetInnerHTML={{ __html: svg }} />
18
+ }
19
+ ```
20
+
21
+ `security: 'strict'` removes external-fetch references and is the safest default for user- or agent-generated diagrams.
22
+
23
+ ## Theme from CSS variables
24
+
25
+ ```tsx
26
+ const svg = renderMermaidSVG(source, {
27
+ bg: 'var(--diagram-bg)',
28
+ fg: 'var(--diagram-fg)',
29
+ accent: 'var(--diagram-accent)',
30
+ surface: 'var(--diagram-surface)',
31
+ border: 'var(--diagram-border)',
32
+ embedFontImport: false,
33
+ })
34
+ ```
35
+
36
+ ```css
37
+ .diagram-surface {
38
+ --diagram-bg: #ffffff;
39
+ --diagram-fg: #18181b;
40
+ --diagram-accent: #2563eb;
41
+ --diagram-surface: #f8fafc;
42
+ --diagram-border: #cbd5e1;
43
+ }
44
+
45
+ .dark .diagram-surface {
46
+ --diagram-bg: #0f172a;
47
+ --diagram-fg: #f8fafc;
48
+ --diagram-accent: #38bdf8;
49
+ --diagram-surface: #1e293b;
50
+ --diagram-border: #334155;
51
+ }
52
+ ```
53
+
54
+ Because the SVG keeps CSS variables in place, toggling `.dark` updates existing diagrams without re-rendering.
55
+
56
+ ## PNG export button
57
+
58
+ PNG rendering returns bytes. In browser-oriented React apps, call the PNG helper from a server action/API route or other Node-capable boundary where `@resvg/resvg-js` is available.
59
+
60
+ ```ts
61
+ import { renderMermaidPNG } from 'agentic-mermaid/agent'
62
+
63
+ export function renderDiagramPng(source: string) {
64
+ return renderMermaidPNG(source, { fitTo: { width: 1200 }, background: '#fff' })
65
+ }
66
+ ```
67
+
68
+ For client-only exports, use the live editor/browser's existing download path or post the source to a server endpoint.
69
+
70
+ ## Error handling
71
+
72
+ For untrusted source, parse or verify before rendering:
73
+
74
+ ```tsx
75
+ import { parseRegisteredMermaid, verifyMermaid, renderMermaidSVG } from 'agentic-mermaid/agent'
76
+
77
+ export function SafeDiagram({ source }: { source: string }) {
78
+ const result = useMemo(() => {
79
+ const parsed = parseRegisteredMermaid(source)
80
+ if (!parsed.ok) return { ok: false, message: parsed.error.map(e => e.message).join('\n') }
81
+
82
+ const verify = verifyMermaid(parsed.value)
83
+ if (!verify.ok) return { ok: false, message: verify.warnings.map(w => w.code).join(', ') }
84
+
85
+ return { ok: true, svg: renderMermaidSVG(parsed.value, { security: 'strict' }) }
86
+ }, [source])
87
+
88
+ if (!result.ok) return <pre>{result.message}</pre>
89
+ return <div dangerouslySetInnerHTML={{ __html: result.svg }} />
90
+ }
91
+ ```
92
+
93
+ ## See also
94
+
95
+ - [`api.md`](./api.md) for render options.
96
+ - [`theming.md`](./theming.md) for palette and CSS-variable details.
97
+ - [`config.md`](./config.md) for Mermaid frontmatter/init support.