agentic-mermaid 0.1.1 → 0.1.2

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 (336) hide show
  1. package/CHANGELOG.md +144 -4
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/README.md +25 -32
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/dist/agent-core.d.ts +463 -0
  6. package/dist/agent-core.js +397 -0
  7. package/dist/agent.d.ts +73 -1868
  8. package/dist/agent.js +336 -37325
  9. package/dist/agentic-mermaid-mcp.js +7 -38399
  10. package/dist/am.js +33632 -40445
  11. package/dist/chunk-4QECACIA.js +4702 -0
  12. package/dist/chunk-CDPKJ2BQ.js +37 -0
  13. package/dist/chunk-QKDLMOJJ.js +3487 -0
  14. package/dist/chunk-WI35L7IR.js +615 -0
  15. package/dist/chunk-WN3YDOBG.js +149 -0
  16. package/dist/chunk-ZYMMKQ3I.js +62637 -0
  17. package/dist/index-D54dqSdC.d.ts +6777 -0
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +310 -33014
  20. package/dist/mcp-cli-SBQSTJFC.js +17 -0
  21. package/docs/README.md +14 -6
  22. package/docs/schemas/style-spec.schema.json +1221 -30
  23. package/examples/styles/catalog.json +54 -0
  24. package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
  25. package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
  26. package/examples/styles/cupertino-prototype.mmd +10 -0
  27. package/examples/styles/cupertino-prototype.style.json +63 -0
  28. package/examples/styles/mid-century-report.style.json +2 -2
  29. package/examples/styles/star-chart-atlas.style.json +2 -1
  30. package/examples/styles/transit-route-map.style.json +2 -2
  31. package/examples/styles/vercel-inspired-prototype.mmd +6 -0
  32. package/examples/styles/vercel-inspired-prototype.style.json +87 -0
  33. package/package.json +68 -43
  34. package/server.json +2 -2
  35. package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
  36. package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
  37. package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
  38. package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
  39. package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
  40. package/AGENT_NATIVE.md +0 -607
  41. package/Instructions_for_agents.md +0 -89
  42. package/TODO.md +0 -684
  43. package/assets/hero.png +0 -0
  44. package/dist/index-wjRjbsP-.d.ts +0 -1102
  45. package/docs/agent-api-cookbook.md +0 -308
  46. package/docs/agent-mutation-policy.md +0 -104
  47. package/docs/agent-workflow-examples.md +0 -46
  48. package/docs/api.md +0 -211
  49. package/docs/ascii.md +0 -103
  50. package/docs/assets/improvements/box-start-connector.png +0 -0
  51. package/docs/assets/improvements/comment-dropped-lint.png +0 -0
  52. package/docs/assets/improvements/er-content-loss.png +0 -0
  53. package/docs/assets/improvements/fan-in-trunks.png +0 -0
  54. package/docs/assets/improvements/gantt-family.png +0 -0
  55. package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
  56. package/docs/assets/improvements/gantt-verify-seam.png +0 -0
  57. package/docs/assets/improvements/subgraph-container-edges.png +0 -0
  58. package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
  59. package/docs/assets/improvements/wrapper-init-directive.png +0 -0
  60. package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
  61. package/docs/assets/style-cookbook/mid-century-report.png +0 -0
  62. package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
  63. package/docs/assets/style-cookbook/transit-route-map.png +0 -0
  64. package/docs/comparison.md +0 -94
  65. package/docs/config.md +0 -105
  66. package/docs/contributing/adding-diagram-types.md +0 -144
  67. package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
  68. package/docs/contributing/diagram-family-citizenship.md +0 -82
  69. package/docs/contributing/harvesting-upstream-tests.md +0 -126
  70. package/docs/contributing/lessons-learned.md +0 -276
  71. package/docs/contributing/releasing.md +0 -82
  72. package/docs/contributing/visual-review-evidence.md +0 -53
  73. package/docs/custom-style-cookbook.md +0 -154
  74. package/docs/design/families/README.md +0 -26
  75. package/docs/design/families/architecture-beta.md +0 -48
  76. package/docs/design/families/flowchart-parser-conformance.md +0 -22
  77. package/docs/design/families/gantt-research.md +0 -137
  78. package/docs/design/families/gantt.md +0 -329
  79. package/docs/design/families/journey-agentic-cards-before.png +0 -0
  80. package/docs/design/families/journey-experience-curve-implementation.png +0 -0
  81. package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
  82. package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
  83. package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
  84. package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
  85. package/docs/design/families/journey-migration-parity.md +0 -439
  86. package/docs/design/families/journey-section-overlap-after.png +0 -0
  87. package/docs/design/families/journey-section-overlap-before.png +0 -0
  88. package/docs/design/families/journey-usage-research.md +0 -423
  89. package/docs/design/families/journey.md +0 -153
  90. package/docs/design/families/xychart.md +0 -85
  91. package/docs/design/system/README.md +0 -45
  92. package/docs/design/system/abstraction-audit.md +0 -224
  93. package/docs/design/system/abstraction-recommendations.md +0 -655
  94. package/docs/design/system/architecture.mmd +0 -29
  95. package/docs/design/system/architecture.svg +0 -137
  96. package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
  97. package/docs/design/system/layout-rubric.md +0 -138
  98. package/docs/design/system/route-contracts.md +0 -848
  99. package/docs/design/system/source-preservation-ladder.md +0 -52
  100. package/docs/design/system/styles-rollout.md +0 -216
  101. package/docs/design/system/ugly-layouts.md +0 -119
  102. package/docs/diagram-families.md +0 -193
  103. package/docs/features.md +0 -147
  104. package/docs/fork-differences.md +0 -130
  105. package/docs/getting-started.md +0 -175
  106. package/docs/issue-derived-test-cases.md +0 -156
  107. package/docs/layout-characterization/README.md +0 -245
  108. package/docs/layout-characterization/contact-sheet-families.md +0 -498
  109. package/docs/layout-characterization/contact-sheet.md +0 -638
  110. package/docs/layout-characterization/properties.md +0 -281
  111. package/docs/layout-characterization/visual-quality.md +0 -160
  112. package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
  113. package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
  114. package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
  115. package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
  116. package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
  117. package/docs/layout-characterization/visual-snapshots/journey.svg +0 -98
  118. package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
  119. package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
  120. package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
  121. package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
  122. package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
  123. package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
  124. package/docs/mcp-code-mode-rationale.md +0 -101
  125. package/docs/mcp-http-transport.md +0 -223
  126. package/docs/mermaid-layout-complaints.md +0 -837
  127. package/docs/mutation-testing.md +0 -206
  128. package/docs/project/divergences.md +0 -98
  129. package/docs/project/dogfooding-docs-strategy.md +0 -66
  130. package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
  131. package/docs/project/lessons-learned.md +0 -1019
  132. package/docs/project/remove-role-styling-plan.md +0 -176
  133. package/docs/quality.md +0 -215
  134. package/docs/react.md +0 -97
  135. package/docs/style-authoring.md +0 -133
  136. package/docs/testing-strategy.md +0 -335
  137. package/docs/theming.md +0 -162
  138. package/examples/agent-improve-auth-flow.ts +0 -206
  139. package/examples/agent-loop.ts +0 -53
  140. package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
  141. package/llms.txt +0 -104
  142. package/skill-evals/README.md +0 -77
  143. package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
  144. package/skill-evals/fixtures/class-payment/input.mmd +0 -4
  145. package/skill-evals/fixtures/er-orders/input.mmd +0 -10
  146. package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
  147. package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
  148. package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
  149. package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
  150. package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
  151. package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
  152. package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
  153. package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
  154. package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
  155. package/skill-evals/shared-benchmark.json +0 -510
  156. package/src/agent/analyze.ts +0 -157
  157. package/src/agent/apply.ts +0 -146
  158. package/src/agent/architecture-body.ts +0 -458
  159. package/src/agent/certificates.ts +0 -110
  160. package/src/agent/class-body.ts +0 -319
  161. package/src/agent/core.ts +0 -207
  162. package/src/agent/create.ts +0 -106
  163. package/src/agent/describe.ts +0 -368
  164. package/src/agent/er-body.ts +0 -224
  165. package/src/agent/facts.ts +0 -360
  166. package/src/agent/families-builtin.ts +0 -920
  167. package/src/agent/families.ts +0 -232
  168. package/src/agent/family-layouts.ts +0 -832
  169. package/src/agent/flowchart-body.ts +0 -372
  170. package/src/agent/flowchart-unsupported.ts +0 -243
  171. package/src/agent/font-coverage.ts +0 -288
  172. package/src/agent/gantt-body.ts +0 -495
  173. package/src/agent/index.ts +0 -10
  174. package/src/agent/journey-body.ts +0 -374
  175. package/src/agent/label-metrics.ts +0 -28
  176. package/src/agent/layout-to-rendered.ts +0 -63
  177. package/src/agent/mutate.ts +0 -97
  178. package/src/agent/mutation-ops.ts +0 -51
  179. package/src/agent/op-schema.ts +0 -443
  180. package/src/agent/parse.ts +0 -223
  181. package/src/agent/pie-body.ts +0 -270
  182. package/src/agent/png.ts +0 -171
  183. package/src/agent/quadrant-body.ts +0 -336
  184. package/src/agent/quality.ts +0 -363
  185. package/src/agent/readability-audit.ts +0 -92
  186. package/src/agent/rendered-route-audit.ts +0 -61
  187. package/src/agent/sequence-body.ts +0 -345
  188. package/src/agent/serialize.ts +0 -214
  189. package/src/agent/state-body.ts +0 -589
  190. package/src/agent/structural-count.ts +0 -150
  191. package/src/agent/timeline-body.ts +0 -280
  192. package/src/agent/trace-log.ts +0 -43
  193. package/src/agent/types.ts +0 -995
  194. package/src/agent/verify.ts +0 -603
  195. package/src/agent/xychart-body.ts +0 -472
  196. package/src/architecture/config.ts +0 -282
  197. package/src/architecture/layout.ts +0 -496
  198. package/src/architecture/parser.ts +0 -375
  199. package/src/architecture/renderer.ts +0 -611
  200. package/src/architecture/types.ts +0 -113
  201. package/src/ascii/ansi.ts +0 -423
  202. package/src/ascii/architecture.ts +0 -210
  203. package/src/ascii/canvas.ts +0 -443
  204. package/src/ascii/class-diagram.ts +0 -697
  205. package/src/ascii/converter.ts +0 -475
  206. package/src/ascii/draw.ts +0 -1693
  207. package/src/ascii/edge-bundling.ts +0 -328
  208. package/src/ascii/edge-routing.ts +0 -330
  209. package/src/ascii/er-diagram.ts +0 -439
  210. package/src/ascii/gantt.ts +0 -276
  211. package/src/ascii/grid.ts +0 -747
  212. package/src/ascii/index.ts +0 -196
  213. package/src/ascii/journey.ts +0 -169
  214. package/src/ascii/meta.ts +0 -244
  215. package/src/ascii/multiline-utils.ts +0 -80
  216. package/src/ascii/pathfinder.ts +0 -301
  217. package/src/ascii/pie.ts +0 -67
  218. package/src/ascii/quadrant.ts +0 -158
  219. package/src/ascii/reverse.ts +0 -216
  220. package/src/ascii/sequence.ts +0 -499
  221. package/src/ascii/shapes/circle.ts +0 -27
  222. package/src/ascii/shapes/corners.ts +0 -143
  223. package/src/ascii/shapes/diamond.ts +0 -27
  224. package/src/ascii/shapes/hexagon.ts +0 -27
  225. package/src/ascii/shapes/index.ts +0 -106
  226. package/src/ascii/shapes/rectangle.ts +0 -168
  227. package/src/ascii/shapes/rounded.ts +0 -27
  228. package/src/ascii/shapes/special.ts +0 -332
  229. package/src/ascii/shapes/stadium.ts +0 -110
  230. package/src/ascii/shapes/state.ts +0 -192
  231. package/src/ascii/shapes/types.ts +0 -73
  232. package/src/ascii/timeline.ts +0 -141
  233. package/src/ascii/types.ts +0 -312
  234. package/src/ascii/validate.ts +0 -120
  235. package/src/ascii/width.ts +0 -81
  236. package/src/ascii/wrap.ts +0 -131
  237. package/src/ascii/xychart.ts +0 -919
  238. package/src/browser.ts +0 -29
  239. package/src/class/layout.ts +0 -240
  240. package/src/class/parser.ts +0 -332
  241. package/src/class/renderer.ts +0 -638
  242. package/src/class/types.ts +0 -129
  243. package/src/cli/agent-instructions.ts +0 -1
  244. package/src/cli/am-bin.ts +0 -6
  245. package/src/cli/exit-codes.ts +0 -18
  246. package/src/cli/index.ts +0 -1132
  247. package/src/cli/init-agent.ts +0 -147
  248. package/src/cli/run-entrypoint.ts +0 -10
  249. package/src/color-resolver.ts +0 -152
  250. package/src/elk-instance.ts +0 -114
  251. package/src/er/layout.ts +0 -187
  252. package/src/er/parser.ts +0 -226
  253. package/src/er/renderer.ts +0 -792
  254. package/src/er/types.ts +0 -99
  255. package/src/family-rubric.ts +0 -425
  256. package/src/font-manifest.ts +0 -31
  257. package/src/gantt/layout.ts +0 -365
  258. package/src/gantt/parser.ts +0 -282
  259. package/src/gantt/pipeline.ts +0 -27
  260. package/src/gantt/renderer.ts +0 -414
  261. package/src/gantt/schedule.ts +0 -513
  262. package/src/gantt/types.ts +0 -255
  263. package/src/index.ts +0 -368
  264. package/src/journey/layout.ts +0 -554
  265. package/src/journey/parse-core.ts +0 -251
  266. package/src/journey/parser.ts +0 -101
  267. package/src/journey/renderer.ts +0 -917
  268. package/src/journey/types.ts +0 -144
  269. package/src/layout/geometry.ts +0 -182
  270. package/src/layout/pass.ts +0 -142
  271. package/src/layout/passes/index.ts +0 -3916
  272. package/src/layout-engine.ts +0 -1565
  273. package/src/layout-rubric.ts +0 -659
  274. package/src/mcp/artifacts.ts +0 -137
  275. package/src/mcp/dynamic-harness.ts +0 -31
  276. package/src/mcp/dynamic-modules.d.ts +0 -7
  277. package/src/mcp/facade.ts +0 -608
  278. package/src/mcp/harness-runtime.ts +0 -203
  279. package/src/mcp/hosted-server.ts +0 -468
  280. package/src/mcp/mcp-bin.ts +0 -3
  281. package/src/mcp/mcp-cli.ts +0 -82
  282. package/src/mcp/protocol.ts +0 -13
  283. package/src/mcp/sandbox.ts +0 -195
  284. package/src/mcp/sdk-decl.ts +0 -391
  285. package/src/mcp/server.ts +0 -362
  286. package/src/mcp/tool-surface.ts +0 -146
  287. package/src/mermaid-source.ts +0 -655
  288. package/src/multiline-utils.ts +0 -228
  289. package/src/parser.ts +0 -1031
  290. package/src/pie/layout.ts +0 -168
  291. package/src/pie/parser.ts +0 -136
  292. package/src/pie/renderer.ts +0 -234
  293. package/src/pie/types.ts +0 -82
  294. package/src/quadrant/layout.ts +0 -191
  295. package/src/quadrant/parser.ts +0 -183
  296. package/src/quadrant/renderer.ts +0 -322
  297. package/src/quadrant/types.ts +0 -113
  298. package/src/render-family-hooks.ts +0 -244
  299. package/src/renderer.ts +0 -1099
  300. package/src/route-contracts.ts +0 -2723
  301. package/src/scene/backend.ts +0 -76
  302. package/src/scene/fidelity.ts +0 -183
  303. package/src/scene/hybrid-backend.ts +0 -174
  304. package/src/scene/ir.ts +0 -181
  305. package/src/scene/marks.ts +0 -127
  306. package/src/scene/rough-backend.ts +0 -378
  307. package/src/scene/seed.ts +0 -46
  308. package/src/scene/style-registry.ts +0 -556
  309. package/src/sequence/layout.ts +0 -456
  310. package/src/sequence/parser.ts +0 -255
  311. package/src/sequence/renderer.ts +0 -604
  312. package/src/sequence/types.ts +0 -154
  313. package/src/shape-clipping.ts +0 -351
  314. package/src/shared/accessibility-directives.ts +0 -22
  315. package/src/shared/batched.ts +0 -61
  316. package/src/shared/color-math.ts +0 -93
  317. package/src/shared/svg-a11y.ts +0 -29
  318. package/src/shared/syntax-error.ts +0 -17
  319. package/src/shared/unicode-ranges.ts +0 -135
  320. package/src/styles.ts +0 -318
  321. package/src/svg-paths.ts +0 -28
  322. package/src/text-metrics.ts +0 -248
  323. package/src/theme.ts +0 -710
  324. package/src/timeline/layout.ts +0 -381
  325. package/src/timeline/parser.ts +0 -215
  326. package/src/timeline/renderer.ts +0 -538
  327. package/src/timeline/types.ts +0 -105
  328. package/src/types.ts +0 -462
  329. package/src/website-worker-modules.d.ts +0 -18
  330. package/src/xychart/axis-utils.ts +0 -109
  331. package/src/xychart/colors.ts +0 -124
  332. package/src/xychart/config.ts +0 -88
  333. package/src/xychart/layout.ts +0 -656
  334. package/src/xychart/parser.ts +0 -504
  335. package/src/xychart/renderer.ts +0 -640
  336. 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
-