agentic-mermaid 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +149 -4
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +25 -32
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/assets/fonts/FONT-LICENSES.md +9 -5
- package/assets/fonts/Inter-Bold.ttf +0 -0
- package/assets/fonts/Inter-Medium.ttf +0 -0
- package/assets/fonts/Inter-Regular.ttf +0 -0
- package/assets/fonts/Inter-SemiBold.ttf +0 -0
- package/dist/agent-core.d.ts +463 -0
- package/dist/agent-core.js +397 -0
- package/dist/agent.d.ts +81 -1822
- package/dist/agent.js +336 -36839
- package/dist/agentic-mermaid-mcp.js +7 -37908
- package/dist/am.js +33638 -36261
- package/dist/chunk-4QECACIA.js +4702 -0
- package/dist/chunk-CDPKJ2BQ.js +37 -0
- package/dist/chunk-QKDLMOJJ.js +3487 -0
- package/dist/chunk-WI35L7IR.js +615 -0
- package/dist/chunk-WN3YDOBG.js +149 -0
- package/dist/chunk-ZYMMKQ3I.js +62637 -0
- package/dist/index-D54dqSdC.d.ts +6777 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +310 -32836
- package/dist/mcp-cli-SBQSTJFC.js +17 -0
- package/docs/README.md +14 -6
- package/docs/schemas/style-spec.schema.json +1221 -30
- 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 +70 -43
- package/server.json +41 -0
- 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/AGENT_NATIVE.md +0 -602
- package/Instructions_for_agents.md +0 -83
- package/TODO.md +0 -662
- package/assets/hero.png +0 -0
- package/dist/index-x2Cj1dVv.d.ts +0 -1089
- package/docs/agent-api-cookbook.md +0 -308
- package/docs/agent-mutation-policy.md +0 -104
- package/docs/agent-workflow-examples.md +0 -46
- package/docs/api.md +0 -203
- package/docs/ascii.md +0 -103
- 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/comparison.md +0 -94
- package/docs/config.md +0 -105
- 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 -71
- package/docs/contributing/visual-review-evidence.md +0 -53
- package/docs/custom-style-cookbook.md +0 -154
- 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-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 -433
- package/docs/design/families/journey-usage-research.md +0 -423
- package/docs/design/families/journey.md +0 -125
- 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/diagram-families.md +0 -193
- package/docs/features.md +0 -147
- package/docs/fork-differences.md +0 -130
- package/docs/getting-started.md +0 -175
- package/docs/issue-derived-test-cases.md +0 -156
- package/docs/layout-characterization/README.md +0 -245
- package/docs/layout-characterization/contact-sheet-families.md +0 -496
- 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 -118
- 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/mcp-code-mode-rationale.md +0 -101
- package/docs/mcp-http-transport.md +0 -223
- package/docs/mermaid-layout-complaints.md +0 -837
- package/docs/mutation-testing.md +0 -206
- 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/docs/quality.md +0 -212
- package/docs/react.md +0 -97
- package/docs/style-authoring.md +0 -133
- package/docs/testing-strategy.md +0 -335
- package/docs/theming.md +0 -162
- 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 -364
- package/src/agent/er-body.ts +0 -224
- package/src/agent/facts.ts +0 -337
- package/src/agent/families-builtin.ts +0 -934
- package/src/agent/families.ts +0 -232
- package/src/agent/family-layouts.ts +0 -826
- package/src/agent/flowchart-body.ts +0 -372
- package/src/agent/flowchart-unsupported.ts +0 -243
- package/src/agent/gantt-body.ts +0 -495
- package/src/agent/index.ts +0 -10
- package/src/agent/journey-body.ts +0 -414
- 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 -439
- package/src/agent/parse.ts +0 -223
- package/src/agent/pie-body.ts +0 -270
- package/src/agent/png.ts +0 -117
- 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 -979
- package/src/agent/verify.ts +0 -571
- 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 -189
- 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 -128
- package/src/ascii/types.ts +0 -312
- package/src/ascii/validate.ts +0 -120
- package/src/ascii/width.ts +0 -81
- 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 -1117
- package/src/cli/init-agent.ts +0 -147
- 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/font-manifest.ts +0 -27
- 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 -520
- package/src/journey/parser.ts +0 -218
- package/src/journey/renderer.ts +0 -811
- 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 -387
- 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 -72
- 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 -639
- 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 -454
- 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
package/src/agent/describe.ts
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// describeMermaid — natural-language summary of a diagram.
|
|
3
|
-
//
|
|
4
|
-
// Loop 9 M12. Returns a one- or two-sentence prose summary per family covering
|
|
5
|
-
// entities, edges, and notable structure. Intended for screen-reader output,
|
|
6
|
-
// doc generation, and LLM context compaction without re-parsing.
|
|
7
|
-
//
|
|
8
|
-
// Library: describeMermaid(d, opts?) — accepts a ValidDiagram. Convenience
|
|
9
|
-
// describeMermaidSource(source) wraps parseMermaid for callers (CLI/MCP) that
|
|
10
|
-
// only have a string.
|
|
11
|
-
// ============================================================================
|
|
12
|
-
|
|
13
|
-
import { parseMermaid } from './parse.ts'
|
|
14
|
-
import type {
|
|
15
|
-
ValidDiagram, FlowchartValidDiagram, SequenceValidDiagram, TimelineValidDiagram,
|
|
16
|
-
ClassValidDiagram, ErValidDiagram,
|
|
17
|
-
} from './types.ts'
|
|
18
|
-
import { getFamily, extractLabelsGeneric } from './families.ts'
|
|
19
|
-
import { describeMermaidFacts } from './facts.ts'
|
|
20
|
-
import './families-builtin.ts'
|
|
21
|
-
import { parseGanttModel } from '../gantt/parser.ts'
|
|
22
|
-
import { resolveGanttSchedule, formatGanttInstant } from '../gantt/schedule.ts'
|
|
23
|
-
import { toMermaidLines } from '../mermaid-source.ts'
|
|
24
|
-
|
|
25
|
-
export interface DescribeOptions {
|
|
26
|
-
/** 'text' (default): prose summary. 'json': structured AX tree (#7349). 'facts': deterministic semantic facts. */
|
|
27
|
-
format?: 'text' | 'json' | 'facts'
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Structured accessibility tree (#7349): the graph as a list of nodes + edges. */
|
|
31
|
-
export interface DescribeTree {
|
|
32
|
-
kind: string
|
|
33
|
-
nodes: Array<{ id: string; label: string }>
|
|
34
|
-
edges: Array<{ from: string; to: string; label?: string }>
|
|
35
|
-
entryPoints: string[]
|
|
36
|
-
sinks: string[]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function describeMermaidSource(source: string, opts: DescribeOptions = {}): string {
|
|
40
|
-
const r = parseMermaid(source)
|
|
41
|
-
if (!r.ok) {
|
|
42
|
-
const first = Array.isArray(r.error) ? r.error[0] : undefined
|
|
43
|
-
if (opts.format === 'json') {
|
|
44
|
-
return JSON.stringify({ error: first?.message ?? 'parse error', nodes: [], edges: [] })
|
|
45
|
-
}
|
|
46
|
-
if (opts.format === 'facts') return `error parse ${first?.message ?? 'parse error'}`
|
|
47
|
-
return `Unparseable Mermaid source: ${first?.message ?? 'parse error'}.`
|
|
48
|
-
}
|
|
49
|
-
return describeMermaid(r.value, opts)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function describeMermaid(d: ValidDiagram, opts: DescribeOptions = {}): string {
|
|
53
|
-
if (opts.format === 'json') return JSON.stringify(describeMermaidTree(d))
|
|
54
|
-
if (opts.format === 'facts') return describeMermaidFacts(d).join('\n')
|
|
55
|
-
if (d.body.kind === 'flowchart') return describeFlowchart(d as FlowchartValidDiagram)
|
|
56
|
-
if (d.body.kind === 'state') return describeState(d.body)
|
|
57
|
-
if (d.body.kind === 'sequence') return describeSequence(d as SequenceValidDiagram)
|
|
58
|
-
if (d.body.kind === 'timeline') return describeTimeline(d as TimelineValidDiagram)
|
|
59
|
-
if (d.body.kind === 'class') return describeClass(d as ClassValidDiagram)
|
|
60
|
-
if (d.body.kind === 'er') return describeEr(d as ErValidDiagram)
|
|
61
|
-
if (d.body.kind === 'journey') return describeJourney(d.body)
|
|
62
|
-
if (d.body.kind === 'architecture') return describeArchitecture(d.body)
|
|
63
|
-
if (d.body.kind === 'xychart') return describeXyChart(d.body)
|
|
64
|
-
if (d.body.kind === 'gantt') return describeGantt(d as ValidDiagram & { body: import('./types.ts').GanttBody })
|
|
65
|
-
if (d.body.kind === 'pie') return describePie(d.body)
|
|
66
|
-
if (d.body.kind === 'quadrant') return describeQuadrant(d.body)
|
|
67
|
-
if (d.body.kind === 'opaque') return describeOpaque(d.kind, d.body.source)
|
|
68
|
-
// Exhaustiveness guard: a new family must declare its prose here. This is why
|
|
69
|
-
// pie/quadrant silently fell through to a "not yet supported" line for months.
|
|
70
|
-
const _never: never = d.body
|
|
71
|
-
void _never
|
|
72
|
-
return `A ${d.kind} diagram.`
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* #7349: machine-readable accessibility tree — the graph as a flat node/edge
|
|
77
|
-
* list with entry points and sinks. More useful to agents and screen-reader
|
|
78
|
-
* tooling than prose. Covers the structured families; source-level/opaque
|
|
79
|
-
* families expose extracted labels as nodes and no edges.
|
|
80
|
-
*/
|
|
81
|
-
export function describeMermaidTree(d: ValidDiagram): DescribeTree {
|
|
82
|
-
const tree: DescribeTree = { kind: d.kind, nodes: [], edges: [], entryPoints: [], sinks: [] }
|
|
83
|
-
if (d.body.kind === 'flowchart') {
|
|
84
|
-
const g = d.body.graph
|
|
85
|
-
for (const n of g.nodes.values()) tree.nodes.push({ id: n.id, label: n.label || n.id })
|
|
86
|
-
for (const e of g.edges) tree.edges.push({ from: e.source, to: e.target, label: e.label || undefined })
|
|
87
|
-
} else if (d.body.kind === 'state') {
|
|
88
|
-
const visit = (states: import('./types.ts').StateNode[], transitions: import('./types.ts').StateTransition[]) => {
|
|
89
|
-
for (const s of states) {
|
|
90
|
-
tree.nodes.push({ id: s.id, label: s.label || s.id })
|
|
91
|
-
if (s.states !== undefined) visit(s.states, s.transitions ?? [])
|
|
92
|
-
}
|
|
93
|
-
for (const t of transitions) tree.edges.push({ from: t.from, to: t.to, label: t.label || undefined })
|
|
94
|
-
}
|
|
95
|
-
visit(d.body.states, d.body.transitions)
|
|
96
|
-
} else if (d.body.kind === 'sequence') {
|
|
97
|
-
for (const p of d.body.participants) tree.nodes.push({ id: p.id, label: p.label || p.id })
|
|
98
|
-
d.body.messages.forEach(m => tree.edges.push({ from: m.from, to: m.to, label: m.text || undefined }))
|
|
99
|
-
} else if (d.body.kind === 'class') {
|
|
100
|
-
for (const c of d.body.classes) tree.nodes.push({ id: c.id, label: c.label || c.id })
|
|
101
|
-
for (const r of d.body.relations) tree.edges.push({ from: r.from, to: r.to, label: r.label || r.kind })
|
|
102
|
-
} else if (d.body.kind === 'er') {
|
|
103
|
-
for (const e of d.body.entities) tree.nodes.push({ id: e.id, label: e.id })
|
|
104
|
-
for (const r of d.body.relations) tree.edges.push({ from: r.from, to: r.to, label: r.label || undefined })
|
|
105
|
-
} else if (d.body.kind === 'timeline') {
|
|
106
|
-
for (const s of d.body.sections) for (const p of s.periods) {
|
|
107
|
-
tree.nodes.push({ id: p.id, label: p.label })
|
|
108
|
-
}
|
|
109
|
-
} else if (d.body.kind === 'journey') {
|
|
110
|
-
for (const s of d.body.sections) for (const t of s.tasks) {
|
|
111
|
-
tree.nodes.push({ id: t.id, label: t.text })
|
|
112
|
-
}
|
|
113
|
-
} else if (d.body.kind === 'architecture') {
|
|
114
|
-
for (const g of d.body.groups) tree.nodes.push({ id: g.id, label: g.label || g.id })
|
|
115
|
-
for (const s of d.body.services) tree.nodes.push({ id: s.id, label: s.label || s.id })
|
|
116
|
-
for (const j of d.body.junctions) tree.nodes.push({ id: j.id, label: j.id })
|
|
117
|
-
for (const e of d.body.edges) tree.edges.push({ from: e.source.id, to: e.target.id, label: e.label || undefined })
|
|
118
|
-
} else if (d.body.kind === 'xychart') {
|
|
119
|
-
// Series are the nodes of an xychart AX tree; charts have no edges.
|
|
120
|
-
for (const s of d.body.series) tree.nodes.push({ id: s.id, label: s.name || `${s.kind} series` })
|
|
121
|
-
} else if (d.body.kind === 'gantt') {
|
|
122
|
-
// Tasks are the nodes; after/until references are the edges, so the
|
|
123
|
-
// generic entry/sink pass below reports dependency entry tasks and sinks.
|
|
124
|
-
for (const s of d.body.sections) for (const t of s.tasks) {
|
|
125
|
-
tree.nodes.push({ id: t.taskId ?? t.id, label: t.label })
|
|
126
|
-
}
|
|
127
|
-
const idOf = new Map<string, string>()
|
|
128
|
-
for (const s of d.body.sections) for (const t of s.tasks) if (t.taskId) idOf.set(t.taskId, t.taskId)
|
|
129
|
-
for (const s of d.body.sections) for (const t of s.tasks) {
|
|
130
|
-
for (const [expr, label] of [[t.start, 'after'], [t.end, 'until']] as const) {
|
|
131
|
-
const m = expr?.match(/^(?:after|until)\s+(.+)$/)
|
|
132
|
-
if (!m || !expr!.startsWith(label)) continue
|
|
133
|
-
for (const ref of m[1]!.split(/\s+/).filter(Boolean)) {
|
|
134
|
-
if (idOf.has(ref)) tree.edges.push({ from: ref, to: t.taskId ?? t.id, label })
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
} else if (d.body.kind === 'pie') {
|
|
139
|
-
// Slices are the nodes of a pie AX tree; charts have no edges.
|
|
140
|
-
for (const sl of d.body.slices) tree.nodes.push({ id: sl.id, label: `${sl.label} (${sl.value})` })
|
|
141
|
-
} else if (d.body.kind === 'quadrant') {
|
|
142
|
-
// Plotted points are the nodes; quadrant charts have no edges.
|
|
143
|
-
d.body.points.forEach((p, i) => tree.nodes.push({ id: `point-${i}`, label: `${p.label} [${p.x}, ${p.y}]` }))
|
|
144
|
-
} else if (d.body.kind === 'opaque') {
|
|
145
|
-
const plugin = getFamily(d.kind)
|
|
146
|
-
const labels = (plugin?.extractLabels ?? extractLabelsGeneric)(d.body.source)
|
|
147
|
-
labels.forEach((label, index) => tree.nodes.push({ id: label.target || `label-${index}`, label: label.text }))
|
|
148
|
-
} else {
|
|
149
|
-
// Exhaustiveness guard: a new structured family must add a node/edge branch
|
|
150
|
-
// above rather than silently producing an empty accessibility tree.
|
|
151
|
-
const _never: never = d.body
|
|
152
|
-
void _never
|
|
153
|
-
}
|
|
154
|
-
// Entry points (no incoming) and sinks (no outgoing) from the edge list.
|
|
155
|
-
const incoming = new Set(tree.edges.map(e => e.to))
|
|
156
|
-
const outgoing = new Set(tree.edges.map(e => e.from))
|
|
157
|
-
for (const n of tree.nodes) {
|
|
158
|
-
if (!incoming.has(n.id) && outgoing.has(n.id)) tree.entryPoints.push(n.id)
|
|
159
|
-
if (!outgoing.has(n.id) && incoming.has(n.id)) tree.sinks.push(n.id)
|
|
160
|
-
}
|
|
161
|
-
return tree
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function describeFlowchart(d: FlowchartValidDiagram): string {
|
|
165
|
-
const g = d.body.graph
|
|
166
|
-
const nodes = Array.from(g.nodes.values())
|
|
167
|
-
const nodeIds = nodes.map(n => n.id)
|
|
168
|
-
const edges = g.edges
|
|
169
|
-
const labels = nodes.map(n => n.label || n.id)
|
|
170
|
-
const incoming = new Map<string, number>()
|
|
171
|
-
const outgoing = new Map<string, number>()
|
|
172
|
-
for (const id of nodeIds) { incoming.set(id, 0); outgoing.set(id, 0) }
|
|
173
|
-
for (const e of edges) {
|
|
174
|
-
outgoing.set(e.source, (outgoing.get(e.source) ?? 0) + 1)
|
|
175
|
-
incoming.set(e.target, (incoming.get(e.target) ?? 0) + 1)
|
|
176
|
-
}
|
|
177
|
-
const entries = nodeIds.filter(id => (incoming.get(id) ?? 0) === 0 && (outgoing.get(id) ?? 0) > 0)
|
|
178
|
-
const sinks = nodeIds.filter(id => (outgoing.get(id) ?? 0) === 0 && (incoming.get(id) ?? 0) > 0)
|
|
179
|
-
const edgeStr = edges.map(e => {
|
|
180
|
-
const lbl = e.label ? ` (${e.label})` : ''
|
|
181
|
-
return `${e.source} -> ${e.target}${lbl}`
|
|
182
|
-
})
|
|
183
|
-
let s = `A ${nodes.length}-node flowchart with ${edges.length} edges. Nodes: ${labels.join(', ')}.`
|
|
184
|
-
if (edgeStr.length > 0) s += ` Edges: ${edgeStr.join('; ')}.`
|
|
185
|
-
if (entries.length > 0) s += ` Entry points: ${entries.join(', ')}.`
|
|
186
|
-
if (sinks.length > 0) s += ` Sinks: ${sinks.join(', ')}.`
|
|
187
|
-
return s
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function describeState(body: import('./types.ts').StateBody): string {
|
|
191
|
-
const flat: import('./types.ts').StateNode[] = []
|
|
192
|
-
const allTransitions: import('./types.ts').StateTransition[] = []
|
|
193
|
-
const collect = (states: import('./types.ts').StateNode[], transitions: import('./types.ts').StateTransition[]) => {
|
|
194
|
-
for (const s of states) {
|
|
195
|
-
flat.push(s)
|
|
196
|
-
if (s.states !== undefined) collect(s.states, s.transitions ?? [])
|
|
197
|
-
}
|
|
198
|
-
allTransitions.push(...transitions)
|
|
199
|
-
}
|
|
200
|
-
collect(body.states, body.transitions)
|
|
201
|
-
const composites = flat.filter(s => s.states !== undefined)
|
|
202
|
-
const stateLabels = flat.map(s => s.label || s.id)
|
|
203
|
-
const transStr = allTransitions.map(t => {
|
|
204
|
-
const lbl = t.label ? ` (${t.label})` : ''
|
|
205
|
-
return `${t.from} -> ${t.to}${lbl}`
|
|
206
|
-
})
|
|
207
|
-
const starts = body.transitions.filter(t => t.from === '[*]').map(t => t.to)
|
|
208
|
-
let s = `A state diagram with ${flat.length} states and ${allTransitions.length} transitions.`
|
|
209
|
-
if (composites.length > 0) s += ` Composite states: ${composites.map(c => c.label || c.id).join(', ')}.`
|
|
210
|
-
if (stateLabels.length > 0) s += ` States: ${stateLabels.join(', ')}.`
|
|
211
|
-
if (transStr.length > 0) s += ` Transitions: ${transStr.join('; ')}.`
|
|
212
|
-
if (starts.length > 0) s += ` Initial: ${starts.join(', ')}.`
|
|
213
|
-
return s
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function describeSequence(d: SequenceValidDiagram): string {
|
|
217
|
-
const parts = d.body.participants
|
|
218
|
-
const msgs = d.body.messages
|
|
219
|
-
const partStr = parts.map(p => p.label || p.id).join(', ')
|
|
220
|
-
const msgStr = msgs.map(m => `${m.from} -> ${m.to}: ${m.text}`)
|
|
221
|
-
let s = `A sequence diagram between ${partStr || '(no participants)'}.`
|
|
222
|
-
if (msgStr.length > 0) s += ` Messages in order: ${msgStr.join('; ')}.`
|
|
223
|
-
return s
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function describeTimeline(d: TimelineValidDiagram): string {
|
|
227
|
-
const sections = d.body.sections
|
|
228
|
-
const sectionLabels = sections.map(s => s.label || s.id)
|
|
229
|
-
const periodLabels: string[] = []
|
|
230
|
-
for (const s of sections) for (const p of s.periods) periodLabels.push(p.label)
|
|
231
|
-
let s = `A timeline with ${sections.length} sections.`
|
|
232
|
-
if (sectionLabels.length > 0) s += ` Sections: ${sectionLabels.join(', ')}.`
|
|
233
|
-
if (periodLabels.length > 0) s += ` Periods: ${periodLabels.join(', ')}.`
|
|
234
|
-
return s
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function describeJourney(body: import('./types.ts').JourneyBody): string {
|
|
238
|
-
const sections = body.sections
|
|
239
|
-
const taskCount = sections.reduce((n, s) => n + s.tasks.length, 0)
|
|
240
|
-
const actors = [...new Set(sections.flatMap(s => s.tasks.flatMap(t => t.actors)))]
|
|
241
|
-
let s = `A user journey${body.title ? ` titled "${body.title}"` : ''} with ${sections.length} sections and ${taskCount} tasks.`
|
|
242
|
-
const sectionLabels = sections.map(sec => sec.label).filter((l): l is string => l !== undefined)
|
|
243
|
-
if (sectionLabels.length > 0) s += ` Sections: ${sectionLabels.join(', ')}.`
|
|
244
|
-
const taskStr = sections.flatMap(sec => sec.tasks.map(t => `${t.text} (${t.score})`))
|
|
245
|
-
if (taskStr.length > 0) s += ` Tasks: ${taskStr.join(', ')}.`
|
|
246
|
-
if (actors.length > 0) s += ` Actors: ${actors.join(', ')}.`
|
|
247
|
-
return s
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function describeArchitecture(body: import('./types.ts').ArchitectureBody): string {
|
|
251
|
-
const groups = body.groups
|
|
252
|
-
const services = body.services
|
|
253
|
-
const junctions = body.junctions
|
|
254
|
-
const edges = body.edges
|
|
255
|
-
let s = `An architecture diagram with ${groups.length} groups, ${services.length} services, and ${edges.length} connections.`
|
|
256
|
-
if (groups.length > 0) s += ` Groups: ${groups.map(g => g.label || g.id).join(', ')}.`
|
|
257
|
-
if (services.length > 0) s += ` Services: ${services.map(sv => sv.label || sv.id).join(', ')}.`
|
|
258
|
-
if (junctions.length > 0) s += ` Junctions: ${junctions.map(j => j.id).join(', ')}.`
|
|
259
|
-
const edgeStr = edges.map(e => `${e.source.id} -> ${e.target.id}${e.label ? ` (${e.label})` : ''}`)
|
|
260
|
-
if (edgeStr.length > 0) s += ` Connections: ${edgeStr.join('; ')}.`
|
|
261
|
-
return s
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function describeXyChart(body: import('./types.ts').XyChartBody): string {
|
|
265
|
-
const series = body.series
|
|
266
|
-
const orientation = body.horizontal ? 'horizontal ' : ''
|
|
267
|
-
let s = `An ${orientation}XY chart${body.title ? ` titled "${body.title}"` : ''} with ${series.length} series.`
|
|
268
|
-
const axisDesc = (label: string, axis: import('./types.ts').XyChartAxis | undefined): string | undefined => {
|
|
269
|
-
if (!axis) return undefined
|
|
270
|
-
const parts: string[] = []
|
|
271
|
-
if (axis.name !== undefined) parts.push(`"${axis.name}"`)
|
|
272
|
-
if (axis.categories) parts.push(`categories ${axis.categories.join(', ')}`)
|
|
273
|
-
if (axis.range) parts.push(`range ${axis.range.min}–${axis.range.max}`)
|
|
274
|
-
return parts.length > 0 ? `${label}: ${parts.join(', ')}` : undefined
|
|
275
|
-
}
|
|
276
|
-
const axes = [axisDesc('x-axis', body.xAxis), axisDesc('y-axis', body.yAxis)].filter((a): a is string => a !== undefined)
|
|
277
|
-
if (axes.length > 0) s += ` Axes — ${axes.join('; ')}.`
|
|
278
|
-
const seriesStr = series.map(se => `${se.kind}${se.name ? ` ${se.name}` : ''} [${se.values.join(', ')}]`)
|
|
279
|
-
if (seriesStr.length > 0) s += ` Series: ${seriesStr.join('; ')}.`
|
|
280
|
-
return s
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function describePie(body: import('./types.ts').PieBody): string {
|
|
284
|
-
let s = `A pie chart${body.title ? ` titled "${body.title}"` : ''} with ${body.slices.length} slices.`
|
|
285
|
-
const sliceStr = body.slices.map(sl => `${sl.label} (${sl.value})`)
|
|
286
|
-
if (sliceStr.length > 0) s += ` Slices: ${sliceStr.join(', ')}.`
|
|
287
|
-
return s
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function describeQuadrant(body: import('./types.ts').QuadrantBody): string {
|
|
291
|
-
let s = `A quadrant chart${body.title ? ` titled "${body.title}"` : ''} with ${body.points.length} points.`
|
|
292
|
-
const regions = body.quadrants.filter((q): q is string => Boolean(q))
|
|
293
|
-
if (regions.length > 0) s += ` Quadrants: ${regions.join(', ')}.`
|
|
294
|
-
const pointStr = body.points.map(p => `${p.label} [${p.x}, ${p.y}]`)
|
|
295
|
-
if (pointStr.length > 0) s += ` Points: ${pointStr.join(', ')}.`
|
|
296
|
-
return s
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function describeClass(d: ClassValidDiagram): string {
|
|
300
|
-
const classes = d.body.classes
|
|
301
|
-
const relations = d.body.relations
|
|
302
|
-
const names = classes.map(c => c.id)
|
|
303
|
-
const relStr = relations.map(r => `${r.from} ${r.kind} ${r.to}`)
|
|
304
|
-
let s = `A class diagram with ${classes.length} classes.`
|
|
305
|
-
if (names.length > 0) s += ` Classes: ${names.join(', ')}.`
|
|
306
|
-
if (relStr.length > 0) s += ` Relations: ${relStr.join('; ')}.`
|
|
307
|
-
return s
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function describeEr(d: ErValidDiagram): string {
|
|
311
|
-
const entities = d.body.entities
|
|
312
|
-
const relations = d.body.relations
|
|
313
|
-
const names = entities.map(e => e.id)
|
|
314
|
-
const relStr = relations.map(r => {
|
|
315
|
-
const lbl = r.label ? ` (${r.label})` : ''
|
|
316
|
-
return `${r.from} ${r.leftCard}-${r.rightCard} ${r.to}${lbl}`
|
|
317
|
-
})
|
|
318
|
-
let s = `An ER diagram with ${entities.length} entities.`
|
|
319
|
-
if (names.length > 0) s += ` Entities: ${names.join(', ')}.`
|
|
320
|
-
if (relStr.length > 0) s += ` Relations: ${relStr.join('; ')}.`
|
|
321
|
-
return s
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function describeGantt(d: ValidDiagram & { body: import('./types.ts').GanttBody }): string {
|
|
325
|
-
const body = d.body
|
|
326
|
-
const tasks = body.sections.flatMap(s => s.tasks)
|
|
327
|
-
let s = `A Gantt chart${body.title ? ` titled "${body.title}"` : ''} with ${body.sections.filter(sec => sec.label !== undefined).length} sections and ${tasks.length} tasks.`
|
|
328
|
-
|
|
329
|
-
// Resolve the schedule for date range / critical path; raw expressions are
|
|
330
|
-
// the fallback when the source needs context this body cannot see.
|
|
331
|
-
try {
|
|
332
|
-
const schedule = resolveGanttSchedule(parseGanttModel(toMermaidLines(d.canonicalSource)))
|
|
333
|
-
const fmt = schedule.dateOnly ? '%Y-%m-%d' : '%Y-%m-%d %H:%M'
|
|
334
|
-
s += ` Schedule: ${formatGanttInstant(schedule.timeMin, fmt)} to ${formatGanttInstant(schedule.timeMax, fmt)}.`
|
|
335
|
-
if (schedule.analysis && schedule.analysis.criticalPathTaskIds.length > 0) {
|
|
336
|
-
s += ` Critical path: ${schedule.analysis.criticalPathTaskIds.join(' -> ')}.`
|
|
337
|
-
}
|
|
338
|
-
} catch { /* unresolvable schedule: keep the structural summary */ }
|
|
339
|
-
|
|
340
|
-
const sectionLabels = body.sections.map(sec => sec.label).filter((l): l is string => l !== undefined)
|
|
341
|
-
if (sectionLabels.length > 0) s += ` Sections: ${sectionLabels.join(', ')}.`
|
|
342
|
-
const taskStr = tasks.map(t => {
|
|
343
|
-
const bits: string[] = []
|
|
344
|
-
const status = t.tags.find(tag => tag === 'done' || tag === 'active' || tag === 'crit')
|
|
345
|
-
if (status) bits.push(status)
|
|
346
|
-
if (t.tags.includes('milestone')) bits.push('milestone')
|
|
347
|
-
if (t.tags.includes('vert')) bits.push('vert marker')
|
|
348
|
-
if (t.start) bits.push(t.start)
|
|
349
|
-
bits.push(t.end)
|
|
350
|
-
return `${t.label} (${bits.join(', ')})`
|
|
351
|
-
})
|
|
352
|
-
if (taskStr.length > 0) s += ` Tasks: ${taskStr.join('; ')}.`
|
|
353
|
-
return s
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function describeOpaque(kind: ValidDiagram['kind'], source: string): string {
|
|
357
|
-
const plugin = getFamily(kind)
|
|
358
|
-
const labels = (plugin?.extractLabels ?? extractLabelsGeneric)(source).map(label => label.text)
|
|
359
|
-
const unique = Array.from(new Set(labels)).filter(Boolean)
|
|
360
|
-
const family = kind === 'xychart' ? 'XY chart' : kind
|
|
361
|
-
let s = `A ${family} diagram with a source-level body (structured editing not exposed).`
|
|
362
|
-
if (unique.length > 0) s += ` Labels: ${unique.join(', ')}.`
|
|
363
|
-
return s
|
|
364
|
-
}
|
package/src/agent/er-body.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// ER diagram structured body: parse, serialize, mutate, verify.
|
|
3
|
-
//
|
|
4
|
-
// Supported:
|
|
5
|
-
// CUSTOMER ||--o{ ORDER : places
|
|
6
|
-
// CUSTOMER ||..o{ ORDER : "places (dashed)"
|
|
7
|
-
// CUSTOMER {
|
|
8
|
-
// string name PK
|
|
9
|
-
// string email
|
|
10
|
-
// int age "comment"
|
|
11
|
-
// }
|
|
12
|
-
//
|
|
13
|
-
// Unmodeled (forces opaque):
|
|
14
|
-
// - non-standard cardinality glyphs
|
|
15
|
-
// - directives like `title`
|
|
16
|
-
// ============================================================================
|
|
17
|
-
|
|
18
|
-
import { unknownOpMessage } from './mutation-ops.ts'
|
|
19
|
-
import type {
|
|
20
|
-
ErBody, ErEntity, ErRelation, ErCardinality, ErAttribute,
|
|
21
|
-
ErMutationOp, MutationError, Result, LayoutWarning, VerifyOptions,
|
|
22
|
-
} from './types.ts'
|
|
23
|
-
import { ok, err, DEFAULT_LABEL_CHAR_CAP } from './types.ts'
|
|
24
|
-
import { labelOverflowWarning } from './label-metrics.ts'
|
|
25
|
-
|
|
26
|
-
// ---- Parser ---------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
const LEFT_CARD: Record<string, ErCardinality> = {
|
|
29
|
-
'||': 'one-only', '|o': 'zero-or-one', 'o|': 'zero-or-one',
|
|
30
|
-
'}o': 'zero-or-many', 'o{': 'zero-or-many',
|
|
31
|
-
'}|': 'one-or-many', '|{': 'one-or-many',
|
|
32
|
-
}
|
|
33
|
-
const RIGHT_CARD: Record<string, ErCardinality> = {
|
|
34
|
-
'||': 'one-only', '|o': 'zero-or-one', 'o|': 'zero-or-one',
|
|
35
|
-
'o{': 'zero-or-many', '}o': 'zero-or-many',
|
|
36
|
-
'|{': 'one-or-many', '}|': 'one-or-many',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const REL_RE = /^([A-Za-z_][\w-]*)\s+([|o}{][|o}{])\s*(--|\.\.)\s*([|o}{][|o}{])\s+([A-Za-z_][\w-]*)\s*:\s*(.+)$/
|
|
40
|
-
const ENTITY_OPEN_RE = /^([A-Za-z_][\w-]*)\s*\{$/
|
|
41
|
-
const ENTITY_BARE_RE = /^([A-Za-z_][\w-]*)$/
|
|
42
|
-
|
|
43
|
-
export function parseErBody(lines: string[]): ErBody | null {
|
|
44
|
-
const body: ErBody = { kind: 'er', entities: [], relations: [] }
|
|
45
|
-
const entityMap = new Map<string, ErEntity>()
|
|
46
|
-
const upsert = (id: string): ErEntity => {
|
|
47
|
-
let e = entityMap.get(id)
|
|
48
|
-
if (!e) { e = { id, attributes: [] }; entityMap.set(id, e); body.entities.push(e) }
|
|
49
|
-
return e
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let i = 0
|
|
53
|
-
while (i < lines.length) {
|
|
54
|
-
const raw = lines[i]!.trim()
|
|
55
|
-
i++
|
|
56
|
-
if (!raw || raw.startsWith('%%')) continue
|
|
57
|
-
|
|
58
|
-
const rm = raw.match(REL_RE)
|
|
59
|
-
if (rm) {
|
|
60
|
-
const [, from, leftTok, lineTok, rightTok, to, rawLabel] = rm
|
|
61
|
-
const lc = LEFT_CARD[leftTok!]
|
|
62
|
-
const rc = RIGHT_CARD[rightTok!]
|
|
63
|
-
if (!lc || !rc) return null
|
|
64
|
-
let label: string = rawLabel!.trim()
|
|
65
|
-
if (label.startsWith('"') && label.endsWith('"')) label = label.slice(1, -1)
|
|
66
|
-
body.relations.push({ from: from!, to: to!, leftCard: lc, rightCard: rc, dashed: lineTok === '..', label: label || undefined })
|
|
67
|
-
upsert(from!); upsert(to!)
|
|
68
|
-
continue
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Bare entity declaration: `CUSTOMER` (no attributes)
|
|
72
|
-
const bm = raw.match(ENTITY_BARE_RE)
|
|
73
|
-
if (bm) { upsert(bm[1]!); continue }
|
|
74
|
-
|
|
75
|
-
// Entity with attribute block
|
|
76
|
-
const om = raw.match(ENTITY_OPEN_RE)
|
|
77
|
-
if (om) {
|
|
78
|
-
const e = upsert(om[1]!)
|
|
79
|
-
while (i < lines.length) {
|
|
80
|
-
const al = lines[i]!.trim()
|
|
81
|
-
i++
|
|
82
|
-
if (!al || al.startsWith('%%')) continue
|
|
83
|
-
if (al === '}') break
|
|
84
|
-
e.attributes.push({ text: al })
|
|
85
|
-
}
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return null
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return body
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---- Serializer -----------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
const LEFT_GLYPH: Record<ErCardinality, string> = {
|
|
98
|
-
'one-only': '||', 'zero-or-one': '|o', 'zero-or-many': '}o', 'one-or-many': '}|',
|
|
99
|
-
}
|
|
100
|
-
const RIGHT_GLYPH: Record<ErCardinality, string> = {
|
|
101
|
-
'one-only': '||', 'zero-or-one': 'o|', 'zero-or-many': 'o{', 'one-or-many': '|{',
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function renderEr(body: ErBody): string {
|
|
105
|
-
const lines: string[] = ['erDiagram']
|
|
106
|
-
for (const e of body.entities) {
|
|
107
|
-
if (e.attributes.length === 0) {
|
|
108
|
-
lines.push(` ${e.id}`)
|
|
109
|
-
} else {
|
|
110
|
-
lines.push(` ${e.id} {`)
|
|
111
|
-
for (const a of e.attributes) lines.push(` ${a.text}`)
|
|
112
|
-
lines.push(` }`)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
for (const r of body.relations) {
|
|
116
|
-
const left = LEFT_GLYPH[r.leftCard]
|
|
117
|
-
const right = RIGHT_GLYPH[r.rightCard]
|
|
118
|
-
const link = r.dashed ? '..' : '--'
|
|
119
|
-
const label = r.label ? r.label : ''
|
|
120
|
-
// An empty label serializes as `: ""` — the parser strips the quotes back
|
|
121
|
-
// to undefined, and a bare trailing `:` would not re-parse (opaque fallback).
|
|
122
|
-
lines.push(` ${r.from} ${left}${link}${right} ${r.to} : ${label === '' || label.includes(' ') ? `"${label}"` : label}`)
|
|
123
|
-
}
|
|
124
|
-
return lines.join('\n') + '\n'
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ---- Mutator --------------------------------------------------------------
|
|
128
|
-
|
|
129
|
-
function cloneEr(body: ErBody): ErBody {
|
|
130
|
-
return {
|
|
131
|
-
kind: 'er',
|
|
132
|
-
entities: body.entities.map(e => ({ id: e.id, attributes: e.attributes.map(a => ({ ...a })) })),
|
|
133
|
-
relations: body.relations.map(r => ({ ...r })),
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function mutateEr(body: ErBody, op: ErMutationOp): Result<ErBody, MutationError> {
|
|
138
|
-
const b = cloneEr(body)
|
|
139
|
-
const find = (id: string) => b.entities.find(e => e.id === id)
|
|
140
|
-
|
|
141
|
-
switch (op.kind) {
|
|
142
|
-
case 'add_entity': {
|
|
143
|
-
if (find(op.id)) return err({ code: 'DUPLICATE_ENTITY', message: `entity ${op.id} already exists` })
|
|
144
|
-
b.entities.push({ id: op.id, attributes: (op.attributes ?? []).map(text => ({ text })) })
|
|
145
|
-
return ok(b)
|
|
146
|
-
}
|
|
147
|
-
case 'remove_entity': {
|
|
148
|
-
const i = b.entities.findIndex(e => e.id === op.id)
|
|
149
|
-
if (i < 0) return err({ code: 'ENTITY_NOT_FOUND', message: `entity ${op.id} not found` })
|
|
150
|
-
b.entities.splice(i, 1)
|
|
151
|
-
b.relations = b.relations.filter(r => r.from !== op.id && r.to !== op.id)
|
|
152
|
-
return ok(b)
|
|
153
|
-
}
|
|
154
|
-
case 'rename_entity': {
|
|
155
|
-
const e = find(op.from)
|
|
156
|
-
if (!e) return err({ code: 'ENTITY_NOT_FOUND', message: `entity ${op.from} not found` })
|
|
157
|
-
if (find(op.to)) return err({ code: 'DUPLICATE_ENTITY', message: `entity ${op.to} already exists` })
|
|
158
|
-
e.id = op.to
|
|
159
|
-
for (const r of b.relations) {
|
|
160
|
-
if (r.from === op.from) r.from = op.to
|
|
161
|
-
if (r.to === op.from) r.to = op.to
|
|
162
|
-
}
|
|
163
|
-
return ok(b)
|
|
164
|
-
}
|
|
165
|
-
case 'add_attribute': {
|
|
166
|
-
const e = find(op.entity)
|
|
167
|
-
if (!e) return err({ code: 'ENTITY_NOT_FOUND', message: `entity ${op.entity} not found` })
|
|
168
|
-
e.attributes.push({ text: op.text })
|
|
169
|
-
return ok(b)
|
|
170
|
-
}
|
|
171
|
-
case 'remove_attribute': {
|
|
172
|
-
const e = find(op.entity)
|
|
173
|
-
if (!e) return err({ code: 'ENTITY_NOT_FOUND', message: `entity ${op.entity} not found` })
|
|
174
|
-
if (op.index < 0 || op.index >= e.attributes.length) return err({ code: 'ATTRIBUTE_NOT_FOUND', message: `attribute index ${op.index} out of range` })
|
|
175
|
-
e.attributes.splice(op.index, 1)
|
|
176
|
-
return ok(b)
|
|
177
|
-
}
|
|
178
|
-
case 'add_relation': {
|
|
179
|
-
if (!find(op.from)) return err({ code: 'ENTITY_NOT_FOUND', message: `entity ${op.from} not found` })
|
|
180
|
-
if (!find(op.to)) return err({ code: 'ENTITY_NOT_FOUND', message: `entity ${op.to} not found` })
|
|
181
|
-
b.relations.push({ from: op.from, to: op.to, leftCard: op.leftCard, rightCard: op.rightCard, dashed: op.dashed ?? false, label: op.label })
|
|
182
|
-
return ok(b)
|
|
183
|
-
}
|
|
184
|
-
case 'remove_relation': {
|
|
185
|
-
if (op.index < 0 || op.index >= b.relations.length) return err({ code: 'RELATION_NOT_FOUND', message: `relation index ${op.index} out of range` })
|
|
186
|
-
b.relations.splice(op.index, 1)
|
|
187
|
-
return ok(b)
|
|
188
|
-
}
|
|
189
|
-
default:
|
|
190
|
-
return err({ code: 'INVALID_OP', message: unknownOpMessage('er', op) })
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ---- Verifier -------------------------------------------------------------
|
|
195
|
-
|
|
196
|
-
export function verifyErBody(body: ErBody, opts: VerifyOptions): LayoutWarning[] {
|
|
197
|
-
const warnings: LayoutWarning[] = []
|
|
198
|
-
const cap = opts.labelCharCap ?? DEFAULT_LABEL_CHAR_CAP
|
|
199
|
-
if (body.entities.length === 0 && body.relations.length === 0) {
|
|
200
|
-
warnings.push({ code: 'EMPTY_DIAGRAM' })
|
|
201
|
-
return warnings
|
|
202
|
-
}
|
|
203
|
-
const ids = new Set(body.entities.map(e => e.id))
|
|
204
|
-
const overflow = (target: string, text: string) => {
|
|
205
|
-
const w = labelOverflowWarning(target, text, cap)
|
|
206
|
-
if (w) warnings.push(w)
|
|
207
|
-
}
|
|
208
|
-
for (const e of body.entities) {
|
|
209
|
-
for (let i = 0; i < e.attributes.length; i++) {
|
|
210
|
-
overflow(`${e.id}#a${i}`, e.attributes[i]!.text)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
for (let i = 0; i < body.relations.length; i++) {
|
|
214
|
-
const r = body.relations[i]!
|
|
215
|
-
if (!ids.has(r.from) || !ids.has(r.to)) {
|
|
216
|
-
warnings.push({
|
|
217
|
-
code: 'EDGE_MISANCHORED', edge: `rel#${i}:${r.from}->${r.to}`,
|
|
218
|
-
from: ids.has(r.from) ? r.from : undefined, to: ids.has(r.to) ? r.to : undefined,
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
if (r.label) overflow(`rel#${i}`, r.label)
|
|
222
|
-
}
|
|
223
|
-
return warnings
|
|
224
|
-
}
|