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,374 +0,0 @@
1
- // ============================================================================
2
- // Journey structured body (BUILD-15 pilot for promoting source-level families).
3
- //
4
- // The grammar lives in src/journey/parse-core.ts and is shared with the
5
- // renderer parser, so the two surfaces cannot drift on documented syntax:
6
- // title <text>
7
- // accTitle: <text>
8
- // accDescr: <text>
9
- // accDescr { <multiline text> }
10
- // section <label>
11
- // <task text>: <score 1..5>[: <actor>[, <actor>…]]
12
- //
13
- // Structured-or-opaque with a typed reason: any line the grammar rejects
14
- // yields the JourneyParseIssue that triggered opacity, so the caller can fall
15
- // back to a lossless opaque body AND verify can say why. Render support stays
16
- // unchanged — the legacy renderer keeps parsing canonical source.
17
- // ============================================================================
18
-
19
- import { unknownOpMessage } from './mutation-ops.ts'
20
- import type {
21
- JourneyBody, JourneySection, JourneyTask, JourneyMutationOp,
22
- MutationError, Result, LayoutWarning, VerifyOptions,
23
- } from './types.ts'
24
- import { ok, err, DEFAULT_LABEL_CHAR_CAP } from './types.ts'
25
- import { labelOverflowWarning } from './label-metrics.ts'
26
- import {
27
- walkJourneyLines, normalizeJourneyText, normalizeJourneyActor,
28
- isValidJourneyScore, type JourneyParseIssue,
29
- } from '../journey/parse-core.ts'
30
-
31
- // ---- Parser -----------------------------------------------------------------
32
-
33
- export type JourneyBodyParse =
34
- | { ok: true; body: JourneyBody }
35
- | { ok: false; issue: JourneyParseIssue }
36
-
37
- function formatJourneyInline(value: string): string {
38
- return value.split(/\r?\n/).map(part => part.trim()).join('<br>')
39
- }
40
-
41
- /**
42
- * Parse journey body lines (header excluded). Returns a structured body only
43
- * if EVERY non-blank, non-comment statement is modeled grammar with a valid
44
- * 1..5 integer score. Otherwise returns the first JourneyParseIssue so the
45
- * opaque fallback can carry its reason.
46
- */
47
- export function parseJourneyBody(lines: string[]): JourneyBodyParse {
48
- const body: JourneyBody = { kind: 'journey', sections: [] }
49
- let currentSection: JourneySection | undefined
50
- let sIdx = 0, tIdx = 0
51
- let firstIssue: JourneyParseIssue | undefined
52
-
53
- const implicitSection = (): JourneySection => {
54
- if (!currentSection) {
55
- currentSection = { id: `section-${sIdx++}`, tasks: [] }
56
- body.sections.push(currentSection)
57
- }
58
- return currentSection
59
- }
60
-
61
- walkJourneyLines(lines, 0, {
62
- title: text => { body.title = text },
63
- accTitle: text => { body.accessibilityTitle = text },
64
- accDescr: text => { body.accessibilityDescription = text },
65
- section: label => {
66
- currentSection = { id: `section-${sIdx++}`, label, tasks: [] }
67
- body.sections.push(currentSection)
68
- },
69
- task: (text, score, actors) => {
70
- implicitSection().tasks.push({ id: `task-${tIdx++}`, text, score, actors })
71
- },
72
- issue: issue => {
73
- firstIssue = issue
74
- return 'stop'
75
- },
76
- })
77
-
78
- if (firstIssue) return { ok: false, issue: firstIssue }
79
-
80
- // Upstream parity: title/acc metadata-only journeys are renderable header
81
- // furniture. A truly empty journey still stays opaque for source fidelity.
82
- if (body.sections.length === 0 && body.sections.every(s => s.tasks.length === 0) && !body.title && !body.accessibilityTitle && !body.accessibilityDescription) {
83
- return {
84
- ok: false,
85
- issue: { code: 'empty_journey', lineIndex: 0, statement: '', detail: 'Journey has no title, sections, or scored tasks' },
86
- }
87
- }
88
-
89
- return { ok: true, body }
90
- }
91
-
92
- // ---- Serializer -------------------------------------------------------------
93
-
94
- export function renderJourney(body: JourneyBody): string {
95
- const lines: string[] = ['journey']
96
- if (body.title) lines.push(` title ${formatJourneyInline(body.title)}`)
97
- if (body.accessibilityTitle) lines.push(` accTitle: ${formatJourneyInline(body.accessibilityTitle)}`)
98
- if (body.accessibilityDescription) {
99
- if (body.accessibilityDescription.includes('\n')) {
100
- lines.push(' accDescr {')
101
- for (const line of body.accessibilityDescription.split(/\r?\n/)) {
102
- lines.push(` ${line.trim()}`)
103
- }
104
- lines.push(' }')
105
- } else {
106
- lines.push(` accDescr: ${formatJourneyInline(body.accessibilityDescription)}`)
107
- }
108
- }
109
- for (const section of body.sections) {
110
- if (section.label !== undefined) lines.push(` section ${formatJourneyInline(section.label)}`)
111
- for (const task of section.tasks) {
112
- const actors = task.actors.length > 0 ? `: ${task.actors.join(', ')}` : ''
113
- lines.push(` ${formatJourneyInline(task.text)}: ${task.score}${actors}`)
114
- }
115
- }
116
- return lines.join('\n') + '\n'
117
- }
118
-
119
- // ---- Mutator ----------------------------------------------------------------
120
-
121
- function cloneJourney(b: JourneyBody): JourneyBody {
122
- return {
123
- kind: 'journey',
124
- title: b.title,
125
- accessibilityTitle: b.accessibilityTitle,
126
- accessibilityDescription: b.accessibilityDescription,
127
- sections: b.sections.map(s => ({
128
- id: s.id, label: s.label,
129
- tasks: s.tasks.map(t => ({ id: t.id, text: t.text, score: t.score, actors: [...t.actors] })),
130
- })),
131
- }
132
- }
133
-
134
- function makeIdAllocator(body: JourneyBody): (prefix: 'section' | 'task') => string {
135
- const seen = new Set<string>()
136
- for (const s of body.sections) { seen.add(s.id); for (const t of s.tasks) seen.add(t.id) }
137
- return prefix => {
138
- let n = 0
139
- while (seen.has(`${prefix}-${n}`)) n++
140
- const id = `${prefix}-${n}`
141
- seen.add(id)
142
- return id
143
- }
144
- }
145
-
146
- function normalizeOpText(value: string, field: string, opts: { allowColon?: boolean } = {}): Result<string, MutationError> {
147
- if (typeof value !== 'string') return err({ code: 'INVALID_OP', message: `Journey ${field} must be a string` })
148
- const normalized = normalizeJourneyText(value)
149
- if (!normalized || (!opts.allowColon && normalized.includes(':'))) {
150
- return err({ code: 'INVALID_OP', message: `Journey ${field} must be non-empty and must not contain :` })
151
- }
152
- return ok(normalized)
153
- }
154
-
155
- function resolveInsertIndex(index: number | undefined, length: number): Result<number, MutationError> {
156
- if (index === undefined) return ok(length)
157
- if (!Number.isInteger(index) || index < 0 || index > length) {
158
- return err({ code: 'INVALID_OP', message: `Journey insert index ${index} out of range (0..${length})` })
159
- }
160
- return ok(index)
161
- }
162
-
163
- /** Accessibility text is line-oriented free text, but `;` terminates a
164
- * journey statement and `{`/`}` delimit the accDescr block form — text
165
- * carrying them would not survive serialize → re-parse. null clears. */
166
- function normalizeAccessibilityOpText(
167
- value: string | null,
168
- field: string,
169
- ): Result<string | undefined, MutationError> {
170
- if (value === null) return ok(undefined)
171
- if (typeof value !== 'string') return err({ code: 'INVALID_OP', message: `Journey ${field} must be a string or null` })
172
- const normalized = normalizeJourneyText(value)
173
- if (!normalized || /[;{}]/.test(normalized)) {
174
- return err({ code: 'INVALID_OP', message: `Journey ${field} must be non-empty and must not contain ;, {, or }` })
175
- }
176
- return ok(normalized)
177
- }
178
-
179
- function normalizeActors(actors: string[] | undefined): Result<string[], MutationError> {
180
- const out: string[] = []
181
- for (const raw of actors ?? []) {
182
- if (typeof raw !== 'string') return err({ code: 'INVALID_OP', message: 'Journey actor must be a string' })
183
- const normalized = normalizeJourneyActor(raw)
184
- if (!normalized) return err({ code: 'INVALID_OP', message: 'Journey actor must be non-empty' })
185
- out.push(normalized)
186
- }
187
- return ok(out)
188
- }
189
-
190
- export function mutateJourney(body: JourneyBody, op: JourneyMutationOp): Result<JourneyBody, MutationError> {
191
- const next = cloneJourney(body)
192
- const nextId = makeIdAllocator(next)
193
-
194
- const getSection = (i: number): JourneySection | undefined => next.sections[i]
195
- const getTask = (si: number, ti: number): JourneyTask | undefined => getSection(si)?.tasks[ti]
196
-
197
- switch (op.kind) {
198
- case 'set_title': {
199
- if (op.title === null) delete next.title
200
- else {
201
- const title = normalizeOpText(op.title, 'title', { allowColon: true })
202
- if (!title.ok) return title
203
- next.title = title.value
204
- }
205
- break
206
- }
207
- case 'add_section': {
208
- const label = normalizeOpText(op.label, 'section label')
209
- if (!label.ok) return label
210
- const index = resolveInsertIndex(op.index, next.sections.length)
211
- if (!index.ok) return index
212
- next.sections.splice(index.value, 0, { id: nextId('section'), label: label.value, tasks: [] })
213
- break
214
- }
215
- case 'remove_section': {
216
- if (!getSection(op.index)) return err({ code: 'SECTION_NOT_FOUND', message: `No section at index ${op.index}` })
217
- next.sections.splice(op.index, 1)
218
- break
219
- }
220
- case 'set_section_label': {
221
- const s = getSection(op.index)
222
- if (!s) return err({ code: 'SECTION_NOT_FOUND', message: `No section at index ${op.index}` })
223
- const label = normalizeOpText(op.label, 'section label')
224
- if (!label.ok) return label
225
- s.label = label.value
226
- break
227
- }
228
- case 'add_task': {
229
- const s = getSection(op.sectionIndex)
230
- if (!s) return err({ code: 'SECTION_NOT_FOUND', message: `No section at index ${op.sectionIndex}` })
231
- const text = normalizeOpText(op.text, 'task text')
232
- if (!text.ok) return text
233
- if (!isValidJourneyScore(op.score)) return err({ code: 'INVALID_OP', message: `Journey score must be an integer 1..5, got ${op.score}` })
234
- const actors = normalizeActors(op.actors)
235
- if (!actors.ok) return actors
236
- const index = resolveInsertIndex(op.index, s.tasks.length)
237
- if (!index.ok) return index
238
- s.tasks.splice(index.value, 0, { id: nextId('task'), text: text.value, score: op.score, actors: actors.value })
239
- break
240
- }
241
- case 'remove_task': {
242
- const s = getSection(op.sectionIndex)
243
- if (!s) return err({ code: 'SECTION_NOT_FOUND', message: `No section at index ${op.sectionIndex}` })
244
- if (!s.tasks[op.taskIndex]) return err({ code: 'TASK_NOT_FOUND', message: `No task at index ${op.taskIndex}` })
245
- s.tasks.splice(op.taskIndex, 1)
246
- if (s.label === undefined && s.tasks.length === 0) next.sections.splice(op.sectionIndex, 1)
247
- break
248
- }
249
- case 'set_task_text': {
250
- const t = getTask(op.sectionIndex, op.taskIndex)
251
- if (!t) return err({ code: 'TASK_NOT_FOUND', message: `No task at (${op.sectionIndex},${op.taskIndex})` })
252
- const text = normalizeOpText(op.text, 'task text')
253
- if (!text.ok) return text
254
- t.text = text.value
255
- break
256
- }
257
- case 'set_task_score': {
258
- const t = getTask(op.sectionIndex, op.taskIndex)
259
- if (!t) return err({ code: 'TASK_NOT_FOUND', message: `No task at (${op.sectionIndex},${op.taskIndex})` })
260
- if (!isValidJourneyScore(op.score)) return err({ code: 'INVALID_OP', message: `Journey score must be an integer 1..5, got ${op.score}` })
261
- t.score = op.score
262
- break
263
- }
264
- case 'set_task_actors': {
265
- const t = getTask(op.sectionIndex, op.taskIndex)
266
- if (!t) return err({ code: 'TASK_NOT_FOUND', message: `No task at (${op.sectionIndex},${op.taskIndex})` })
267
- const actors = normalizeActors(op.actors)
268
- if (!actors.ok) return actors
269
- t.actors = actors.value
270
- break
271
- }
272
- case 'move_task': {
273
- const from = getSection(op.fromSection)
274
- if (!from) return err({ code: 'SECTION_NOT_FOUND', message: `No section at index ${op.fromSection}` })
275
- if (!from.tasks[op.fromIndex]) return err({ code: 'TASK_NOT_FOUND', message: `No task at index ${op.fromIndex}` })
276
- const to = getSection(op.toSection)
277
- if (!to) return err({ code: 'SECTION_NOT_FOUND', message: `No section at index ${op.toSection}` })
278
- const [task] = from.tasks.splice(op.fromIndex, 1)
279
- if (!Number.isInteger(op.toIndex) || op.toIndex < 0 || op.toIndex > to.tasks.length) {
280
- return err({ code: 'TASK_NOT_FOUND', message: `No insert position ${op.toIndex} in section ${op.toSection} (0..${to.tasks.length})` })
281
- }
282
- to.tasks.splice(op.toIndex, 0, task!)
283
- // remove_task parity: an emptied implicit (unlabeled) section disappears.
284
- if (from !== to && from.label === undefined && from.tasks.length === 0) {
285
- next.sections.splice(next.sections.indexOf(from), 1)
286
- }
287
- break
288
- }
289
- case 'move_section': {
290
- if (!getSection(op.from)) return err({ code: 'SECTION_NOT_FOUND', message: `No section at index ${op.from}` })
291
- if (!Number.isInteger(op.to) || op.to < 0 || op.to >= next.sections.length) {
292
- return err({ code: 'SECTION_NOT_FOUND', message: `No section position ${op.to} (0..${next.sections.length - 1})` })
293
- }
294
- const [section] = next.sections.splice(op.from, 1)
295
- next.sections.splice(op.to, 0, section!)
296
- break
297
- }
298
- case 'set_accessibility_title': {
299
- const title = normalizeAccessibilityOpText(op.title, 'accessibility title')
300
- if (!title.ok) return title
301
- if (title.value === undefined) delete next.accessibilityTitle
302
- else next.accessibilityTitle = title.value
303
- break
304
- }
305
- case 'set_accessibility_description': {
306
- const description = normalizeAccessibilityOpText(op.description, 'accessibility description')
307
- if (!description.ok) return description
308
- if (description.value === undefined) delete next.accessibilityDescription
309
- else next.accessibilityDescription = description.value
310
- break
311
- }
312
- case 'rename_actor': {
313
- const from = normalizeOpText(op.from, 'actor', { allowColon: true })
314
- if (!from.ok) return from
315
- const to = normalizeOpText(op.to, 'actor', { allowColon: true })
316
- if (!to.ok) return to
317
- let found = false
318
- for (const s of next.sections) {
319
- for (const t of s.tasks) {
320
- t.actors = t.actors.map(a => {
321
- if (a === from.value) { found = true; return to.value }
322
- return a
323
- })
324
- }
325
- }
326
- if (!found) return err({ code: 'ACTOR_NOT_FOUND', message: `Actor "${from.value}" not found` })
327
- break
328
- }
329
- default: {
330
- const _x: never = op
331
- return err({ code: 'INVALID_OP', message: unknownOpMessage('journey', _x) })
332
- }
333
- }
334
-
335
- // Preserve the structured floor: a journey without a scored task cannot
336
- // render, so refuse mutations that would EMPTY a non-empty journey. A body
337
- // that starts empty (createMermaid/buildMermaid) may stay empty while ops
338
- // build it up — title/sections first, tasks after.
339
- const hadTask = body.sections.some(s => s.tasks.length > 0)
340
- if (hadTask && next.sections.every(s => s.tasks.length === 0)) {
341
- return err({ code: 'INVALID_OP', message: 'Journey must keep at least one scored task' })
342
- }
343
-
344
- return ok(next)
345
- }
346
-
347
- // ---- Verifier (FamilyPlugin.verify hook) ------------------------------------
348
-
349
- export function verifyJourney(body: JourneyBody, opts: VerifyOptions): LayoutWarning[] {
350
- const cap = opts.labelCharCap ?? DEFAULT_LABEL_CHAR_CAP
351
- const warnings: LayoutWarning[] = []
352
- const overflow = (target: string, text: string) => {
353
- const w = labelOverflowWarning(target, text, cap)
354
- if (w) warnings.push(w)
355
- }
356
- const hasTask = body.sections.some(s => s.tasks.length > 0)
357
- const hasHeaderFurniture = body.title !== undefined
358
- || body.accessibilityTitle !== undefined
359
- || body.accessibilityDescription !== undefined
360
- || body.sections.length > 0
361
- if (!hasTask && !hasHeaderFurniture) warnings.push({ code: 'EMPTY_DIAGRAM' })
362
- if (body.title !== undefined) {
363
- const w = labelOverflowWarning('title', body.title, Math.max(cap, 80))
364
- if (w) warnings.push(w)
365
- }
366
- for (const s of body.sections) {
367
- if (s.label !== undefined) overflow(s.id, s.label)
368
- for (const t of s.tasks) {
369
- overflow(t.id, t.text)
370
- for (const a of t.actors) overflow(t.id, a)
371
- }
372
- }
373
- return warnings
374
- }
@@ -1,28 +0,0 @@
1
- // ============================================================================
2
- // Label display metrics — the LABEL_OVERFLOW cap measures what the renderer
3
- // draws, not raw source characters. The render pipeline decodes XML entities
4
- // (renderMermaidSVG runs decodeXML before parsing), converts <br> tags and
5
- // literal \n to line breaks, and strips formatting tags before measuring text
6
- // (multiline-utils.ts). Counting source chars instead flags legitimately-fine
7
- // multi-line labels whose markup (`<br/>`, `&#160;`) pads the count.
8
- // ============================================================================
9
-
10
- import { decodeXML } from 'entities'
11
- import { normalizeBrTags, stripFormattingTags } from '../multiline-utils.ts'
12
- import type { LayoutWarning } from './types.ts'
13
-
14
- /**
15
- * Length of a label as rendered: the longest line after entity decoding,
16
- * <br>/\n splitting, and formatting-tag stripping. `&#160;` counts as one
17
- * character; `<br/>` starts a new line and counts as zero.
18
- */
19
- export function labelDisplayLength(label: string): number {
20
- const rendered = stripFormattingTags(normalizeBrTags(decodeXML(label)))
21
- return rendered.split('\n').reduce((max, line) => Math.max(max, line.length), 0)
22
- }
23
-
24
- /** Build a LABEL_OVERFLOW warning when the rendered length exceeds the cap, else null. */
25
- export function labelOverflowWarning(target: string, text: string, cap: number): LayoutWarning | null {
26
- const charCount = labelDisplayLength(text)
27
- return charCount > cap ? { code: 'LABEL_OVERFLOW', target, charCount, limit: cap } : null
28
- }
@@ -1,63 +0,0 @@
1
- // ============================================================================
2
- // Shared converter: ELK PositionedGraph → public RenderedLayout JSON.
3
- // Previously implemented in two places (verify.ts and index.ts's layoutMermaid)
4
- // — see DIVERGENCES "Cleanup findings". Single source of truth.
5
- // ============================================================================
6
-
7
- import type { PositionedGraph, PositionedNode, PositionedEdge, PositionedGroup } from '../types.ts'
8
- import type {
9
- DiagramKind, Finite, RenderedLayout, RenderedLayoutNode, RenderedLayoutEdge, RenderedLayoutGroup,
10
- } from './types.ts'
11
- import { toFinite } from './types.ts'
12
-
13
- function f(n: number): Finite { return toFinite(Math.round(n)) }
14
-
15
- function node(n: PositionedNode): RenderedLayoutNode {
16
- return { id: n.id, x: f(n.x), y: f(n.y), w: f(n.width), h: f(n.height), shape: n.shape, label: n.label }
17
- }
18
-
19
- function edge(e: PositionedEdge, debug: boolean): RenderedLayoutEdge {
20
- return {
21
- id: `${e.source}->${e.target}`,
22
- from: e.source, to: e.target,
23
- path: e.points.map(p => [f(p.x), f(p.y)] as [Finite, Finite]),
24
- label: e.label && e.labelPosition
25
- ? { x: f(e.labelPosition.x), y: f(e.labelPosition.y), text: e.label }
26
- : undefined,
27
- ...(debug && e.routeCertificate ? { route: e.routeCertificate } : {}),
28
- }
29
- }
30
-
31
- export function positionedToRenderedLayout(p: PositionedGraph, kind: DiagramKind, opts: { debug?: boolean } = {}): RenderedLayout {
32
- return {
33
- version: 1, kind,
34
- nodes: p.nodes.map(node),
35
- edges: p.edges.map(e => edge(e, opts.debug === true)),
36
- groups: renderedGroups(p.groups, p.nodes),
37
- bounds: { w: f(p.width), h: f(p.height) },
38
- }
39
- }
40
-
41
- function renderedGroups(groups: PositionedGroup[], nodes: PositionedNode[]): RenderedLayoutGroup[] {
42
- const out: RenderedLayoutGroup[] = []
43
- const containsNode = (g: PositionedGroup, n: PositionedNode): boolean =>
44
- n.x >= g.x - 0.5 && n.y >= g.y - 0.5 && n.x + n.width <= g.x + g.width + 0.5 && n.y + n.height <= g.y + g.height + 0.5
45
- const visit = (g: PositionedGroup, parentId?: string): void => {
46
- const members = nodes
47
- .filter(n => containsNode(g, n) && !g.children.some(child => containsNode(child, n)))
48
- .map(n => n.id)
49
- out.push({
50
- id: g.id,
51
- x: f(g.x), y: f(g.y), w: f(g.width), h: f(g.height),
52
- members, label: g.label,
53
- ...(parentId ? { parentId } : {}),
54
- })
55
- for (const child of g.children) visit(child, g.id)
56
- }
57
- for (const g of groups) visit(g)
58
- return out
59
- }
60
-
61
- export function emptyRenderedLayout(kind: DiagramKind): RenderedLayout {
62
- return { version: 1, kind, nodes: [], edges: [], groups: [], bounds: { w: f(0), h: f(0) } }
63
- }
@@ -1,97 +0,0 @@
1
- // ============================================================================
2
- // mutate: typed structural edits. Overloaded by family.
3
- // ============================================================================
4
-
5
- import type {
6
- FlowchartValidDiagram, StateValidDiagram, SequenceValidDiagram, TimelineValidDiagram,
7
- ClassValidDiagram, ErValidDiagram, JourneyValidDiagram, ArchitectureValidDiagram, XyChartValidDiagram, PieValidDiagram, QuadrantValidDiagram, GanttValidDiagram, MutableValidDiagram,
8
- FlowchartMutationOp, StateMutationOp, SequenceMutationOp, TimelineMutationOp,
9
- ClassMutationOp, ErMutationOp, JourneyMutationOp, ArchitectureMutationOp, XyChartMutationOp, PieMutationOp, QuadrantMutationOp, GanttMutationOp, AnyMutationOp,
10
- MutationError, Result,
11
- } from './types.ts'
12
- import { ok, err } from './types.ts'
13
- import { wrapperPrefix } from './serialize.ts'
14
- import { logToolInvocation } from './trace-log.ts'
15
- import { getFamily } from './families.ts'
16
- import { validateOp, hasOpSchema } from './op-schema.ts'
17
- import './families-builtin.ts' // registers built-in family mutate hooks
18
-
19
- export function mutate(d: FlowchartValidDiagram, op: FlowchartMutationOp): Result<FlowchartValidDiagram, MutationError>
20
- export function mutate(d: StateValidDiagram, op: StateMutationOp): Result<StateValidDiagram, MutationError>
21
- export function mutate(d: SequenceValidDiagram, op: SequenceMutationOp): Result<SequenceValidDiagram, MutationError>
22
- export function mutate(d: TimelineValidDiagram, op: TimelineMutationOp): Result<TimelineValidDiagram, MutationError>
23
- export function mutate(d: ClassValidDiagram, op: ClassMutationOp): Result<ClassValidDiagram, MutationError>
24
- export function mutate(d: ErValidDiagram, op: ErMutationOp): Result<ErValidDiagram, MutationError>
25
- export function mutate(d: JourneyValidDiagram, op: JourneyMutationOp): Result<JourneyValidDiagram, MutationError>
26
- export function mutate(d: ArchitectureValidDiagram, op: ArchitectureMutationOp): Result<ArchitectureValidDiagram, MutationError>
27
- export function mutate(d: XyChartValidDiagram, op: XyChartMutationOp): Result<XyChartValidDiagram, MutationError>
28
- export function mutate(d: PieValidDiagram, op: PieMutationOp): Result<PieValidDiagram, MutationError>
29
- export function mutate(d: QuadrantValidDiagram, op: QuadrantMutationOp): Result<QuadrantValidDiagram, MutationError>
30
- export function mutate(d: GanttValidDiagram, op: GanttMutationOp): Result<GanttValidDiagram, MutationError>
31
- // General form for callers holding the union (e.g. the CLI): dispatch is by
32
- // registry at runtime either way, so kind-agnostic call sites don't need a
33
- // per-family narrowing cascade.
34
- export function mutate(d: MutableValidDiagram, op: AnyMutationOp): Result<MutableValidDiagram, MutationError>
35
- export function mutate(
36
- d: MutableValidDiagram,
37
- op: AnyMutationOp,
38
- ): Result<MutableValidDiagram, MutationError> {
39
- // Log the OUTCOME (not just the call): an `{ok:false}` trace line records a
40
- // failed op attempt — the observable signal the agent-usage eval reads to
41
- // measure a run's op-error rate directly, rather than inferring retries from
42
- // excess mutate-call counts.
43
- const r = applyOneMutation(d, op)
44
- logToolInvocation('mutate', r.ok)
45
- return r
46
- }
47
-
48
- function applyOneMutation(
49
- d: MutableValidDiagram,
50
- op: AnyMutationOp,
51
- ): Result<MutableValidDiagram, MutationError> {
52
- // Every structured family mutates through its FamilyPlugin hook, then
53
- // rebuilds canonicalSource from the new body so a mutated diagram never
54
- // carries stale source. Lookup is by DIAGRAM kind, not body kind. State
55
- // diagrams (BUILD-19) own a dedicated StateBody and bind the stateDiagram-v2
56
- // header through their own plugin registration.
57
- const plugin = getFamily(d.kind)
58
- if (plugin?.mutate && plugin.serialize) {
59
- const r = plugin.mutate(d.body, op)
60
- if (!r.ok) return r
61
- // 1C wrapper policy: a mutated diagram keeps its leading wrapper
62
- // (frontmatter/directives/comments) byte-verbatim; only the body changes.
63
- const canonicalSource = wrapperPrefix(d.meta) + plugin.serialize(r.value)
64
- return ok({ ...d, body: r.value, canonicalSource } as MutableValidDiagram)
65
- }
66
- return err({ code: 'INVALID_OP', message: `Unsupported mutable diagram kind: ${d.kind}` })
67
- }
68
-
69
- /**
70
- * mutate with a shape check in front — the one choke point every UNTYPED edit
71
- * path funnels through: the declarative applyOps/buildChecked entrypoints, the
72
- * Code Mode facade, and the CLI `--op`/`--ops` path. Typed callers keep using
73
- * `mutate` directly, where the compiler already guarantees op shape;
74
- * `mutateChecked` restores that guarantee at the boundaries where ops arrive as
75
- * raw JSON.
76
- *
77
- * `validateOp` proves the op's SHAPE (field names, primitive types, enums)
78
- * BEFORE the mutator runs, so a malformed op is rejected with a prescriptive
79
- * INVALID_OP error instead of silently mangling the diagram. Semantics — does
80
- * the referenced node exist, is the id a duplicate — stay in the mutator, which
81
- * runs second on a shape-proven op. Result shape is identical to `mutate`, so
82
- * this is a drop-in wherever the raw mutator was called with untyped ops.
83
- */
84
- export function mutateChecked(d: MutableValidDiagram, op: unknown): Result<MutableValidDiagram, MutationError> {
85
- if (hasOpSchema(d.kind)) {
86
- const invalid = validateOp(d.kind, op)
87
- // A shape rejection short-circuits before `mutate`, so record the failed
88
- // attempt here — otherwise checked-path errors (e.g. the op-array slip)
89
- // would go uncounted in the op-error rate.
90
- if (invalid) { logToolInvocation('mutate', false); return err(invalid) }
91
- }
92
- return mutate(d, op as AnyMutationOp)
93
- }
94
-
95
- // Flowchart graph mutation + helpers live in flowchart-body.ts; edgeIdOf is
96
- // re-exported here for existing consumers (agent/index.ts, eval harness).
97
- export { edgeIdOf } from './flowchart-body.ts'
@@ -1,51 +0,0 @@
1
- import type { BuiltinFamilyId } from './families.ts'
2
- import type { DiagramKind } from './types.ts'
3
-
4
- // Single source of truth for each family's structured mutation op `kind`s. It
5
- // lives in the agent layer, next to the mutators, so two consumers read the
6
- // same list: the CLI `capabilities` envelope (re-exported from src/cli/index.ts)
7
- // and the mutators' "unknown op" errors, which name the family's valid ops
8
- // instead of leaving a caller — especially a smaller model — to guess them.
9
- export const MUTATION_OPS_BY_FAMILY = {
10
- flowchart: ['add_node', 'remove_node', 'rename_node', 'set_label', 'add_edge', 'remove_edge'],
11
- state: ['add_state', 'remove_state', 'rename_state', 'set_state_label', 'add_transition', 'remove_transition', 'set_transition_label', 'make_composite'],
12
- sequence: ['add_participant', 'remove_participant', 'add_message', 'remove_message', 'set_message_text'],
13
- timeline: ['set_title', 'add_section', 'remove_section', 'set_section_label', 'add_period', 'remove_period', 'set_period_label', 'add_event', 'remove_event', 'set_event_text'],
14
- class: ['set_title', 'add_class', 'remove_class', 'rename_class', 'add_member', 'remove_member', 'add_relation', 'remove_relation', 'add_note', 'remove_note'],
15
- er: ['add_entity', 'remove_entity', 'rename_entity', 'add_attribute', 'remove_attribute', 'add_relation', 'remove_relation'],
16
- journey: ['set_title', 'add_section', 'remove_section', 'set_section_label', 'add_task', 'remove_task', 'set_task_text', 'set_task_score', 'set_task_actors', 'rename_actor', 'move_task', 'move_section', 'set_accessibility_title', 'set_accessibility_description'],
17
- architecture: ['add_service', 'remove_service', 'rename_service', 'set_service_label', 'set_service_icon', 'move_service', 'add_group', 'remove_group', 'add_edge', 'remove_edge'],
18
- xychart: ['set_title', 'set_x_axis', 'set_y_axis', 'add_series', 'remove_series', 'set_series_values', 'set_series_name', 'reorder_series'],
19
- pie: ['set_title', 'set_show_data', 'add_slice', 'remove_slice', 'rename_slice', 'set_slice_value', 'reorder_slice'],
20
- quadrant: ['set_title', 'set_axis_labels', 'set_quadrant_label', 'add_point', 'remove_point', 'move_point', 'rename_point'],
21
- gantt: ['set_title', 'add_section', 'rename_section', 'remove_section', 'add_task', 'remove_task', 'rename_task', 'set_task_status', 'set_task_dates'],
22
- } as const satisfies Record<BuiltinFamilyId, readonly string[]>
23
-
24
- export type MutableFamilyId = keyof typeof MUTATION_OPS_BY_FAMILY
25
-
26
- /** The valid mutation-op `kind`s for a family, as a display string — for
27
- * prescriptive "unknown op" errors that hand the caller the actual menu. */
28
- export function validOpsFor(family: DiagramKind): string {
29
- const ops = (MUTATION_OPS_BY_FAMILY as Record<string, readonly string[]>)[family]
30
- return ops ? ops.join(', ') : '(none — this family has no structured ops)'
31
- }
32
-
33
- /** Message for an op whose `kind` a family's mutator does not recognize: names
34
- * the offending kind AND the family's valid ops, so the caller can correct it
35
- * from the error alone instead of guessing which ops exist. */
36
- export function unknownOpMessage(family: DiagramKind, op: unknown): string {
37
- // Passing an op ARRAY where one op is expected is the single most common shape
38
- // slip — it recurs on nearly every case across every model tier in the
39
- // agent-usage eval (mutate() applies ONE op, but "apply these ops" reads as a
40
- // list). Hand back the rule and the batch alternatives so the next action is
41
- // in the message, not left to infer from a dumped array.
42
- if (Array.isArray(op)) {
43
- return `Expected a single ${family} op, got an array of ${op.length}. `
44
- + `Ops apply one at a time: call mutate() once per op, or pass the whole list to a batch `
45
- + `entrypoint — applyOps({ source, family, ops }) to edit source, or buildMermaid(kind, ops) `
46
- + `to author — valid ${family} ops: ${validOpsFor(family)}`
47
- }
48
- const kind = (op as { kind?: unknown } | null | undefined)?.kind
49
- const got = typeof kind === 'string' ? `"${kind}"` : JSON.stringify(op)
50
- return `Unknown ${family} op ${got} — valid ops: ${validOpsFor(family)}`
51
- }