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.
- package/AGENT_NATIVE.md +671 -0
- package/CHANGELOG.md +19 -0
- package/Instructions_for_agents.md +89 -0
- package/README.md +8 -8
- package/assets/hero.png +0 -0
- package/dist/agent-core.d.ts +2 -2
- package/dist/agent-core.js +2 -2
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +4 -4
- package/dist/agentic-mermaid-mcp.js +5 -5
- package/dist/am.js +8 -15
- package/dist/{chunk-QKDLMOJJ.js → chunk-2P5FKUQC.js} +7 -9
- package/dist/{chunk-WN3YDOBG.js → chunk-2S3VSS6I.js} +4 -4
- package/dist/{chunk-CDPKJ2BQ.js → chunk-2SILJDW2.js} +2 -2
- package/dist/{chunk-ZYMMKQ3I.js → chunk-BJYMBT4I.js} +299 -973
- package/dist/{chunk-WI35L7IR.js → chunk-FDM43XU7.js} +2 -2
- package/dist/{chunk-4QECACIA.js → chunk-MYPDVZCO.js} +19 -19
- package/dist/{index-D54dqSdC.d.ts → index-DWPa2MJs.d.ts} +7 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-cli-5J47WEY3.js +17 -0
- package/docs/README.md +34 -34
- package/docs/agent-api-cookbook.md +317 -0
- package/docs/agent-mutation-policy.md +104 -0
- package/docs/agent-workflow-examples.md +46 -0
- package/docs/api.md +418 -0
- package/docs/ascii.md +113 -0
- package/docs/code-mode-impact.md +214 -0
- package/docs/comparison.md +106 -0
- package/docs/computational-aesthetics-prototype-plan.md +252 -0
- package/docs/config.md +121 -0
- package/docs/custom-fonts.md +265 -0
- package/docs/custom-style-cookbook.md +275 -0
- package/docs/diagram-families.md +218 -0
- package/docs/features.md +154 -0
- package/docs/fork-differences.md +133 -0
- package/docs/getting-started.md +182 -0
- package/docs/issue-derived-test-cases.md +163 -0
- package/docs/mcp-code-mode-rationale.md +108 -0
- package/docs/mcp-http-transport.md +230 -0
- package/docs/mermaid-layout-complaints.md +851 -0
- package/docs/mutation-testing.md +285 -0
- package/docs/quality.md +224 -0
- package/docs/react.md +97 -0
- package/docs/style-authoring.md +336 -0
- package/docs/svg-semantic-contract.md +49 -0
- package/docs/testing-strategy.md +443 -0
- package/docs/theming.md +149 -0
- package/package.json +21 -6
- package/server.json +2 -2
- package/skills/agentic-mermaid-live-editor/SKILL.md +1 -1
- 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%.
|
package/docs/quality.md
ADDED
|
@@ -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.
|