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.
- package/AGENT_NATIVE.md +155 -91
- package/CHANGELOG.md +163 -4
- package/Instructions_for_agents.md +10 -10
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +31 -38
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/assets/hero.png +0 -0
- package/dist/agent-core.d.ts +463 -0
- package/dist/agent-core.js +397 -0
- package/dist/agent.d.ts +73 -1868
- package/dist/agent.js +336 -37325
- package/dist/agentic-mermaid-mcp.js +7 -38399
- package/dist/am.js +33626 -40446
- package/dist/chunk-2P5FKUQC.js +3485 -0
- package/dist/chunk-2S3VSS6I.js +149 -0
- package/dist/chunk-2SILJDW2.js +37 -0
- package/dist/chunk-BJYMBT4I.js +61963 -0
- package/dist/chunk-FDM43XU7.js +615 -0
- package/dist/chunk-MYPDVZCO.js +4702 -0
- package/dist/index-DWPa2MJs.d.ts +6780 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +310 -33014
- package/dist/mcp-cli-5J47WEY3.js +17 -0
- package/docs/README.md +39 -31
- package/docs/agent-api-cookbook.md +44 -35
- package/docs/agent-mutation-policy.md +1 -1
- package/docs/agent-workflow-examples.md +2 -2
- package/docs/api.md +284 -77
- package/docs/ascii.md +26 -16
- package/docs/code-mode-impact.md +214 -0
- package/docs/comparison.md +23 -11
- package/docs/computational-aesthetics-prototype-plan.md +252 -0
- package/docs/config.md +26 -10
- package/docs/custom-fonts.md +265 -0
- package/docs/custom-style-cookbook.md +130 -9
- package/docs/diagram-families.md +48 -23
- package/docs/features.md +59 -52
- package/docs/fork-differences.md +6 -3
- package/docs/getting-started.md +23 -16
- package/docs/issue-derived-test-cases.md +29 -22
- package/docs/mcp-code-mode-rationale.md +13 -6
- package/docs/mcp-http-transport.md +20 -13
- package/docs/mermaid-layout-complaints.md +66 -52
- package/docs/mutation-testing.md +145 -66
- package/docs/quality.md +23 -14
- package/docs/react.md +2 -2
- package/docs/schemas/style-spec.schema.json +1221 -30
- package/docs/style-authoring.md +241 -38
- package/docs/svg-semantic-contract.md +49 -0
- package/docs/testing-strategy.md +166 -58
- package/docs/theming.md +19 -32
- package/examples/styles/catalog.json +54 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
- package/examples/styles/cupertino-prototype.mmd +10 -0
- package/examples/styles/cupertino-prototype.style.json +63 -0
- package/examples/styles/mid-century-report.style.json +2 -2
- package/examples/styles/star-chart-atlas.style.json +2 -1
- package/examples/styles/transit-route-map.style.json +2 -2
- package/examples/styles/vercel-inspired-prototype.mmd +6 -0
- package/examples/styles/vercel-inspired-prototype.style.json +87 -0
- package/package.json +85 -45
- package/server.json +2 -2
- package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
- package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
- package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
- package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
- package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
- package/TODO.md +0 -684
- package/dist/index-wjRjbsP-.d.ts +0 -1102
- package/docs/assets/improvements/box-start-connector.png +0 -0
- package/docs/assets/improvements/comment-dropped-lint.png +0 -0
- package/docs/assets/improvements/er-content-loss.png +0 -0
- package/docs/assets/improvements/fan-in-trunks.png +0 -0
- package/docs/assets/improvements/gantt-family.png +0 -0
- package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
- package/docs/assets/improvements/gantt-verify-seam.png +0 -0
- package/docs/assets/improvements/subgraph-container-edges.png +0 -0
- package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
- package/docs/assets/improvements/wrapper-init-directive.png +0 -0
- package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
- package/docs/assets/style-cookbook/mid-century-report.png +0 -0
- package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
- package/docs/assets/style-cookbook/transit-route-map.png +0 -0
- package/docs/contributing/adding-diagram-types.md +0 -144
- package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
- package/docs/contributing/diagram-family-citizenship.md +0 -82
- package/docs/contributing/harvesting-upstream-tests.md +0 -126
- package/docs/contributing/lessons-learned.md +0 -276
- package/docs/contributing/releasing.md +0 -82
- package/docs/contributing/visual-review-evidence.md +0 -53
- package/docs/design/families/README.md +0 -26
- package/docs/design/families/architecture-beta.md +0 -48
- package/docs/design/families/flowchart-parser-conformance.md +0 -22
- package/docs/design/families/gantt-research.md +0 -137
- package/docs/design/families/gantt.md +0 -329
- package/docs/design/families/journey-agentic-cards-before.png +0 -0
- package/docs/design/families/journey-experience-curve-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
- package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
- package/docs/design/families/journey-migration-parity.md +0 -439
- package/docs/design/families/journey-section-overlap-after.png +0 -0
- package/docs/design/families/journey-section-overlap-before.png +0 -0
- package/docs/design/families/journey-usage-research.md +0 -423
- package/docs/design/families/journey.md +0 -153
- package/docs/design/families/xychart.md +0 -85
- package/docs/design/system/README.md +0 -45
- package/docs/design/system/abstraction-audit.md +0 -224
- package/docs/design/system/abstraction-recommendations.md +0 -655
- package/docs/design/system/architecture.mmd +0 -29
- package/docs/design/system/architecture.svg +0 -137
- package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
- package/docs/design/system/layout-rubric.md +0 -138
- package/docs/design/system/route-contracts.md +0 -848
- package/docs/design/system/source-preservation-ladder.md +0 -52
- package/docs/design/system/styles-rollout.md +0 -216
- package/docs/design/system/ugly-layouts.md +0 -119
- package/docs/layout-characterization/README.md +0 -245
- package/docs/layout-characterization/contact-sheet-families.md +0 -498
- package/docs/layout-characterization/contact-sheet.md +0 -638
- package/docs/layout-characterization/properties.md +0 -281
- package/docs/layout-characterization/visual-quality.md +0 -160
- package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
- package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
- package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
- package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
- package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/journey.svg +0 -98
- package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
- package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
- package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
- package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
- package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
- package/docs/project/divergences.md +0 -98
- package/docs/project/dogfooding-docs-strategy.md +0 -66
- package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
- package/docs/project/lessons-learned.md +0 -1019
- package/docs/project/remove-role-styling-plan.md +0 -176
- package/examples/agent-improve-auth-flow.ts +0 -206
- package/examples/agent-loop.ts +0 -53
- package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
- package/llms.txt +0 -104
- package/skill-evals/README.md +0 -77
- package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
- package/skill-evals/fixtures/class-payment/input.mmd +0 -4
- package/skill-evals/fixtures/er-orders/input.mmd +0 -10
- package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
- package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
- package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
- package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
- package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
- package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
- package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
- package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
- package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
- package/skill-evals/shared-benchmark.json +0 -510
- package/src/agent/analyze.ts +0 -157
- package/src/agent/apply.ts +0 -146
- package/src/agent/architecture-body.ts +0 -458
- package/src/agent/certificates.ts +0 -110
- package/src/agent/class-body.ts +0 -319
- package/src/agent/core.ts +0 -207
- package/src/agent/create.ts +0 -106
- package/src/agent/describe.ts +0 -368
- package/src/agent/er-body.ts +0 -224
- package/src/agent/facts.ts +0 -360
- package/src/agent/families-builtin.ts +0 -920
- package/src/agent/families.ts +0 -232
- package/src/agent/family-layouts.ts +0 -832
- package/src/agent/flowchart-body.ts +0 -372
- package/src/agent/flowchart-unsupported.ts +0 -243
- package/src/agent/font-coverage.ts +0 -288
- package/src/agent/gantt-body.ts +0 -495
- package/src/agent/index.ts +0 -10
- package/src/agent/journey-body.ts +0 -374
- package/src/agent/label-metrics.ts +0 -28
- package/src/agent/layout-to-rendered.ts +0 -63
- package/src/agent/mutate.ts +0 -97
- package/src/agent/mutation-ops.ts +0 -51
- package/src/agent/op-schema.ts +0 -443
- package/src/agent/parse.ts +0 -223
- package/src/agent/pie-body.ts +0 -270
- package/src/agent/png.ts +0 -171
- package/src/agent/quadrant-body.ts +0 -336
- package/src/agent/quality.ts +0 -363
- package/src/agent/readability-audit.ts +0 -92
- package/src/agent/rendered-route-audit.ts +0 -61
- package/src/agent/sequence-body.ts +0 -345
- package/src/agent/serialize.ts +0 -214
- package/src/agent/state-body.ts +0 -589
- package/src/agent/structural-count.ts +0 -150
- package/src/agent/timeline-body.ts +0 -280
- package/src/agent/trace-log.ts +0 -43
- package/src/agent/types.ts +0 -995
- package/src/agent/verify.ts +0 -603
- package/src/agent/xychart-body.ts +0 -472
- package/src/architecture/config.ts +0 -282
- package/src/architecture/layout.ts +0 -496
- package/src/architecture/parser.ts +0 -375
- package/src/architecture/renderer.ts +0 -611
- package/src/architecture/types.ts +0 -113
- package/src/ascii/ansi.ts +0 -423
- package/src/ascii/architecture.ts +0 -210
- package/src/ascii/canvas.ts +0 -443
- package/src/ascii/class-diagram.ts +0 -697
- package/src/ascii/converter.ts +0 -475
- package/src/ascii/draw.ts +0 -1693
- package/src/ascii/edge-bundling.ts +0 -328
- package/src/ascii/edge-routing.ts +0 -330
- package/src/ascii/er-diagram.ts +0 -439
- package/src/ascii/gantt.ts +0 -276
- package/src/ascii/grid.ts +0 -747
- package/src/ascii/index.ts +0 -196
- package/src/ascii/journey.ts +0 -169
- package/src/ascii/meta.ts +0 -244
- package/src/ascii/multiline-utils.ts +0 -80
- package/src/ascii/pathfinder.ts +0 -301
- package/src/ascii/pie.ts +0 -67
- package/src/ascii/quadrant.ts +0 -158
- package/src/ascii/reverse.ts +0 -216
- package/src/ascii/sequence.ts +0 -499
- package/src/ascii/shapes/circle.ts +0 -27
- package/src/ascii/shapes/corners.ts +0 -143
- package/src/ascii/shapes/diamond.ts +0 -27
- package/src/ascii/shapes/hexagon.ts +0 -27
- package/src/ascii/shapes/index.ts +0 -106
- package/src/ascii/shapes/rectangle.ts +0 -168
- package/src/ascii/shapes/rounded.ts +0 -27
- package/src/ascii/shapes/special.ts +0 -332
- package/src/ascii/shapes/stadium.ts +0 -110
- package/src/ascii/shapes/state.ts +0 -192
- package/src/ascii/shapes/types.ts +0 -73
- package/src/ascii/timeline.ts +0 -141
- package/src/ascii/types.ts +0 -312
- package/src/ascii/validate.ts +0 -120
- package/src/ascii/width.ts +0 -81
- package/src/ascii/wrap.ts +0 -131
- package/src/ascii/xychart.ts +0 -919
- package/src/browser.ts +0 -29
- package/src/class/layout.ts +0 -240
- package/src/class/parser.ts +0 -332
- package/src/class/renderer.ts +0 -638
- package/src/class/types.ts +0 -129
- package/src/cli/agent-instructions.ts +0 -1
- package/src/cli/am-bin.ts +0 -6
- package/src/cli/exit-codes.ts +0 -18
- package/src/cli/index.ts +0 -1132
- package/src/cli/init-agent.ts +0 -147
- package/src/cli/run-entrypoint.ts +0 -10
- package/src/color-resolver.ts +0 -152
- package/src/elk-instance.ts +0 -114
- package/src/er/layout.ts +0 -187
- package/src/er/parser.ts +0 -226
- package/src/er/renderer.ts +0 -792
- package/src/er/types.ts +0 -99
- package/src/family-rubric.ts +0 -425
- package/src/font-manifest.ts +0 -31
- package/src/gantt/layout.ts +0 -365
- package/src/gantt/parser.ts +0 -282
- package/src/gantt/pipeline.ts +0 -27
- package/src/gantt/renderer.ts +0 -414
- package/src/gantt/schedule.ts +0 -513
- package/src/gantt/types.ts +0 -255
- package/src/index.ts +0 -368
- package/src/journey/layout.ts +0 -554
- package/src/journey/parse-core.ts +0 -251
- package/src/journey/parser.ts +0 -101
- package/src/journey/renderer.ts +0 -917
- package/src/journey/types.ts +0 -144
- package/src/layout/geometry.ts +0 -182
- package/src/layout/pass.ts +0 -142
- package/src/layout/passes/index.ts +0 -3916
- package/src/layout-engine.ts +0 -1565
- package/src/layout-rubric.ts +0 -659
- package/src/mcp/artifacts.ts +0 -137
- package/src/mcp/dynamic-harness.ts +0 -31
- package/src/mcp/dynamic-modules.d.ts +0 -7
- package/src/mcp/facade.ts +0 -608
- package/src/mcp/harness-runtime.ts +0 -203
- package/src/mcp/hosted-server.ts +0 -468
- package/src/mcp/mcp-bin.ts +0 -3
- package/src/mcp/mcp-cli.ts +0 -82
- package/src/mcp/protocol.ts +0 -13
- package/src/mcp/sandbox.ts +0 -195
- package/src/mcp/sdk-decl.ts +0 -391
- package/src/mcp/server.ts +0 -362
- package/src/mcp/tool-surface.ts +0 -146
- package/src/mermaid-source.ts +0 -655
- package/src/multiline-utils.ts +0 -228
- package/src/parser.ts +0 -1031
- package/src/pie/layout.ts +0 -168
- package/src/pie/parser.ts +0 -136
- package/src/pie/renderer.ts +0 -234
- package/src/pie/types.ts +0 -82
- package/src/quadrant/layout.ts +0 -191
- package/src/quadrant/parser.ts +0 -183
- package/src/quadrant/renderer.ts +0 -322
- package/src/quadrant/types.ts +0 -113
- package/src/render-family-hooks.ts +0 -244
- package/src/renderer.ts +0 -1099
- package/src/route-contracts.ts +0 -2723
- package/src/scene/backend.ts +0 -76
- package/src/scene/fidelity.ts +0 -183
- package/src/scene/hybrid-backend.ts +0 -174
- package/src/scene/ir.ts +0 -181
- package/src/scene/marks.ts +0 -127
- package/src/scene/rough-backend.ts +0 -378
- package/src/scene/seed.ts +0 -46
- package/src/scene/style-registry.ts +0 -556
- package/src/sequence/layout.ts +0 -456
- package/src/sequence/parser.ts +0 -255
- package/src/sequence/renderer.ts +0 -604
- package/src/sequence/types.ts +0 -154
- package/src/shape-clipping.ts +0 -351
- package/src/shared/accessibility-directives.ts +0 -22
- package/src/shared/batched.ts +0 -61
- package/src/shared/color-math.ts +0 -93
- package/src/shared/svg-a11y.ts +0 -29
- package/src/shared/syntax-error.ts +0 -17
- package/src/shared/unicode-ranges.ts +0 -135
- package/src/styles.ts +0 -318
- package/src/svg-paths.ts +0 -28
- package/src/text-metrics.ts +0 -248
- package/src/theme.ts +0 -710
- package/src/timeline/layout.ts +0 -381
- package/src/timeline/parser.ts +0 -215
- package/src/timeline/renderer.ts +0 -538
- package/src/timeline/types.ts +0 -105
- package/src/types.ts +0 -462
- package/src/website-worker-modules.d.ts +0 -18
- package/src/xychart/axis-utils.ts +0 -109
- package/src/xychart/colors.ts +0 -124
- package/src/xychart/config.ts +0 -88
- package/src/xychart/layout.ts +0 -656
- package/src/xychart/parser.ts +0 -504
- package/src/xychart/renderer.ts +0 -640
- 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/>`, ` `) 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. ` ` 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
|
-
}
|
package/src/agent/mutate.ts
DELETED
|
@@ -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
|
-
}
|