agentic-mermaid 0.1.1 → 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 (340) hide show
  1. package/AGENT_NATIVE.md +155 -91
  2. package/CHANGELOG.md +163 -4
  3. package/Instructions_for_agents.md +10 -10
  4. package/LICENSES/Apache-2.0.txt +202 -0
  5. package/README.md +31 -38
  6. package/THIRD_PARTY_NOTICES.md +29 -0
  7. package/assets/hero.png +0 -0
  8. package/dist/agent-core.d.ts +463 -0
  9. package/dist/agent-core.js +397 -0
  10. package/dist/agent.d.ts +73 -1868
  11. package/dist/agent.js +336 -37325
  12. package/dist/agentic-mermaid-mcp.js +7 -38399
  13. package/dist/am.js +33626 -40446
  14. package/dist/chunk-2P5FKUQC.js +3485 -0
  15. package/dist/chunk-2S3VSS6I.js +149 -0
  16. package/dist/chunk-2SILJDW2.js +37 -0
  17. package/dist/chunk-BJYMBT4I.js +61963 -0
  18. package/dist/chunk-FDM43XU7.js +615 -0
  19. package/dist/chunk-MYPDVZCO.js +4702 -0
  20. package/dist/index-DWPa2MJs.d.ts +6780 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +310 -33014
  23. package/dist/mcp-cli-5J47WEY3.js +17 -0
  24. package/docs/README.md +39 -31
  25. package/docs/agent-api-cookbook.md +44 -35
  26. package/docs/agent-mutation-policy.md +1 -1
  27. package/docs/agent-workflow-examples.md +2 -2
  28. package/docs/api.md +284 -77
  29. package/docs/ascii.md +26 -16
  30. package/docs/code-mode-impact.md +214 -0
  31. package/docs/comparison.md +23 -11
  32. package/docs/computational-aesthetics-prototype-plan.md +252 -0
  33. package/docs/config.md +26 -10
  34. package/docs/custom-fonts.md +265 -0
  35. package/docs/custom-style-cookbook.md +130 -9
  36. package/docs/diagram-families.md +48 -23
  37. package/docs/features.md +59 -52
  38. package/docs/fork-differences.md +6 -3
  39. package/docs/getting-started.md +23 -16
  40. package/docs/issue-derived-test-cases.md +29 -22
  41. package/docs/mcp-code-mode-rationale.md +13 -6
  42. package/docs/mcp-http-transport.md +20 -13
  43. package/docs/mermaid-layout-complaints.md +66 -52
  44. package/docs/mutation-testing.md +145 -66
  45. package/docs/quality.md +23 -14
  46. package/docs/react.md +2 -2
  47. package/docs/schemas/style-spec.schema.json +1221 -30
  48. package/docs/style-authoring.md +241 -38
  49. package/docs/svg-semantic-contract.md +49 -0
  50. package/docs/testing-strategy.md +166 -58
  51. package/docs/theming.md +19 -32
  52. package/examples/styles/catalog.json +54 -0
  53. package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
  54. package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
  55. package/examples/styles/cupertino-prototype.mmd +10 -0
  56. package/examples/styles/cupertino-prototype.style.json +63 -0
  57. package/examples/styles/mid-century-report.style.json +2 -2
  58. package/examples/styles/star-chart-atlas.style.json +2 -1
  59. package/examples/styles/transit-route-map.style.json +2 -2
  60. package/examples/styles/vercel-inspired-prototype.mmd +6 -0
  61. package/examples/styles/vercel-inspired-prototype.style.json +87 -0
  62. package/package.json +85 -45
  63. package/server.json +2 -2
  64. package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
  65. package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
  66. package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
  67. package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
  68. package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
  69. package/TODO.md +0 -684
  70. package/dist/index-wjRjbsP-.d.ts +0 -1102
  71. package/docs/assets/improvements/box-start-connector.png +0 -0
  72. package/docs/assets/improvements/comment-dropped-lint.png +0 -0
  73. package/docs/assets/improvements/er-content-loss.png +0 -0
  74. package/docs/assets/improvements/fan-in-trunks.png +0 -0
  75. package/docs/assets/improvements/gantt-family.png +0 -0
  76. package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
  77. package/docs/assets/improvements/gantt-verify-seam.png +0 -0
  78. package/docs/assets/improvements/subgraph-container-edges.png +0 -0
  79. package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
  80. package/docs/assets/improvements/wrapper-init-directive.png +0 -0
  81. package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
  82. package/docs/assets/style-cookbook/mid-century-report.png +0 -0
  83. package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
  84. package/docs/assets/style-cookbook/transit-route-map.png +0 -0
  85. package/docs/contributing/adding-diagram-types.md +0 -144
  86. package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
  87. package/docs/contributing/diagram-family-citizenship.md +0 -82
  88. package/docs/contributing/harvesting-upstream-tests.md +0 -126
  89. package/docs/contributing/lessons-learned.md +0 -276
  90. package/docs/contributing/releasing.md +0 -82
  91. package/docs/contributing/visual-review-evidence.md +0 -53
  92. package/docs/design/families/README.md +0 -26
  93. package/docs/design/families/architecture-beta.md +0 -48
  94. package/docs/design/families/flowchart-parser-conformance.md +0 -22
  95. package/docs/design/families/gantt-research.md +0 -137
  96. package/docs/design/families/gantt.md +0 -329
  97. package/docs/design/families/journey-agentic-cards-before.png +0 -0
  98. package/docs/design/families/journey-experience-curve-implementation.png +0 -0
  99. package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
  100. package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
  101. package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
  102. package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
  103. package/docs/design/families/journey-migration-parity.md +0 -439
  104. package/docs/design/families/journey-section-overlap-after.png +0 -0
  105. package/docs/design/families/journey-section-overlap-before.png +0 -0
  106. package/docs/design/families/journey-usage-research.md +0 -423
  107. package/docs/design/families/journey.md +0 -153
  108. package/docs/design/families/xychart.md +0 -85
  109. package/docs/design/system/README.md +0 -45
  110. package/docs/design/system/abstraction-audit.md +0 -224
  111. package/docs/design/system/abstraction-recommendations.md +0 -655
  112. package/docs/design/system/architecture.mmd +0 -29
  113. package/docs/design/system/architecture.svg +0 -137
  114. package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
  115. package/docs/design/system/layout-rubric.md +0 -138
  116. package/docs/design/system/route-contracts.md +0 -848
  117. package/docs/design/system/source-preservation-ladder.md +0 -52
  118. package/docs/design/system/styles-rollout.md +0 -216
  119. package/docs/design/system/ugly-layouts.md +0 -119
  120. package/docs/layout-characterization/README.md +0 -245
  121. package/docs/layout-characterization/contact-sheet-families.md +0 -498
  122. package/docs/layout-characterization/contact-sheet.md +0 -638
  123. package/docs/layout-characterization/properties.md +0 -281
  124. package/docs/layout-characterization/visual-quality.md +0 -160
  125. package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
  126. package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
  127. package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
  128. package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
  129. package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
  130. package/docs/layout-characterization/visual-snapshots/journey.svg +0 -98
  131. package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
  132. package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
  133. package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
  134. package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
  135. package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
  136. package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
  137. package/docs/project/divergences.md +0 -98
  138. package/docs/project/dogfooding-docs-strategy.md +0 -66
  139. package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
  140. package/docs/project/lessons-learned.md +0 -1019
  141. package/docs/project/remove-role-styling-plan.md +0 -176
  142. package/examples/agent-improve-auth-flow.ts +0 -206
  143. package/examples/agent-loop.ts +0 -53
  144. package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
  145. package/llms.txt +0 -104
  146. package/skill-evals/README.md +0 -77
  147. package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
  148. package/skill-evals/fixtures/class-payment/input.mmd +0 -4
  149. package/skill-evals/fixtures/er-orders/input.mmd +0 -10
  150. package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
  151. package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
  152. package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
  153. package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
  154. package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
  155. package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
  156. package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
  157. package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
  158. package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
  159. package/skill-evals/shared-benchmark.json +0 -510
  160. package/src/agent/analyze.ts +0 -157
  161. package/src/agent/apply.ts +0 -146
  162. package/src/agent/architecture-body.ts +0 -458
  163. package/src/agent/certificates.ts +0 -110
  164. package/src/agent/class-body.ts +0 -319
  165. package/src/agent/core.ts +0 -207
  166. package/src/agent/create.ts +0 -106
  167. package/src/agent/describe.ts +0 -368
  168. package/src/agent/er-body.ts +0 -224
  169. package/src/agent/facts.ts +0 -360
  170. package/src/agent/families-builtin.ts +0 -920
  171. package/src/agent/families.ts +0 -232
  172. package/src/agent/family-layouts.ts +0 -832
  173. package/src/agent/flowchart-body.ts +0 -372
  174. package/src/agent/flowchart-unsupported.ts +0 -243
  175. package/src/agent/font-coverage.ts +0 -288
  176. package/src/agent/gantt-body.ts +0 -495
  177. package/src/agent/index.ts +0 -10
  178. package/src/agent/journey-body.ts +0 -374
  179. package/src/agent/label-metrics.ts +0 -28
  180. package/src/agent/layout-to-rendered.ts +0 -63
  181. package/src/agent/mutate.ts +0 -97
  182. package/src/agent/mutation-ops.ts +0 -51
  183. package/src/agent/op-schema.ts +0 -443
  184. package/src/agent/parse.ts +0 -223
  185. package/src/agent/pie-body.ts +0 -270
  186. package/src/agent/png.ts +0 -171
  187. package/src/agent/quadrant-body.ts +0 -336
  188. package/src/agent/quality.ts +0 -363
  189. package/src/agent/readability-audit.ts +0 -92
  190. package/src/agent/rendered-route-audit.ts +0 -61
  191. package/src/agent/sequence-body.ts +0 -345
  192. package/src/agent/serialize.ts +0 -214
  193. package/src/agent/state-body.ts +0 -589
  194. package/src/agent/structural-count.ts +0 -150
  195. package/src/agent/timeline-body.ts +0 -280
  196. package/src/agent/trace-log.ts +0 -43
  197. package/src/agent/types.ts +0 -995
  198. package/src/agent/verify.ts +0 -603
  199. package/src/agent/xychart-body.ts +0 -472
  200. package/src/architecture/config.ts +0 -282
  201. package/src/architecture/layout.ts +0 -496
  202. package/src/architecture/parser.ts +0 -375
  203. package/src/architecture/renderer.ts +0 -611
  204. package/src/architecture/types.ts +0 -113
  205. package/src/ascii/ansi.ts +0 -423
  206. package/src/ascii/architecture.ts +0 -210
  207. package/src/ascii/canvas.ts +0 -443
  208. package/src/ascii/class-diagram.ts +0 -697
  209. package/src/ascii/converter.ts +0 -475
  210. package/src/ascii/draw.ts +0 -1693
  211. package/src/ascii/edge-bundling.ts +0 -328
  212. package/src/ascii/edge-routing.ts +0 -330
  213. package/src/ascii/er-diagram.ts +0 -439
  214. package/src/ascii/gantt.ts +0 -276
  215. package/src/ascii/grid.ts +0 -747
  216. package/src/ascii/index.ts +0 -196
  217. package/src/ascii/journey.ts +0 -169
  218. package/src/ascii/meta.ts +0 -244
  219. package/src/ascii/multiline-utils.ts +0 -80
  220. package/src/ascii/pathfinder.ts +0 -301
  221. package/src/ascii/pie.ts +0 -67
  222. package/src/ascii/quadrant.ts +0 -158
  223. package/src/ascii/reverse.ts +0 -216
  224. package/src/ascii/sequence.ts +0 -499
  225. package/src/ascii/shapes/circle.ts +0 -27
  226. package/src/ascii/shapes/corners.ts +0 -143
  227. package/src/ascii/shapes/diamond.ts +0 -27
  228. package/src/ascii/shapes/hexagon.ts +0 -27
  229. package/src/ascii/shapes/index.ts +0 -106
  230. package/src/ascii/shapes/rectangle.ts +0 -168
  231. package/src/ascii/shapes/rounded.ts +0 -27
  232. package/src/ascii/shapes/special.ts +0 -332
  233. package/src/ascii/shapes/stadium.ts +0 -110
  234. package/src/ascii/shapes/state.ts +0 -192
  235. package/src/ascii/shapes/types.ts +0 -73
  236. package/src/ascii/timeline.ts +0 -141
  237. package/src/ascii/types.ts +0 -312
  238. package/src/ascii/validate.ts +0 -120
  239. package/src/ascii/width.ts +0 -81
  240. package/src/ascii/wrap.ts +0 -131
  241. package/src/ascii/xychart.ts +0 -919
  242. package/src/browser.ts +0 -29
  243. package/src/class/layout.ts +0 -240
  244. package/src/class/parser.ts +0 -332
  245. package/src/class/renderer.ts +0 -638
  246. package/src/class/types.ts +0 -129
  247. package/src/cli/agent-instructions.ts +0 -1
  248. package/src/cli/am-bin.ts +0 -6
  249. package/src/cli/exit-codes.ts +0 -18
  250. package/src/cli/index.ts +0 -1132
  251. package/src/cli/init-agent.ts +0 -147
  252. package/src/cli/run-entrypoint.ts +0 -10
  253. package/src/color-resolver.ts +0 -152
  254. package/src/elk-instance.ts +0 -114
  255. package/src/er/layout.ts +0 -187
  256. package/src/er/parser.ts +0 -226
  257. package/src/er/renderer.ts +0 -792
  258. package/src/er/types.ts +0 -99
  259. package/src/family-rubric.ts +0 -425
  260. package/src/font-manifest.ts +0 -31
  261. package/src/gantt/layout.ts +0 -365
  262. package/src/gantt/parser.ts +0 -282
  263. package/src/gantt/pipeline.ts +0 -27
  264. package/src/gantt/renderer.ts +0 -414
  265. package/src/gantt/schedule.ts +0 -513
  266. package/src/gantt/types.ts +0 -255
  267. package/src/index.ts +0 -368
  268. package/src/journey/layout.ts +0 -554
  269. package/src/journey/parse-core.ts +0 -251
  270. package/src/journey/parser.ts +0 -101
  271. package/src/journey/renderer.ts +0 -917
  272. package/src/journey/types.ts +0 -144
  273. package/src/layout/geometry.ts +0 -182
  274. package/src/layout/pass.ts +0 -142
  275. package/src/layout/passes/index.ts +0 -3916
  276. package/src/layout-engine.ts +0 -1565
  277. package/src/layout-rubric.ts +0 -659
  278. package/src/mcp/artifacts.ts +0 -137
  279. package/src/mcp/dynamic-harness.ts +0 -31
  280. package/src/mcp/dynamic-modules.d.ts +0 -7
  281. package/src/mcp/facade.ts +0 -608
  282. package/src/mcp/harness-runtime.ts +0 -203
  283. package/src/mcp/hosted-server.ts +0 -468
  284. package/src/mcp/mcp-bin.ts +0 -3
  285. package/src/mcp/mcp-cli.ts +0 -82
  286. package/src/mcp/protocol.ts +0 -13
  287. package/src/mcp/sandbox.ts +0 -195
  288. package/src/mcp/sdk-decl.ts +0 -391
  289. package/src/mcp/server.ts +0 -362
  290. package/src/mcp/tool-surface.ts +0 -146
  291. package/src/mermaid-source.ts +0 -655
  292. package/src/multiline-utils.ts +0 -228
  293. package/src/parser.ts +0 -1031
  294. package/src/pie/layout.ts +0 -168
  295. package/src/pie/parser.ts +0 -136
  296. package/src/pie/renderer.ts +0 -234
  297. package/src/pie/types.ts +0 -82
  298. package/src/quadrant/layout.ts +0 -191
  299. package/src/quadrant/parser.ts +0 -183
  300. package/src/quadrant/renderer.ts +0 -322
  301. package/src/quadrant/types.ts +0 -113
  302. package/src/render-family-hooks.ts +0 -244
  303. package/src/renderer.ts +0 -1099
  304. package/src/route-contracts.ts +0 -2723
  305. package/src/scene/backend.ts +0 -76
  306. package/src/scene/fidelity.ts +0 -183
  307. package/src/scene/hybrid-backend.ts +0 -174
  308. package/src/scene/ir.ts +0 -181
  309. package/src/scene/marks.ts +0 -127
  310. package/src/scene/rough-backend.ts +0 -378
  311. package/src/scene/seed.ts +0 -46
  312. package/src/scene/style-registry.ts +0 -556
  313. package/src/sequence/layout.ts +0 -456
  314. package/src/sequence/parser.ts +0 -255
  315. package/src/sequence/renderer.ts +0 -604
  316. package/src/sequence/types.ts +0 -154
  317. package/src/shape-clipping.ts +0 -351
  318. package/src/shared/accessibility-directives.ts +0 -22
  319. package/src/shared/batched.ts +0 -61
  320. package/src/shared/color-math.ts +0 -93
  321. package/src/shared/svg-a11y.ts +0 -29
  322. package/src/shared/syntax-error.ts +0 -17
  323. package/src/shared/unicode-ranges.ts +0 -135
  324. package/src/styles.ts +0 -318
  325. package/src/svg-paths.ts +0 -28
  326. package/src/text-metrics.ts +0 -248
  327. package/src/theme.ts +0 -710
  328. package/src/timeline/layout.ts +0 -381
  329. package/src/timeline/parser.ts +0 -215
  330. package/src/timeline/renderer.ts +0 -538
  331. package/src/timeline/types.ts +0 -105
  332. package/src/types.ts +0 -462
  333. package/src/website-worker-modules.d.ts +0 -18
  334. package/src/xychart/axis-utils.ts +0 -109
  335. package/src/xychart/colors.ts +0 -124
  336. package/src/xychart/config.ts +0 -88
  337. package/src/xychart/layout.ts +0 -656
  338. package/src/xychart/parser.ts +0 -504
  339. package/src/xychart/renderer.ts +0 -640
  340. package/src/xychart/types.ts +0 -282
@@ -1,443 +0,0 @@
1
- // ============================================================================
2
- // op-schema: shape validation for every family's structured mutation ops.
3
- //
4
- // The typed `mutate` surface is guaranteed by the compiler — but the MCP and
5
- // CLI boundaries hand it ops as untyped JSON, where those guarantees evaporate:
6
- // a model that writes `{kind:'add_class', name:'Duck'}` (using `name` where
7
- // `id` is expected) slips a shapeless object past the type system, and the
8
- // mutator silently produces `class undefined`. This layer restores the missing
9
- // check at the one place untyped ops arrive: it validates op SHAPE (field
10
- // presence, primitive type, enum membership) BEFORE the mutator runs, and
11
- // rejects a malformed op with a PRESCRIPTIVE INVALID_OP error that names the
12
- // offending field and lists the valid ones (with a nearest-match suggestion
13
- // for an obvious typo).
14
- //
15
- // Scope: shape only. Semantic checks — does the referenced class exist, is the
16
- // id already taken, is the index in range — stay in the existing mutators. The
17
- // two layers compose: shape first (here), semantics second (the mutator). The
18
- // field lists below are transcribed from the *MutationOp union types in
19
- // types.ts, so a valid typed op always passes; the whole unit suite exercising
20
- // the mutators through the checked core is the proof (see apply.ts).
21
- // ============================================================================
22
-
23
- import type { NodeShape, EdgeStyle } from '../types.ts'
24
- import type {
25
- SequenceMessageStyle, ClassRelationKind, ErCardinality, ArchitectureSide, GanttBodyTaskTag,
26
- } from './types.ts'
27
- import { unknownOpMessage } from './mutation-ops.ts'
28
- import type { MutableFamilyId } from './mutation-ops.ts'
29
-
30
- export type OpFamily = MutableFamilyId
31
-
32
- /** Primitive kinds a field may hold. `enum` carries an explicit value list;
33
- * `nullable` lets an enum/primitive also accept `null` (e.g. set_task_status). */
34
- type FieldType =
35
- | { type: 'string' }
36
- | { type: 'number' }
37
- | { type: 'boolean' }
38
- | { type: 'string-or-null' }
39
- | { type: 'enum'; values: readonly string[]; nullable?: boolean }
40
- | { type: 'string-array' }
41
- | { type: 'number-array' }
42
- | { type: 'object-or-null' } // a nested record (deep shape validated by the mutator)
43
-
44
- /** A field type plus its required flag. Intersection (not `extends`) because
45
- * FieldType is a union — an interface cannot extend a union. */
46
- type FieldSpec = FieldType & {
47
- /** required = must be present and non-undefined. Optional fields may be omitted. */
48
- required: boolean
49
- /** Optional human note surfaced in discovery: a value constraint the MUTATOR
50
- * (not this shape layer) enforces (e.g. "1..5"), or the default applied when
51
- * an optional field is omitted (e.g. "default: rectangle"). Descriptive only
52
- * — the mutator remains the single enforcement point. */
53
- note?: string
54
- }
55
-
56
- interface OpSpec {
57
- /** field-name -> spec. `kind` is implicit and never listed. */
58
- fields: Record<string, FieldSpec>
59
- /** Optional cross-field rule: at least one of these must be present (e.g.
60
- * architecture remove_edge accepts `index` OR `id`). */
61
- requireOneOf?: readonly string[]
62
- }
63
-
64
- // Enum value lists, kept in sync with the type aliases they mirror.
65
- const NODE_SHAPES: readonly NodeShape[] = [
66
- 'rectangle', 'service', 'rounded', 'diamond', 'stadium', 'circle', 'subroutine',
67
- 'doublecircle', 'hexagon', 'cylinder', 'asymmetric', 'trapezoid', 'trapezoid-alt',
68
- 'lean-r', 'lean-l', 'state-start', 'state-end',
69
- ]
70
- const EDGE_STYLES: readonly EdgeStyle[] = ['solid', 'dotted', 'thick', 'invisible']
71
- const SEQUENCE_MESSAGE_STYLES: readonly SequenceMessageStyle[] = ['sync', 'reply', 'async', 'async-dashed', 'lost', 'lost-dashed']
72
- const PARTICIPANT_KINDS = ['participant', 'actor'] as const
73
- const CLASS_REL_KINDS: readonly ClassRelationKind[] = ['inheritance', 'composition', 'aggregation', 'association', 'dependency', 'realization', 'link-solid', 'link-dashed']
74
- const ER_CARDINALITIES: readonly ErCardinality[] = ['one-only', 'zero-or-one', 'zero-or-many', 'one-or-many']
75
- const ARCH_SIDES: readonly ArchitectureSide[] = ['L', 'R', 'T', 'B']
76
- const SERIES_KINDS = ['bar', 'line'] as const
77
- const AXIS_KINDS = ['x', 'y'] as const
78
- const GANTT_STATUSES = ['active', 'done', 'crit'] as const
79
- const _GANTT_TAGS: readonly GanttBodyTaskTag[] = ['active', 'done', 'crit', 'milestone', 'vert'] // tags are shape-checked as string[]; deep enum check stays in the mutator
80
-
81
- // Field-spec shorthands.
82
- const str = (required = true): FieldSpec => ({ type: 'string', required })
83
- const num = (required = true): FieldSpec => ({ type: 'number', required })
84
- const bool = (required = true): FieldSpec => ({ type: 'boolean', required })
85
- const strOrNull = (required = true): FieldSpec => ({ type: 'string-or-null', required })
86
- const strArr = (required = true): FieldSpec => ({ type: 'string-array', required })
87
- const numArr = (required = true): FieldSpec => ({ type: 'number-array', required })
88
- const objOrNull = (required = true): FieldSpec => ({ type: 'object-or-null', required })
89
- const oneOf = (values: readonly string[], required = true, nullable = false): FieldSpec => ({ type: 'enum', values, nullable, required })
90
- /** Attach a discovery note (mutator-enforced constraint or omit-default) to a
91
- * field spec without changing its shape validation. */
92
- const withNote = (spec: FieldSpec, note: string): FieldSpec => ({ ...spec, note })
93
-
94
- // ---- Per-op schemas (transcribed from the *MutationOp unions in types.ts) ---
95
-
96
- const FLOWCHART_SCHEMA: Record<string, OpSpec> = {
97
- add_node: { fields: { id: str(), label: str(), shape: withNote(oneOf(NODE_SHAPES, false), 'default: rectangle'), parent: str(false) } },
98
- remove_node: { fields: { id: str() } },
99
- rename_node: { fields: { from: str(), to: str() } },
100
- set_label: { fields: { target: str(), label: str() } },
101
- add_edge: { fields: { from: str(), to: str(), label: str(false), style: withNote(oneOf(EDGE_STYLES, false), 'default: solid') } },
102
- remove_edge: { fields: { id: str() } },
103
- }
104
-
105
- const STATE_SCHEMA: Record<string, OpSpec> = {
106
- add_state: { fields: { id: str(), label: strOrNull(false), parent: strOrNull(false) } },
107
- remove_state: { fields: { id: str() } },
108
- rename_state: { fields: { from: str(), to: str() } },
109
- set_state_label: { fields: { id: str(), label: strOrNull() } },
110
- add_transition: { fields: { from: str(), to: str(), label: strOrNull(false), parent: strOrNull(false) } },
111
- remove_transition: { fields: { index: num(false), from: str(false), to: str(false), parent: strOrNull(false) }, requireOneOf: ['index', 'from', 'to'] },
112
- set_transition_label: { fields: { index: num(false), from: str(false), to: str(false), label: strOrNull(), parent: strOrNull(false) }, requireOneOf: ['index', 'from', 'to'] },
113
- make_composite: { fields: { id: str(), members: strArr(), label: strOrNull(false) } },
114
- }
115
-
116
- const SEQUENCE_SCHEMA: Record<string, OpSpec> = {
117
- add_participant: { fields: { id: str(), label: str(false), participantKind: oneOf(PARTICIPANT_KINDS, false) } },
118
- remove_participant: { fields: { id: str() } },
119
- add_message: { fields: { from: str(), to: str(), text: str(), style: withNote(oneOf(SEQUENCE_MESSAGE_STYLES, false), 'default: sync') } },
120
- remove_message: { fields: { index: num() } },
121
- set_message_text: { fields: { index: num(), text: str() } },
122
- }
123
-
124
- const TIMELINE_SCHEMA: Record<string, OpSpec> = {
125
- set_title: { fields: { title: strOrNull() } },
126
- add_section: { fields: { label: str() } },
127
- remove_section: { fields: { index: num() } },
128
- set_section_label: { fields: { index: num(), label: str() } },
129
- add_period: { fields: { sectionIndex: num(), label: str(), events: strArr(false) } },
130
- remove_period: { fields: { sectionIndex: num(), periodIndex: num() } },
131
- set_period_label: { fields: { sectionIndex: num(), periodIndex: num(), label: str() } },
132
- add_event: { fields: { sectionIndex: num(), periodIndex: num(), text: str() } },
133
- remove_event: { fields: { sectionIndex: num(), periodIndex: num(), eventIndex: num() } },
134
- set_event_text: { fields: { sectionIndex: num(), periodIndex: num(), eventIndex: num(), text: str() } },
135
- }
136
-
137
- const CLASS_SCHEMA: Record<string, OpSpec> = {
138
- set_title: { fields: { title: strOrNull() } },
139
- add_class: { fields: { id: str(), label: str(false), members: strArr(false) } },
140
- remove_class: { fields: { id: str() } },
141
- rename_class: { fields: { from: str(), to: str() } },
142
- add_member: { fields: { class: str(), text: str() } },
143
- remove_member: { fields: { class: str(), index: num() } },
144
- add_relation: { fields: { from: str(), to: str(), relKind: oneOf(CLASS_REL_KINDS), label: str(false) } },
145
- remove_relation: { fields: { index: num() } },
146
- add_note: { fields: { text: str(), for: str(false) } },
147
- remove_note: { fields: { index: num() } },
148
- }
149
-
150
- const ER_SCHEMA: Record<string, OpSpec> = {
151
- add_entity: { fields: { id: str(), attributes: strArr(false) } },
152
- remove_entity: { fields: { id: str() } },
153
- rename_entity: { fields: { from: str(), to: str() } },
154
- add_attribute: { fields: { entity: str(), text: str() } },
155
- remove_attribute: { fields: { entity: str(), index: num() } },
156
- add_relation: { fields: { from: str(), to: str(), leftCard: oneOf(ER_CARDINALITIES), rightCard: oneOf(ER_CARDINALITIES), dashed: bool(false), label: str(false) } },
157
- remove_relation: { fields: { index: num() } },
158
- }
159
-
160
- const JOURNEY_SCHEMA: Record<string, OpSpec> = {
161
- set_title: { fields: { title: strOrNull() } },
162
- add_section: { fields: { label: str(), index: withNote(num(false), 'insert position; omit to append') } },
163
- remove_section: { fields: { index: num() } },
164
- set_section_label: { fields: { index: num(), label: str() } },
165
- add_task: { fields: { sectionIndex: num(), text: str(), score: withNote(num(), 'integer 1..5'), actors: strArr(false), index: withNote(num(false), 'insert position; omit to append') } },
166
- remove_task: { fields: { sectionIndex: num(), taskIndex: num() } },
167
- set_task_text: { fields: { sectionIndex: num(), taskIndex: num(), text: str() } },
168
- set_task_score: { fields: { sectionIndex: num(), taskIndex: num(), score: withNote(num(), 'integer 1..5') } },
169
- set_task_actors: { fields: { sectionIndex: num(), taskIndex: num(), actors: strArr() } },
170
- rename_actor: { fields: { from: str(), to: str() } },
171
- move_task: { fields: { fromSection: num(), fromIndex: num(), toSection: num(), toIndex: withNote(num(), 'insert position in the target section, applied after removal') } },
172
- move_section: { fields: { from: num(), to: num() } },
173
- set_accessibility_title: { fields: { title: strOrNull() } },
174
- set_accessibility_description: { fields: { description: strOrNull() } },
175
- }
176
-
177
- const ARCHITECTURE_SCHEMA: Record<string, OpSpec> = {
178
- add_service: { fields: { id: str(), label: str(false), icon: strOrNull(false), group: strOrNull(false) } },
179
- remove_service: { fields: { id: str() } },
180
- rename_service: { fields: { from: str(), to: str() } },
181
- set_service_label: { fields: { id: str(), label: str() } },
182
- set_service_icon: { fields: { id: str(), icon: strOrNull() } },
183
- move_service: { fields: { id: str(), group: strOrNull() } },
184
- add_group: { fields: { id: str(), label: str(false), icon: strOrNull(false), parent: strOrNull(false) } },
185
- remove_group: { fields: { id: str() } },
186
- add_edge: { fields: { from: str(), to: str(), fromSide: oneOf(ARCH_SIDES), toSide: oneOf(ARCH_SIDES), label: strOrNull(false), hasArrowStart: bool(false), hasArrowEnd: bool(false) } },
187
- remove_edge: { fields: { index: num(false), id: str(false) }, requireOneOf: ['index', 'id'] },
188
- }
189
-
190
- const XYCHART_SCHEMA: Record<string, OpSpec> = {
191
- set_title: { fields: { title: strOrNull() } },
192
- set_x_axis: { fields: { axis: objOrNull() } },
193
- set_y_axis: { fields: { axis: objOrNull() } },
194
- add_series: { fields: { kind2: withNote(oneOf(SERIES_KINDS), 'series type — the field is named kind2, not kind'), name: strOrNull(false), values: numArr() } },
195
- remove_series: { fields: { index: num() } },
196
- set_series_values: { fields: { index: num(), values: numArr() } },
197
- set_series_name: { fields: { index: num(), name: strOrNull() } },
198
- reorder_series: { fields: { from: num(), to: num() } },
199
- }
200
-
201
- const PIE_SCHEMA: Record<string, OpSpec> = {
202
- set_title: { fields: { title: strOrNull() } },
203
- set_show_data: { fields: { showData: bool() } },
204
- add_slice: { fields: { label: str(), value: withNote(num(), '> 0, finite') } },
205
- remove_slice: { fields: { label: str() } },
206
- rename_slice: { fields: { from: str(), to: str() } },
207
- set_slice_value: { fields: { label: str(), value: withNote(num(), '> 0, finite') } },
208
- reorder_slice: { fields: { from: num(), to: num() } },
209
- }
210
-
211
- const QUADRANT_SCHEMA: Record<string, OpSpec> = {
212
- set_title: { fields: { title: strOrNull() } },
213
- set_axis_labels: { fields: { axis: oneOf(AXIS_KINDS), near: strOrNull(), far: strOrNull(false) } },
214
- set_quadrant_label: { fields: { quadrant: num(), label: strOrNull() } },
215
- add_point: { fields: { label: str(), x: withNote(num(), '0..1'), y: withNote(num(), '0..1') } },
216
- remove_point: { fields: { label: str() } },
217
- move_point: { fields: { label: str(), x: withNote(num(), '0..1'), y: withNote(num(), '0..1') } },
218
- rename_point: { fields: { from: str(), to: str() } },
219
- }
220
-
221
- const GANTT_SCHEMA: Record<string, OpSpec> = {
222
- set_title: { fields: { title: strOrNull() } },
223
- add_section: { fields: { label: str() } },
224
- rename_section: { fields: { index: num(), label: str() } },
225
- remove_section: { fields: { index: num() } },
226
- add_task: { fields: { sectionIndex: num(), label: str(), taskId: str(false), tags: strArr(false), start: withNote(str(false), 'date, or "after <taskId>"'), end: withNote(str(), 'date, a duration like "3d", or "until <taskId>"') } },
227
- remove_task: { fields: { sectionIndex: num(), taskIndex: num() } },
228
- rename_task: { fields: { sectionIndex: num(), taskIndex: num(), label: str() } },
229
- set_task_status: { fields: { sectionIndex: num(), taskIndex: num(), status: oneOf(GANTT_STATUSES, true, true) } },
230
- set_task_dates: { fields: { sectionIndex: num(), taskIndex: num(), start: strOrNull(false), end: str(false) } },
231
- }
232
-
233
- const SCHEMAS: Record<OpFamily, Record<string, OpSpec>> = {
234
- flowchart: FLOWCHART_SCHEMA,
235
- state: STATE_SCHEMA,
236
- sequence: SEQUENCE_SCHEMA,
237
- timeline: TIMELINE_SCHEMA,
238
- class: CLASS_SCHEMA,
239
- er: ER_SCHEMA,
240
- journey: JOURNEY_SCHEMA,
241
- architecture: ARCHITECTURE_SCHEMA,
242
- xychart: XYCHART_SCHEMA,
243
- pie: PIE_SCHEMA,
244
- quadrant: QUADRANT_SCHEMA,
245
- gantt: GANTT_SCHEMA,
246
- }
247
-
248
- /** True when `family` has a shape schema (i.e. is a mutable structured family). */
249
- export function hasOpSchema(family: string): family is OpFamily {
250
- return family in SCHEMAS
251
- }
252
-
253
- // ---- Prescriptive error ---------------------------------------------------
254
-
255
- /**
256
- * Structured, model-actionable validation error. `code` is always INVALID_OP
257
- * (so it flows through the existing MutationError channel unchanged), but the
258
- * payload names the exact problem and enumerates the legal fields so a weak
259
- * model can self-correct in one turn.
260
- */
261
- export interface OpValidationError {
262
- code: 'INVALID_OP'
263
- /** 'expected_single_op' | 'unknown_kind' | 'unknown_field' | 'missing_field' | 'wrong_type' | 'require_one_of' */
264
- reason: string
265
- message: string
266
- family: OpFamily
267
- opKind?: string
268
- /** the offending field, when applicable */
269
- field?: string
270
- /** valid field names for this op (or valid op kinds, for unknown_kind) */
271
- validFields?: string[]
272
- /** nearest valid name to a typo'd field/kind, when an obvious match exists */
273
- didYouMean?: string
274
- }
275
-
276
- function levenshtein(a: string, b: string): number {
277
- const m = a.length, n = b.length
278
- const d = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)])
279
- for (let j = 0; j <= n; j++) d[0]![j] = j
280
- for (let i = 1; i <= m; i++)
281
- for (let j = 1; j <= n; j++)
282
- d[i]![j] = Math.min(d[i - 1]![j]! + 1, d[i]![j - 1]! + 1, d[i - 1]![j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1))
283
- return d[m]![n]!
284
- }
285
-
286
- /** Nearest candidate within an edit-distance threshold (typo tolerance). */
287
- function nearest(target: string, candidates: string[]): string | undefined {
288
- let best: string | undefined
289
- let bestD = Infinity
290
- for (const c of candidates) {
291
- const dist = levenshtein(target.toLowerCase(), c.toLowerCase())
292
- if (dist < bestD) { bestD = dist; best = c }
293
- }
294
- // Only suggest when the match is close (<= ~40% of the longer string).
295
- return best !== undefined && bestD <= Math.max(2, Math.ceil(best.length * 0.4)) ? best : undefined
296
- }
297
-
298
- function typeName(f: FieldType): string {
299
- switch (f.type) {
300
- case 'string': return 'string'
301
- case 'number': return 'number'
302
- case 'boolean': return 'boolean'
303
- case 'string-or-null': return 'string | null'
304
- case 'string-array': return 'string[]'
305
- case 'number-array': return 'number[]'
306
- case 'object-or-null': return 'object | null'
307
- case 'enum': return `one of ${f.values.map(v => JSON.stringify(v)).join(', ')}${f.nullable ? ' | null' : ''}`
308
- }
309
- }
310
-
311
- function matchesType(spec: FieldType, value: unknown): boolean {
312
- switch (spec.type) {
313
- case 'string': return typeof value === 'string'
314
- case 'number': return typeof value === 'number' && Number.isFinite(value)
315
- case 'boolean': return typeof value === 'boolean'
316
- case 'string-or-null': return value === null || typeof value === 'string'
317
- case 'string-array': return Array.isArray(value) && value.every(v => typeof v === 'string')
318
- case 'number-array': return Array.isArray(value) && value.every(v => typeof v === 'number' && Number.isFinite(v))
319
- case 'object-or-null': return value === null || (typeof value === 'object' && !Array.isArray(value))
320
- case 'enum': return (spec.nullable === true && value === null) || (typeof value === 'string' && spec.values.includes(value))
321
- }
322
- }
323
-
324
- /**
325
- * Validate one op's SHAPE against its family schema. Returns null when valid.
326
- * The one and only shape choke point: every untyped edit path (declarative
327
- * applyOps / buildChecked and the Code Mode facade) calls this — through
328
- * mutateChecked — before touching a mutator.
329
- */
330
- export function validateOp(family: OpFamily, op: unknown): OpValidationError | null {
331
- const schema = SCHEMAS[family]
332
- // An op array is the common "apply these ops" slip — give it a dedicated
333
- // reason so a caller can branch on it, and a message that names the batch fix.
334
- if (Array.isArray(op)) {
335
- return { code: 'INVALID_OP', reason: 'expected_single_op', family, message: unknownOpMessage(family, op), validFields: Object.keys(schema) }
336
- }
337
- if (!op || typeof op !== 'object') {
338
- return { code: 'INVALID_OP', reason: 'unknown_kind', family, message: unknownOpMessage(family, op), validFields: Object.keys(schema) }
339
- }
340
- const rec = op as Record<string, unknown>
341
- const kind = rec.kind
342
- if (typeof kind !== 'string' || !(kind in schema)) {
343
- const validKinds = Object.keys(schema)
344
- const dym = typeof kind === 'string' ? nearest(kind, validKinds) : undefined
345
- // Reuse the mutator's own prescriptive phrasing ("valid ops: …") so the
346
- // message is identical no matter which layer catches the unknown kind; add
347
- // a typo suggestion on top.
348
- return {
349
- code: 'INVALID_OP', reason: 'unknown_kind', family, opKind: typeof kind === 'string' ? kind : undefined,
350
- message: `${unknownOpMessage(family, op)}${dym ? ` Did you mean "${dym}"?` : ''}`,
351
- validFields: validKinds, didYouMean: dym,
352
- }
353
- }
354
-
355
- const spec = schema[kind]!
356
- const validFields = Object.keys(spec.fields)
357
-
358
- // 1) Unknown / misnamed fields (this catches the `name`-instead-of-`id` bug).
359
- for (const key of Object.keys(rec)) {
360
- if (key === 'kind') continue
361
- if (!(key in spec.fields)) {
362
- const dym = nearest(key, validFields)
363
- return {
364
- code: 'INVALID_OP', reason: 'unknown_field', family, opKind: kind, field: key,
365
- message: `Unknown field "${key}" for ${family} op "${kind}".${dym ? ` Did you mean "${dym}"?` : ''} Valid fields: ${validFields.join(', ')}.`,
366
- validFields, didYouMean: dym,
367
- }
368
- }
369
- }
370
-
371
- // 2) Missing required fields.
372
- for (const [name, fspec] of Object.entries(spec.fields)) {
373
- if (!fspec.required) continue
374
- if (rec[name] === undefined) {
375
- return {
376
- code: 'INVALID_OP', reason: 'missing_field', family, opKind: kind, field: name,
377
- message: `Missing required field "${name}" (${typeName(fspec)}) for ${family} op "${kind}". Fields: ${validFields.join(', ')}.`,
378
- validFields,
379
- }
380
- }
381
- }
382
-
383
- // 3) requireOneOf (e.g. remove_edge needs index OR id).
384
- if (spec.requireOneOf && !spec.requireOneOf.some(f => rec[f] !== undefined)) {
385
- return {
386
- code: 'INVALID_OP', reason: 'require_one_of', family, opKind: kind,
387
- message: `${family} op "${kind}" requires at least one of: ${spec.requireOneOf.join(', ')}.`,
388
- validFields,
389
- }
390
- }
391
-
392
- // 4) Present fields must match their declared type.
393
- for (const [name, fspec] of Object.entries(spec.fields)) {
394
- if (rec[name] === undefined) continue
395
- if (!matchesType(fspec, rec[name])) {
396
- return {
397
- code: 'INVALID_OP', reason: 'wrong_type', family, opKind: kind, field: name,
398
- message: `Field "${name}" of ${family} op "${kind}" must be ${typeName(fspec)}, got ${JSON.stringify(rec[name])}.`,
399
- validFields,
400
- }
401
- }
402
- }
403
-
404
- return null
405
- }
406
-
407
- /** Machine-readable op menu for a family — { opKind: [fieldName, …] } — the
408
- * basis for tool-discovery output. Optional fields are marked with a trailing
409
- * `?`; the op's `kind` is implicit and omitted. */
410
- export function opMenu(family: OpFamily): Record<string, string[]> {
411
- const out: Record<string, string[]> = {}
412
- for (const [kind, spec] of Object.entries(SCHEMAS[family])) {
413
- out[kind] = Object.entries(spec.fields).map(([n, f]) => (f.required ? n : `${n}?`))
414
- }
415
- return out
416
- }
417
-
418
- /** One field of an op, as a model needs it to fill the op correctly: the name,
419
- * whether it is required, and a human type that spells out enum values inline
420
- * (e.g. `one of "inheritance", "composition", …`). */
421
- export interface OpFieldDoc { name: string; required: boolean; type: string; note?: string }
422
-
423
- /** Full field shapes for every op of a family — the thing a model must know to
424
- * author a correct op without guessing (which the prescriptive INVALID_OP error
425
- * only teaches AFTER a wrong guess). Surfaced in `am capabilities --json` and
426
- * the declarative MCP tool descriptions so field names, required-ness, and enum
427
- * vocabularies are discoverable up front. */
428
- export function describeOps(family: OpFamily): Record<string, OpFieldDoc[]> {
429
- const out: Record<string, OpFieldDoc[]> = {}
430
- for (const [kind, spec] of Object.entries(SCHEMAS[family])) {
431
- out[kind] = Object.entries(spec.fields).map(([name, f]) => ({ name, required: f.required, type: typeName(f), ...(f.note !== undefined ? { note: f.note } : {}) }))
432
- }
433
- return out
434
- }
435
-
436
- /** Compact one-line signatures for a family's ops — `add_relation(from, to,
437
- * relKind, label?)` — for embedding in a tool description. Optional fields carry
438
- * a trailing `?`; enum values are left to `describeOps`/the error to keep the
439
- * menu short. */
440
- export function opSignatures(family: OpFamily): string[] {
441
- const menu = opMenu(family)
442
- return Object.entries(menu).map(([kind, fields]) => `${kind}(${fields.join(', ')})`)
443
- }
@@ -1,223 +0,0 @@
1
- // ============================================================================
2
- // parseMermaid: source → ValidDiagram (multi-error).
3
- //
4
- // v4 sequence fidelity: parseSequenceBody returns null if it encounters ANY
5
- // non-blank, non-comment line it doesn't fully understand. parse then falls
6
- // back to an opaque body (lossless round-trip via canonicalSource) rather
7
- // than silently dropping the unrecognized construct.
8
- // ============================================================================
9
-
10
- import { normalizeMermaidSource, detectLooseDiagramTypeFromFirstLine } from '../mermaid-source.ts'
11
- import { getFamily } from './families.ts'
12
- import './families-builtin.ts' // registers built-in family parse/serialize/mutate hooks
13
- import { serializeMermaid } from './serialize.ts'
14
- import type {
15
- ValidDiagram, ParseError, Result, DiagramKind, ValidDiagramMeta,
16
- SourceMap, InitDirective, SourceComment,
17
- } from './types.ts'
18
- import { ok, err } from './types.ts'
19
-
20
- // Re-exports for callers/tests that used the previous in-tree parser homes.
21
- export { parseSequenceBody } from './sequence-body.ts'
22
- export { parseTimelineBody } from './timeline-body.ts'
23
-
24
- const FRONTMATTER_REGEX = /^?\s*---\s*\r?\n([\s\S]*?)\r?\n\s*---\s*(?:\r?\n|$)/
25
- const INIT_DIRECTIVE_REGEX = /^\s*%%\{\s*(?:init|initialize)\s*:\s*([\s\S]*?)\}\s*%%\s*(?:\r?\n|$)?/gm
26
- const COMMENT_LINE_REGEX = /^\s*%%(?!\{)\s*(.*)$/
27
- const ACC_TITLE_REGEX = /^\s*accTitle\s*:\s*(.+)$/i
28
- const ACC_DESCR_INLINE_REGEX = /^\s*accDescr\s*:\s*(.+)$/i
29
- const ACC_DESCR_BLOCK_START = /^\s*accDescr\s*\{\s*$/i
30
-
31
- export function parseMermaid(source: string): Result<ValidDiagram, ParseError[]> {
32
- const errors: ParseError[] = []
33
- // Normalize once and reuse its frontmatter in extractMeta, rather than
34
- // re-running the full preprocess a second time just for frontmatter.
35
- const normalized = normalizeMermaidSource(source)
36
- const meta = extractMeta(source, normalized.frontmatter)
37
- const canonicalSource = normalized.text
38
- // For opaque bodies, preserve original indentation/blank lines so the
39
- // serializer can re-emit the untouched body. canonicalSource at the
40
- // ValidDiagram level remains the normalized (line-trimmed) form for
41
- // back-compat with the existing flowchart/legacy paths.
42
- const opaqueSource = normalized.body
43
-
44
- if (normalized.lines.length === 0) {
45
- errors.push({ code: 'EMPTY', message: 'Empty diagram' })
46
- return err(errors)
47
- }
48
-
49
- const header = normalized.lines[0]!.toLowerCase()
50
- const kind = detectKind(header)
51
- if (!kind) {
52
- errors.push({ code: 'UNKNOWN_HEADER', message: `Unrecognized header: "${normalized.lines[0]}"`, line: 1 })
53
- return err(errors)
54
- }
55
-
56
- // BUILD-3: every family dispatches through its FamilyPlugin.parse hook —
57
- // structured-or-opaque for the narrow families, error semantics for
58
- // flowchart/state. Families without a hook stay source-level.
59
- const plugin = getFamily(kind)
60
- if (plugin?.parse) {
61
- const parsed = plugin.parse(normalized.lines, opaqueSource, meta, canonicalSource)
62
- if (!parsed.ok) {
63
- errors.push(...parsed.error)
64
- return err(errors)
65
- }
66
- const sourceMap = plugin.buildSourceMap?.(parsed.value, canonicalSource) ?? emptySourceMap()
67
- const diagram: ValidDiagram = { kind, meta, body: parsed.value, source: sourceMap, canonicalSource }
68
- markDroppedComments(diagram, normalized.body)
69
- return ok(diagram)
70
- }
71
-
72
- return ok<ValidDiagram>({
73
- kind, meta, body: { kind: 'opaque', family: kind, source: opaqueSource }, source: emptySourceMap(), canonicalSource,
74
- })
75
- }
76
-
77
- /**
78
- * 2C comment policy: structured bodies serialize to canonical source, which
79
- * does not model `%%` comment lines. Rather than dropping them *silently*,
80
- * diff the parsed comments against what actually survives serialization
81
- * (wrapper comments ride along verbatim via meta.wrapperSource; sequence
82
- * opaque segments may preserve in-body comments) and record the casualties so
83
- * verify can surface the Tier 3 COMMENT_DROPPED lint. Opaque bodies preserve
84
- * everything and never reach here.
85
- */
86
- function markDroppedComments(diagram: ValidDiagram, sourceBody: string): void {
87
- const comments = diagram.meta.comments
88
- if (diagram.body.kind === 'opaque' || comments.length === 0) return
89
-
90
- const sourceLines = sourceBody.split(/\r?\n/).map(line => line.trim())
91
- const serializedLines = serializeMermaid(diagram).split(/\r?\n/).map(line => line.trim())
92
- const keptSourceLines = longestCommonSubsequenceIndices(sourceLines, serializedLines)
93
- const keptCommentLines = new Set<number>()
94
- for (const sourceIndex of keptSourceLines) {
95
- if (COMMENT_LINE_REGEX.test(sourceLines[sourceIndex]!)) keptCommentLines.add(sourceIndex + 1)
96
- }
97
-
98
- const dropped: SourceComment[] = comments.filter(comment => !keptCommentLines.has(comment.line))
99
- if (dropped.length > 0) diagram.meta.droppedComments = dropped
100
- }
101
-
102
- function longestCommonSubsequenceIndices(a: string[], b: string[]): number[] {
103
- const dp: number[][] = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0))
104
- for (let i = a.length - 1; i >= 0; i--) {
105
- for (let j = b.length - 1; j >= 0; j--) {
106
- dp[i]![j] = a[i] === b[j]
107
- ? dp[i + 1]![j + 1]! + 1
108
- : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!)
109
- }
110
- }
111
- const indices: number[] = []
112
- let i = 0
113
- let j = 0
114
- while (i < a.length && j < b.length) {
115
- if (a[i] === b[j]) {
116
- indices.push(i)
117
- i++
118
- j++
119
- } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {
120
- i++
121
- } else {
122
- j++
123
- }
124
- }
125
- return indices
126
- }
127
-
128
- // ---- Family detection ----------------------------------------------------
129
- // Use the shared renderer detector for routed families; the agent layer only
130
- // splits state diagrams out from the renderer's flowchart route because state
131
- // and flowchart share the same legacy graph body but remain distinct families.
132
-
133
- function detectKind(header: string): DiagramKind | null {
134
- if (/^statediagram(?:-v2)?\s*$/i.test(header)) return 'state'
135
- const routed = detectLooseDiagramTypeFromFirstLine(header)
136
- if (!routed) return null
137
- return routed
138
- }
139
-
140
- // ---- Meta extraction -----------------------------------------------------
141
-
142
- function extractMeta(rawSource: string, frontmatter?: Record<string, unknown>): ValidDiagramMeta {
143
- const meta: ValidDiagramMeta = { initDirectives: [], comments: [], accessibility: {} }
144
-
145
- const wrapperSource = extractWrapperSource(rawSource)
146
- if (wrapperSource !== undefined) meta.wrapperSource = wrapperSource
147
-
148
- if (frontmatter && Object.keys(frontmatter).length > 0) {
149
- meta.frontmatter = frontmatter as ValidDiagramMeta['frontmatter']
150
- }
151
-
152
- const directiveRegex = new RegExp(INIT_DIRECTIVE_REGEX.source, 'gm')
153
- let m: RegExpExecArray | null
154
- while ((m = directiveRegex.exec(rawSource)) !== null) {
155
- meta.initDirectives.push({ raw: m[0], parsed: tryParseInitObject((m[1] ?? '').trim()) as InitDirective['parsed'] })
156
- }
157
-
158
- const stripped = rawSource
159
- .replace(FRONTMATTER_REGEX, '')
160
- .replace(new RegExp(INIT_DIRECTIVE_REGEX.source, 'gm'), '')
161
- const lines = stripped.split(/\r?\n/)
162
- let inAccDescr = false
163
- let buf: string[] = []
164
- for (let i = 0; i < lines.length; i++) {
165
- const line = lines[i]!
166
- if (inAccDescr) {
167
- if (/^\s*\}\s*$/.test(line)) { meta.accessibility.descr = buf.join('\n').trim(); inAccDescr = false; buf = [] }
168
- else buf.push(line)
169
- continue
170
- }
171
- if (ACC_DESCR_BLOCK_START.test(line)) { inAccDescr = true; continue }
172
- const at = line.match(ACC_TITLE_REGEX)
173
- if (at) { meta.accessibility.title = at[1]!.trim(); continue }
174
- const ad = line.match(ACC_DESCR_INLINE_REGEX)
175
- if (ad) { meta.accessibility.descr = ad[1]!.trim(); continue }
176
- const cm = line.match(COMMENT_LINE_REGEX)
177
- if (cm) meta.comments.push({ text: cm[1]!, line: i + 1 })
178
- }
179
-
180
- return meta
181
- }
182
-
183
- /**
184
- * The leading source wrapper, byte-verbatim: an optional frontmatter block
185
- * followed by any run of blank lines, `%%` comments, and `%%{init}%%`
186
- * directives, up to (excluding) the diagram header line. This is what
187
- * serializeMermaid re-emits untouched by default (1C wrapper policy).
188
- */
189
- function extractWrapperSource(rawSource: string): string | undefined {
190
- let pos = 0
191
- const fm = rawSource.match(FRONTMATTER_REGEX)
192
- if (fm) pos = fm[0].length
193
- const directiveAtStart = new RegExp(INIT_DIRECTIVE_REGEX.source)
194
- for (;;) {
195
- const rest = rawSource.slice(pos)
196
- if (rest.length === 0) break
197
- const dm = rest.match(directiveAtStart)
198
- if (dm && dm.index === 0 && dm[0].length > 0) { pos += dm[0].length; continue }
199
- const lineEnd = rest.indexOf('\n')
200
- const line = lineEnd === -1 ? rest : rest.slice(0, lineEnd)
201
- if (/^\s*$/.test(line) || COMMENT_LINE_REGEX.test(line)) {
202
- pos += lineEnd === -1 ? rest.length : lineEnd + 1
203
- continue
204
- }
205
- break
206
- }
207
- return pos > 0 ? rawSource.slice(0, pos) : undefined
208
- }
209
-
210
- function tryParseInitObject(inner: string): Record<string, unknown> {
211
- try { return JSON.parse(inner) as Record<string, unknown> }
212
- catch {
213
- try {
214
- const quoted = inner.replace(/([{,]\s*)([A-Za-z_$][\w$]*)\s*:/g, '$1"$2":')
215
- return JSON.parse(quoted) as Record<string, unknown>
216
- } catch { return {} }
217
- }
218
- }
219
-
220
- function emptySourceMap(): SourceMap {
221
- return { nodes: new Map(), edges: new Map(), groups: new Map(), labels: new Map() }
222
- }
223
-