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,252 @@
1
+ # Computational-aesthetics prototype plan
2
+
3
+ This is the revised plan for cheap, deterministic computational aesthetics in
4
+ Agentic Mermaid. It covers pure functions over geometry and concrete sRGB. It
5
+ does not turn a rubric into an optimizer, add learned scores, or introduce
6
+ stochastic search.
7
+
8
+ The governing rule is stricter than “the formula is deterministic”:
9
+
10
+ > An aesthetic mechanism is ready only when its domain, runtime, product
11
+ > reachability, and evidence are all explicit.
12
+
13
+ Agentic Mermaid already operationalizes graph-layout research in
14
+ `src/layout-rubric.ts` and `src/family-rubric.ts`. The work below extends that
15
+ discipline to categorical color and whole-canvas composition without pretending
16
+ that one scalar score certifies beauty.
17
+
18
+ ## Status
19
+
20
+ - **shipped** — implemented on a product path with discriminating tests.
21
+ - **prototype** — report-only until the evaluation harness justifies a weight.
22
+ - **rejected** — considered, but redundant or insufficiently specified.
23
+
24
+ | # | Idea | Status | Contract / next question |
25
+ |---|---|---|---|
26
+ | 1 | OKLCH constant-lightness categorical ramps | **shipped** | deterministic concrete-sRGB generation |
27
+ | 2 | Minimum ΔE_OK collision floor | **shipped** | hard for 7–24 fills; best-effort and linear-time above 24 |
28
+ | 3 | APCA plus WCAG background-visibility floors | **shipped** | every derived fill clears both for valid concrete backgrounds |
29
+ | 10 | Common-region ownership purity | **shipped** | architecture, class, ER, and timeline ownership frames |
30
+ | 12 | Separate typographic CPL warning | **rejected** | duplicates public universal `LABEL_OVERFLOW`; no independent signal |
31
+ | 4 | Cohen-Or hue-harmony templates | **rejected as a default** | 4/360 cases retain {1,2,3}; experiment remains reproducible |
32
+ | 5 | Purchase-normalized crosslessness `m_c` | prototype | does normalization discriminate across graph sizes? |
33
+ | 6 | Consistent flow-direction metric | prototype | does it catch anything ELK does not already guarantee? |
34
+ | 7 | Whole-canvas composition | prototype | which Ngo terms correlate with preference on our diagrams? |
35
+ | 8 | Crossing-angle / RAC penalty | prototype | incremental value after crossing count |
36
+ | 9 | Spine-straightness continuity | prototype | incremental value after {5,8} |
37
+ | 11 | Proximity / MST purity | prototype | incremental value after common-region and composition metrics |
38
+ | 13 | Information-theoretic order | prototype | is a pinned gzip ratio more than a coarse alignment proxy? |
39
+
40
+ ## What the first implementation taught us
41
+
42
+ The audit and controlled rollout changed the plan in twelve ways.
43
+
44
+ 1. **Runtime is part of correctness.** A deterministic pairwise repair still
45
+ fails the product if an allowed 1,000-slice input takes seconds. The ΔE
46
+ guarantee is therefore bounded at 24 and the large-count path has expected
47
+ linear work. Portable CI asserts the real branch and its operation counts;
48
+ wall-clock observations live in `eval/palette-performance/report.json` with
49
+ their machine, protocol, input hashes, and limitations. Every prototype must
50
+ declare an input-size budget and a degraded-tail rule.
51
+
52
+ 2. **A threshold needs a corpus, not one friendly example.** The 0.10 ΔE claim
53
+ initially failed on shipped themes and custom accent/background pairs. The
54
+ contract now runs across every built-in theme, every count from 7 through
55
+ 24, and adversarial saturated backgrounds. Future thresholds need the same
56
+ domain matrix before they can be called guarantees.
57
+
58
+ 3. **Use the perceptual quantity that controls the decision.** HSL lightness
59
+ misclassified a bright saturated yellow-green background as dark. Palette
60
+ polarity now uses OKLab lightness, and visibility repair searches both
61
+ lightness directions before falling back to a visible achromatic extreme.
62
+
63
+ 4. **A shared waist multiplies risk as well as value.** `pieSliceColors` already
64
+ reaches pie and radar, so one defect reaches both. Migrating four more
65
+ families remains valuable, but only after the waist has an explicit range,
66
+ a hard runtime bound, adversarial tests, and per-family visual review.
67
+
68
+ 5. **Reachability precedes novelty.** A test-only finding is not a product
69
+ capability. The proposed `LABEL_LINE_OVERLONG` finding was not surfaced by
70
+ verify, CLI, API, or MCP and overlapped the existing public
71
+ `LABEL_OVERFLOW` warning. It is removed rather than wired twice.
72
+
73
+ 6. **Independence applies before implementation too.** A new metric must state
74
+ which existing defect it detects that current metrics miss. Idea #12 fails
75
+ that test. Ideas {7,11} and {5,8,9} must pass it through ablation.
76
+
77
+ 7. **Semantic scope must follow every real model.** Common-region purity is not
78
+ architecture-only: class namespaces, ER groups, and timeline sections also
79
+ express ownership. Ancestor walks must be cycle-safe without arbitrary
80
+ depth cutoffs.
81
+
82
+ 8. **Visual evidence needs its own reproducibility contract.** The rollout now
83
+ freezes pre-change SVGs, extracts the categorical colors actually serialized
84
+ by each renderer, writes a machine-readable comparison, and hashes every
85
+ source input plus the generated report/contact sheet. `--check` fails when
86
+ code, baseline, metrics, or images drift.
87
+
88
+ 9. **A controlled rollout is a compatibility policy, not a global recolor.**
89
+ Counts up to six retain each family's existing derived colors byte-for-byte
90
+ (including Journey's established actor wheel). Authored
91
+ `plotColorPalette`, `actorColours`, `sectionFills`/`sectionColours`, and
92
+ `git0..7` remain authoritative. Only derived high-cardinality peer-category
93
+ colors change.
94
+
95
+ 10. **Harmony and categorical distinctness compete at this density.** Across
96
+ 20 built-in themes × counts 7..24, the Cohen-Or/Matsuda experiment reduced
97
+ mean harmony loss from 8.8303° to 0.0084°, but reduced mean minimum ΔE_OK
98
+ from 0.1107 to 0.0456. Only 4/360 palettes retained {1,2,3}; no count above
99
+ seven passed. That rejects #4 as a default layer over this palette.
100
+
101
+ 11. **A measurement must not claim more than it measures.** The performance
102
+ corpus measures one palette-generation call, not a complete diagram render.
103
+ Most controlled families have one peer-category channel; Journey has two
104
+ independent channels (sections and actors). Following the
105
+ [testing-best-practices guidance](https://github.com/adewale/testing-best-practices/tree/e50479920006aa010850a2c37f9bee1e02b5badf),
106
+ CI gates deterministic complexity and evidence freshness, never a portable
107
+ millisecond threshold.
108
+
109
+ 12. **Normalize at the algorithm boundary without erasing compatibility.**
110
+ StyleSpec accepts concrete `#RGB`, named, `rgb()`, and `hsl()` colors, while
111
+ the perceptual math requires six-digit sRGB. The high-count path now
112
+ normalizes every parser-resolvable concrete color before repair. The ≤6
113
+ path deliberately preserves each family's historical spelling or fallback
114
+ policy byte-for-byte; a global normalizer at that boundary would be a
115
+ compatibility regression disguised as cleanup.
116
+
117
+ ## Shipped color contract and family reach
118
+
119
+ The color work is a generation constraint, not evidence that the quality
120
+ rubric predicts human preference. The neutral shared waist now reaches pie,
121
+ radar, xychart, journey, mindmap, and gitgraph. SVG and terminal xychart/gitgraph
122
+ use the same derived palette; the other terminal families do not currently
123
+ encode peer categories with color.
124
+
125
+ The scope rule for this rollout, and for any future shared-palette expansion,
126
+ is explicit:
127
+
128
+ > The scope is intentionally limited to families where color identifies peer
129
+ > categories. Semantic colors—status, hierarchy, role, or user-authored
130
+ > meaning—should not be automatically redistributed.
131
+
132
+ | Property | 7–24 fills | More than 24 fills |
133
+ |---|---|---|
134
+ | deterministic | hard | hard |
135
+ | valid concrete hex output | hard | hard |
136
+ | WCAG ≥ 1.25 and APCA ≥ 15 vs concrete background | hard | hard |
137
+ | unique fills | hard | best-effort |
138
+ | minimum pairwise ΔE_OK ≥ 0.10 | hard | not claimed |
139
+ | asymptotic repair cost | bounded `O(M·n²)`, with `n ≤ 24` and fixed corpus `M` | expected `O(n)` with average `O(1)` set membership |
140
+
141
+ The established local palette is preserved when it meets the contract. If it
142
+ does not, a deterministic farthest-point packing pass selects background-visible
143
+ concrete sRGB candidates. Above 24, pairwise enforcement is skipped.
144
+
145
+ ## Prototype protocol
146
+
147
+ Reuse the existing evaluator; do not create a second quality system.
148
+
149
+ 1. **Define the contract first.** State applicable families, input-size range,
150
+ asymptotic and wall-clock budgets, degraded-tail behavior, and exclusions.
151
+ 2. **Prove product reachability.** Identify the public result, warning, or
152
+ report that exposes the metric. A private helper plus unit test is not done.
153
+ 3. **Report before weighting.** Add the metric to the per-metric tracker with
154
+ zero score weight. A weight is a separate scoring-contract change.
155
+ 4. **Add trip fixtures.** Include healthy and deliberately bad examples that
156
+ make the metric move. Zero on every committed fixture is a guard, not a
157
+ demonstrated discriminator.
158
+ 5. **Measure the full domain.** Use the Mermaid docs corpus, heuristic tracker,
159
+ deterministic family fuzzers, all built-in themes where color is involved,
160
+ and adversarial boundary inputs.
161
+ 6. **Keep visual evidence.** Any generation or geometry change needs
162
+ before/after renders and snapshot review in every affected family. The
163
+ palette rollout operationalizes this rule in
164
+ `scripts/pr-assets/palette-rollout-evidence.ts`.
165
+ 7. **Gate determinism and cost.** Preserve byte identity and deterministically
166
+ assert that the intended bounded algorithmic path engaged. Keep p50/p95
167
+ observations over a fixed corpus in a separate, provenance-bound report;
168
+ do not use cross-machine wall time as a CI threshold. Idea #13 must also pin
169
+ codec and level.
170
+
171
+ ## Evaluation and graduation
172
+
173
+ For a candidate metric M, record:
174
+
175
+ - **coverage** — fraction of diagrams on which M varies;
176
+ - **discrimination** — LCC/SROCC against a small blinded human A/B set;
177
+ - **independence** — partial correlation after controlling for current metrics;
178
+ - **stability** — sensitivity to harmless translation, scale, and equivalent
179
+ source ordering where those transforms should not matter;
180
+ - **cost** — deterministic operation growth at the declared size boundary,
181
+ plus environment-qualified p50/p95 observations where useful;
182
+ - **reach** — public surfaces and diagram families that consume the result.
183
+
184
+ Graduate a metric from report-only only when it fires on trip fixtures, varies
185
+ on the corpus, adds preference signal after current metrics, and stays inside
186
+ its cost budget. Otherwise remove it or retain it explicitly as a correctness
187
+ guard with no aesthetic weight.
188
+
189
+ For combinations, run small ablations rather than blending everything. Treat
190
+ each combination as a gated ladder: validate the cheaper base term, add one
191
+ term, and stop when independence disappears.
192
+
193
+ - **{1,2,3}:** lightness structure, categorical distance, and background
194
+ visibility are complementary and already ship as one generation contract.
195
+ - **{1,2,3,4}:** the completed experiment found no useful Pareto front at
196
+ 8–24 categories: hue-sector contraction almost eliminates harmony loss but
197
+ breaks minimum categorical distance. Do not ship this bundle as a default.
198
+ - **{5,8}:** crossing prevalence and crossing quality should be complementary.
199
+ - **{5,6,8}:** add direction only if #6 catches defects not explained by the
200
+ crossing pair or by ELK's existing directional constraints.
201
+ - **{5,8,9}:** likely redundant; add #9 only if partial correlation survives.
202
+ - **{7,10,11}:** composition, common region, and proximity span figure,
203
+ ownership, and local grouping scales, but need ablation because the signals
204
+ can collapse onto the same cluster structure.
205
+
206
+ ## Revised stack rank
207
+
208
+ The rank uses five axes: user-visible value, independence from existing
209
+ signals, product reach, bounded runtime, and validation burden. “Foundation” is
210
+ not optional feature work. An arrow means “validate the new idea alone, then
211
+ admit it to the combination”; it is not permission to ship an opaque aggregate.
212
+
213
+ | Rank | Work package | Form | Why it is here | Stop / go gate |
214
+ |---|---|---|---|---|
215
+ | Foundation | **Keep {1,2,3,10}; reject #12** | shipped contract | Color and ownership have bounded, reachable contracts; the extra label warning is redundant. | keep the regression matrix green |
216
+ | Delivered | **Controlled {1,2,3} family rollout** | shipped combination | 14/14 fixture violations corrected; 8/8 family/theme cases pass; ≤6 colors and authored overrides remain compatible; six rich website examples expose the peer mapping at inspectable size. | palette evidence, complexity, website, and family gates |
217
+ | Rejected | **#4 → {1,2,3,4} color-harmony default** | completed experiment | 4/360 pass rate; mean minimum ΔE_OK falls 0.1107 → 0.0456. | reconsider only with a new constraint-preserving algorithm and blinded preference evidence |
218
+ | Paused 1 | **#5 normalized crosslessness `m_c`** | telemetry, postponed | Still the cheapest next metric experiment, but telemetry work is intentionally deferred. | resume only on explicit telemetry restart |
219
+ | Paused 2 | **#8 → {5,8} crossing-quality bundle** | telemetry, postponed | Clear ablation after #5. | #8 must add signal after #5 |
220
+ | Paused 3 | **#6 → {5,6,8} directional-legibility bundle** | telemetry, postponed | Likely overlaps ELK behavior. | independent trip case first |
221
+ | Paused 4 | **#7 whole-canvas composition** | telemetry, postponed | Potential figure-level value, high calibration burden. | stable, nonzero corpus coverage |
222
+ | Paused 5 | **#11 → {7,10,11} multiscale grouping** | telemetry, postponed | Plausible value with high redundancy risk. | term-by-term ablation |
223
+ | Paused 6 | **#9 → {5,8,9} edge-path bundle** | telemetry, postponed | Costlier and likely redundant. | partial-correlation survival |
224
+ | Paused 7 | **#13 pinned-gzip order** | telemetry, postponed | Codec-sensitive and coarse. | beat simpler direct features |
225
+
226
+ This supersedes the earlier ordering. The highest-ranked product change is now
227
+ delivered and its evidence gate is permanent. The harmony experiment has also
228
+ resolved its decision: it is not merely lower priority; this formulation is a
229
+ bad default. With telemetry postponed, there is no authorized next aesthetic
230
+ metric package. If telemetry resumes, it still starts with #5 and every later
231
+ bundle remains conditional on ablation.
232
+
233
+ ## Scope boundaries
234
+
235
+ - This plan covers measurement and closed-form generation, not optimization.
236
+ - Flowchart/state subgraph purity remains in `layout-rubric.ts`; the generic
237
+ #10 implementation covers ownership frames projected by architecture, class,
238
+ ER, and timeline.
239
+ - `LABEL_OVERFLOW` remains the public label-length signal. It applies the
240
+ caller-configurable cap to the longest rendered line after entity decoding,
241
+ line-break normalization, and formatting-tag removal. A second fixed CPL
242
+ warning would be redundant and less reachable.
243
+ - The shared perceptual palette reaches only families where color denotes peer
244
+ categories: pie slices, radar curves, xychart series, journey actors/sections,
245
+ mindmap top-level branches, and gitgraph branches. Structural families use
246
+ color for roles, status, hierarchy, or authored semantics; automatic hue
247
+ spreading there would invent category meaning and can weaken brand/status
248
+ cues. Timeline/gantt/quadrant have family-specific categorical contracts and
249
+ are not silently swept into this rollout.
250
+ - A score of 100 proves only that a renderer clears the measured hygiene floor;
251
+ it does not certify beauty. Each family still needs an aesthetic thesis and
252
+ human visual review.
package/docs/config.md ADDED
@@ -0,0 +1,121 @@
1
+ # Mermaid config support
2
+
3
+ Agentic Mermaid accepts Mermaid-style runtime config through explicit render options, YAML frontmatter, and `%%{init: ...}%%` / `%%{initialize: ...}%%` directives.
4
+
5
+ Round-trip behavior: the agent surface preserves these source wrappers
6
+ byte-verbatim — `parseRegisteredMermaid → serializeMermaid` and every `mutate` re-emit
7
+ the original frontmatter/directives/leading comments untouched, so a
8
+ `config.layout`/`config.look` request written for Mermaid survives an
9
+ Agentic Mermaid edit loop. Canonical wrapper synthesis (config-nested
10
+ frontmatter, directives folded) is opt-in via
11
+ `serializeMermaid(d, { wrapper: 'canonical' })` or `am format
12
+ --canonical-wrapper`.
13
+
14
+ ## Explicit config
15
+
16
+ ```ts
17
+ import { renderMermaidSVG } from 'agentic-mermaid'
18
+
19
+ const svg = renderMermaidSVG(source, {
20
+ mermaidConfig: {
21
+ theme: 'base',
22
+ themeVariables: {
23
+ fontFamily: 'Inter, sans-serif',
24
+ primaryColor: '#eef2ff',
25
+ primaryTextColor: '#111827',
26
+ lineColor: '#64748b',
27
+ },
28
+ },
29
+ })
30
+ ```
31
+
32
+ Explicit options win over source-level config when both specify the same supported field. `RenderOptions.onConfigDiagnostic` can collect warnings for explicit config that is unknown, invalid, or has no faithful effect; without a collector, explicit ineffective State config warns through `console.warn` rather than disappearing silently.
33
+
34
+ ## YAML frontmatter
35
+
36
+ ```mermaid
37
+ ---
38
+ config:
39
+ theme: base
40
+ themeVariables:
41
+ primaryColor: '#eef2ff'
42
+ lineColor: '#64748b'
43
+ ---
44
+ flowchart TD
45
+ A --> B
46
+ ```
47
+
48
+ ## Init directives
49
+
50
+ ```mermaid
51
+ %%{init: { "theme": "base", "themeVariables": { "fontFamily": "Inter" } }}%%
52
+ flowchart TD
53
+ A --> B
54
+ ```
55
+
56
+ Loose Mermaid-style object literals are accepted for documented config surfaces where Mermaid permits them.
57
+
58
+ ## Supported config surfaces
59
+
60
+ Top-level:
61
+
62
+ - `theme`
63
+ - `themeVariables`
64
+ - `fontFamily`
65
+ - `fontSize`
66
+
67
+ (The composable look system is a *render option*, not Mermaid config:
68
+ `RenderOptions.style` — name | JSON record | stack — and
69
+ `RenderOptions.seed`; see `docs/style-authoring.md`. Mermaid
70
+ `themeVariables` and explicit color options still win over a style's
71
+ palette, preserving Mermaid compatibility.)
72
+
73
+ Family-specific config is **wire-or-warn**: every accepted key either changes
74
+ output or produces `INEFFECTIVE_CONFIG` naming the field. Unknown keys also
75
+ warn, so spelling mistakes cannot disappear silently. This contract covers all
76
+ every registered family and both frontmatter and init directives.
77
+
78
+ Notable wired sections include:
79
+
80
+ - `flowchart`: spacing and measured wrapping;
81
+ - `sequence`: actor/message/note spacing, dimensions, activations, and numbering;
82
+ - `timeline`, `journey`, `class`, `er`, `architecture`, `xyChart`, `pie`,
83
+ `quadrantChart`, `mindmap`, and `gitGraph`: the fields documented in their
84
+ family design notes;
85
+ - `gantt.displayMode` (`compact` packs non-overlapping tasks in a section into
86
+ shared rows).
87
+
88
+ `state` has a typed `StateRuntimeConfig`. `nodeSpacing`, `rankSpacing`,
89
+ `padding`, `radius`, `fontSize`, `compositTitleSize` (upstream spelling),
90
+ `forkWidth`, `forkHeight`, `noteMargin`, and `dividerMargin` are wired to
91
+ State-specific geometry/paint. `defaultRenderer: 'elk'` is satisfied;
92
+ alternate renderer requests, invalid values, and legacy Dagre/fixed-metric
93
+ calibration fields emit qualified `state.*` diagnostics. Unsupported fields
94
+ and wrappers remain source-preserved. The executable all-family audit is
95
+ `src/__tests__/unknown-config-wire-or-warn.test.ts`; the independent State key
96
+ inventory and discriminating geometry probes are in `state-config.test.ts`.
97
+
98
+ ## Security
99
+
100
+ Use strict rendering for untrusted source:
101
+
102
+ ```ts
103
+ renderMermaidSVG(source, { security: 'strict' })
104
+ ```
105
+
106
+ Strict mode disables external-fetch references such as remote font imports. Agents should prefer strict mode unless the caller explicitly wants externally loaded fonts.
107
+
108
+ ## PNG and config
109
+
110
+ PNG renders from the same SVG pipeline, so supported Mermaid config affects PNG too:
111
+
112
+ ```ts
113
+ import { renderMermaidPNG } from 'agentic-mermaid/agent'
114
+
115
+ const png = renderMermaidPNG(source, {
116
+ fitTo: { width: 1200 },
117
+ background: '#fff',
118
+ })
119
+ ```
120
+
121
+ For offline/deterministic PNG generation, avoid external font imports by relying on the bundled font fallback or by rendering with strict/offline SVG options where applicable.
@@ -0,0 +1,265 @@
1
+ # Fonts in custom styles
2
+
3
+ A custom style chooses a font family; the environment that renders the
4
+ diagram supplies the actual face. The public `StyleSpec` deliberately keeps
5
+ those responsibilities separate:
6
+
7
+ ```json
8
+ {
9
+ "$schema": "https://agentic-mermaid.dev/schemas/style-spec.schema.json",
10
+ "name": "acme-report",
11
+ "font": "'Acme Sans', Inter, system-ui",
12
+ "colors": {
13
+ "bg": "#fffdf8",
14
+ "fg": "#171512",
15
+ "accent": "#1d4ed8"
16
+ },
17
+ "stroke": "crisp"
18
+ }
19
+ ```
20
+
21
+ `font` is a CSS family name or family stack. It does not load a file, register
22
+ a browser `FontFace`, or tell the PNG rasterizer where to find one. This keeps
23
+ the same style usable across SVG and PNG while each output surface resolves
24
+ fonts in its own way.
25
+
26
+ For the rest of the Style model — inline specs, registration, stacking, and
27
+ validation — see [Authoring styles](./style-authoring.md).
28
+
29
+ ## Quick recipe
30
+
31
+ Save the JSON above as `acme-report.style.json`. For an SVG that declares the
32
+ family without making an external font request:
33
+
34
+ ```bash
35
+ am render diagram.mmd \
36
+ --format svg \
37
+ --style acme-report.style.json \
38
+ --security strict \
39
+ --output diagram.svg
40
+ ```
41
+
42
+ For a PNG, point the local rasterizer at a directory containing the matching
43
+ font faces:
44
+
45
+ ```bash
46
+ am render diagram.mmd \
47
+ --format png \
48
+ --style acme-report.style.json \
49
+ --font-dirs ./fonts \
50
+ --output diagram.png
51
+ ```
52
+
53
+ The same workflow through the library is:
54
+
55
+ ```ts
56
+ import { readFileSync, writeFileSync } from 'node:fs'
57
+ import {
58
+ renderMermaidPNG,
59
+ renderMermaidSVG,
60
+ validateStyleSpec,
61
+ } from 'agentic-mermaid/agent'
62
+
63
+ const source = readFileSync('diagram.mmd', 'utf8')
64
+ const style = JSON.parse(readFileSync('acme-report.style.json', 'utf8'))
65
+ const problems = validateStyleSpec(style)
66
+ if (problems.length) throw new Error(problems.join('\n'))
67
+
68
+ const svg = renderMermaidSVG(source, {
69
+ style,
70
+ security: 'strict',
71
+ })
72
+ writeFileSync('diagram.svg', svg)
73
+
74
+ const png = renderMermaidPNG(source, {
75
+ style,
76
+ fontDirs: ['./fonts'],
77
+ })
78
+ writeFileSync('diagram.png', png)
79
+ ```
80
+
81
+ ## Which font value wins
82
+
83
+ For `renderMermaidSVG`, font selection follows this precedence, from strongest
84
+ to weakest:
85
+
86
+ 1. The render call's explicit `font` option.
87
+ 2. Mermaid `fontFamily` configuration.
88
+ 3. Mermaid `themeVariables.fontFamily` configuration.
89
+ 4. The resolved Style stack's `font` field.
90
+ 5. The default, Inter.
91
+
92
+ `renderMermaidPNG` accepts the same shared `font` option and precedence as SVG.
93
+ `fontDirs` makes faces available to the rasterizer but does not choose the
94
+ family.
95
+
96
+ Within a Style stack, later entries win. For example, `[style, 'dracula']`
97
+ keeps the inline/file-loaded Style's font because `dracula` is a palette-only
98
+ Style; a later Style that defines `font` would replace it. A file's `name`
99
+ field does not register it automatically — give it a canonical `look:` or
100
+ `palette:` name and call `registerStyle(style)` before using that identity in a
101
+ library stack.
102
+
103
+ `validateStyleSpec` checks that `font` is a string. It cannot prove that the
104
+ family is installed, present under `fontDirs`, or available to a browser.
105
+
106
+ ## Resolution by output surface
107
+
108
+ | Surface | Where the face comes from | Custom-font control |
109
+ |---|---|---|
110
+ | SVG library/CLI | Browser, viewing application, or host-page CSS | `font`, `embedFontImport`, `security` |
111
+ | PNG library/CLI | Bundled faces, `fontDirs`, optionally OS fonts | `font`, `fontDirs`, `loadSystemFonts` / `--system-fonts` |
112
+ | Live editor | Faces available to the editor page and browser | Font picker; no `fontDirs` |
113
+ | Local MCP `render_png` | Bundled faces, `fontDirs`, optionally OS fonts | `fontDirs`, `loadSystemFonts` |
114
+ | Hosted MCP `render_png` | Faces bundled into the Worker | No filesystem font input; use local MCP/library/CLI for custom directories |
115
+ | ASCII, Unicode, JSON layout | Terminal/layout output has no font face | Style fonts do not apply |
116
+
117
+ ### SVG
118
+
119
+ SVG output always declares the selected family through the root `--font` CSS
120
+ custom property and a `font-family` rule. The SVG renderer does not read
121
+ `fontDirs`.
122
+
123
+ By default, a single plain family name such as `Acme Sans` produces a Google
124
+ Fonts `@import`. Class and ER diagrams can also import JetBrains Mono for their
125
+ monospace labels. Set `embedFontImport: false`, or use `security: 'strict'`, to
126
+ omit all of those requests while keeping the family declaration:
127
+
128
+ ```ts
129
+ const svg = renderMermaidSVG(source, {
130
+ style: { font: 'Acme Sans' },
131
+ embedFontImport: false,
132
+ })
133
+ ```
134
+
135
+ CSS variable references and family stacks are declared as-is and do not
136
+ produce an import for the selected family:
137
+
138
+ ```ts
139
+ renderMermaidSVG(source, {
140
+ style: { font: "'Acme Sans', Inter, system-ui" },
141
+ })
142
+
143
+ renderMermaidSVG(source, {
144
+ font: 'var(--brand-font, Inter)',
145
+ })
146
+ ```
147
+
148
+ The class/ER JetBrains Mono import still applies unless `embedFontImport` is
149
+ false or strict security is enabled.
150
+
151
+ If the selected family is unavailable when the SVG is viewed, normal CSS
152
+ fallback applies. A family stack makes that fallback explicit.
153
+
154
+ Raw SVG declares the family through `var(--font, ...)`. Some static SVG
155
+ rasterizers, including resvg and librsvg, do not resolve CSS custom properties.
156
+ Use `renderMermaidPNG` for Agentic Mermaid's built-in workaround, or resolve
157
+ the font variable to a concrete `font-family` before feeding raw SVG to another
158
+ static rasterizer.
159
+
160
+ ### Host-page and browser fonts
161
+
162
+ An inline SVG can use a face registered by its host document. A stylesheet can
163
+ map an installed face:
164
+
165
+ ```css
166
+ @font-face {
167
+ font-family: "Acme Sans";
168
+ src: local("Acme Sans Regular"), local("AcmeSans-Regular");
169
+ font-weight: 400;
170
+ }
171
+ ```
172
+
173
+ Browser applications can also register bytes selected by the user:
174
+
175
+ ```ts
176
+ const face = new FontFace('Acme Sans', await file.arrayBuffer(), {
177
+ weight: '400',
178
+ })
179
+ await face.load()
180
+ document.fonts.add(face)
181
+ await document.fonts.load('400 14px "Acme Sans"')
182
+ ```
183
+
184
+ Register one `FontFace` or `@font-face` rule per required weight, or accept the
185
+ browser's synthesized/fallback face for weights that are missing.
186
+
187
+ Register the face in the same document that displays the SVG, and wait for it
188
+ before displaying or raster-capturing the result. A raw SVG export still only
189
+ declares the family; it does not serialize a dynamically registered `FontFace`.
190
+ The public live editor can use faces already available to its page/browser,
191
+ but it has no filesystem `fontDirs` option or arbitrary font-file input.
192
+
193
+ ### PNG
194
+
195
+ `renderMermaidPNG` renders offline with `@resvg/resvg-js`. It loads:
196
+
197
+ 1. The bundled Inter faces used by the text-metrics model.
198
+ 2. DejaVu Sans as a per-glyph fallback for symbols Inter lacks.
199
+ 3. The bundled faces referenced by built-in Styles: Caveat, EB Garamond,
200
+ Architects Daughter, and Share Tech Mono.
201
+ 4. Any directories supplied through `fontDirs`.
202
+ 5. OS-installed fonts only when `loadSystemFonts: true` is requested.
203
+
204
+ The CLI equivalents are `--font-dirs <dir[,dir...]>` and `--system-fonts`.
205
+ Prefer a dedicated `fontDirs` directory when reproducible output matters.
206
+ System-font loading makes the result depend on the machine's installed faces.
207
+
208
+ The family stored inside the font file must match the intended family token in
209
+ the Style's `font` value — normally the first family in its stack. Supply the
210
+ weights the diagram uses; Agentic Mermaid commonly emits 400, 500, 600, and
211
+ 700. If an unbundled requested family cannot be resolved, PNG rasterization
212
+ uses Inter, with DejaVu available for per-glyph fallback.
213
+
214
+ ## Diagnostics and layout limits
215
+
216
+ `PNG_FONT_COVERAGE` warns when no loaded face contains a character, such as a
217
+ CJK or emoji code point. It is a glyph-coverage check, not a family-presence
218
+ check. If `Acme Sans` is missing but Inter covers every Latin character, the
219
+ render can fall back without a coverage warning. Confirm custom-family output
220
+ visually or with a font-specific render fixture.
221
+
222
+ Diagram geometry uses the deterministic proportional measurement contract
223
+ recorded in `TEXT_MEASUREMENT_CONTRACT`; it never reads ambient system-font
224
+ state. Every continuous painted text advance—bundled, monospace, or custom—gets
225
+ SVG `textLength` with `lengthAdjust="spacingAndGlyphs"`, because loading a font
226
+ resource does not calibrate its metrics to the layout estimator. Independently
227
+ positioned multiline tspans are fitted per line, while nested formatting
228
+ contributes inherited size, weight, and tracking to that line's width. Explicit
229
+ emitter-owned `textLength` remains authoritative. Native and browser PNG consume
230
+ the same secured SVG, so a materially wider face cannot silently overflow its
231
+ measured box. The tradeoff is explicit: unusually wide or narrow glyphs may be
232
+ horizontally fitted rather than changing deterministic layout. Test long labels
233
+ across target families for visual quality, especially for extreme proportions.
234
+
235
+ ## Troubleshooting
236
+
237
+ **The custom family is not visible in PNG.** Check the font's internal family
238
+ name, include the directory through `fontDirs`, supply the required weights,
239
+ and make sure Mermaid `fontFamily` configuration in the source is not
240
+ overriding the Style.
241
+
242
+ **The SVG requests a font from the network.** Render with
243
+ `security: 'strict'` or `embedFontImport: false`.
244
+
245
+ **The SVG looks different in another viewer.** The other environment did not
246
+ resolve the same family or selected a different fallback. Use a deliberate
247
+ family stack and make the required face available to that viewer.
248
+
249
+ **PNG warns about CJK or emoji.** Add a face covering those code points through
250
+ `fontDirs`, or opt into `loadSystemFonts` when machine-dependent output is
251
+ acceptable.
252
+
253
+ ## Related documentation
254
+
255
+ - [Authoring styles](./style-authoring.md) — Style fields, stacks, registration,
256
+ validation, and the quality rubric.
257
+ - [Custom style cookbook](./custom-style-cookbook.md) — complete JSON examples
258
+ and screenshots.
259
+ - [API reference](./api.md) — `RenderOptions` and `PngOptions`.
260
+ - [Security posture](../SECURITY.md) — strict SVG and external-reference
261
+ guarantees.
262
+ - [PNG determinism](./quality.md#png-determinism) — bundled fonts and the
263
+ reproducibility tradeoff of system fonts.
264
+ - [Bundled font inventory](../assets/fonts/FONT-LICENSES.md) — the faces shipped
265
+ with the package.