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/verify.ts
DELETED
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// verifyMermaid — Tier 1 (source/structure) + Tier 2 (geometric) + Tier 3 (lint).
|
|
3
|
-
//
|
|
4
|
-
// v4: no LayoutContext, no seed wrapper (ELK is deterministic on its own).
|
|
5
|
-
// LABEL_OVERFLOW is a rendered-line char-count check (Tier 1): the cap applies
|
|
6
|
-
// to the longest displayed line (entities decoded, <br> splits), not raw
|
|
7
|
-
// source chars — see label-metrics.ts.
|
|
8
|
-
// ============================================================================
|
|
9
|
-
|
|
10
|
-
import { parseMermaid as parseValidDiagram } from './parse.ts'
|
|
11
|
-
import { serializeMermaid } from './serialize.ts'
|
|
12
|
-
import { logToolInvocation } from './trace-log.ts'
|
|
13
|
-
import { countStructuralElements, faithfulnessWarning } from './structural-count.ts'
|
|
14
|
-
import { layoutGraphSync } from '../layout-engine.ts'
|
|
15
|
-
import { renderMermaidSVG } from '../index.ts'
|
|
16
|
-
import { parseMermaid as parseFlowchartLegacy } from '../parser.ts'
|
|
17
|
-
import { auditRouteContracts, findRouteHitches } from '../route-contracts.ts'
|
|
18
|
-
import type {
|
|
19
|
-
ValidDiagram, VerifyOptions, VerifyResult, LayoutWarning, RenderedLayout,
|
|
20
|
-
WarningCode, SequenceBody,
|
|
21
|
-
} from './types.ts'
|
|
22
|
-
import { WARNING_SEVERITY, DEFAULT_LABEL_CHAR_CAP } from './types.ts'
|
|
23
|
-
import { positionedToRenderedLayout, emptyRenderedLayout } from './layout-to-rendered.ts'
|
|
24
|
-
import { layoutFamilyToRendered, ganttGeometryWarnings, ganttScheduleWarning, layoutGeometryWarnings } from './family-layouts.ts'
|
|
25
|
-
import { getFamily, extractLabelsGeneric, builtinFamilyMetadata } from './families.ts'
|
|
26
|
-
import { labelOverflowWarning } from './label-metrics.ts'
|
|
27
|
-
import { stateBodyToGraph } from './state-body.ts'
|
|
28
|
-
import { flowchartUnsupportedSyntaxWarnings } from './flowchart-unsupported.ts'
|
|
29
|
-
import './families-builtin.ts' // registers built-in families at import time
|
|
30
|
-
|
|
31
|
-
const KNOWN_SHAPES = new Set([
|
|
32
|
-
'rectangle', 'service', 'rounded', 'diamond', 'stadium', 'circle',
|
|
33
|
-
'subroutine', 'doublecircle', 'hexagon', 'cylinder', 'asymmetric',
|
|
34
|
-
'trapezoid', 'trapezoid-alt', 'lean-r', 'lean-l', 'state-start', 'state-end',
|
|
35
|
-
])
|
|
36
|
-
|
|
37
|
-
function opaqueSourceHasOnlyHeader(kind: ValidDiagram['kind'], source: string): boolean {
|
|
38
|
-
const statements = source
|
|
39
|
-
.split(/[;\n]/)
|
|
40
|
-
.map(part => part.trim())
|
|
41
|
-
.filter(part => part && !part.startsWith('%%'))
|
|
42
|
-
if (statements.length === 0) return true
|
|
43
|
-
if (statements.length > 1) return false
|
|
44
|
-
const header = statements[0]!.toLowerCase()
|
|
45
|
-
// Header aliases come from the family metadata table — the single source of
|
|
46
|
-
// truth — instead of a third hand-encoding of the same strings.
|
|
47
|
-
const aliases = (builtinFamilyMetadata(kind)?.headers ?? [kind]).map(h => h.toLowerCase())
|
|
48
|
-
return aliases.some(alias => header === alias || header.startsWith(`${alias} `))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Tier-3 faithfulness lint: does the structured {nodes, edges, groups} tally
|
|
53
|
-
* survive a parse → serialize → re-parse cycle? Round-trip *byte*-stability
|
|
54
|
-
* (already gated elsewhere) only proves serialize∘parse is idempotent; this
|
|
55
|
-
* proves canonical serialization is not silently dropping a node/edge/group —
|
|
56
|
-
* the ER `}o` class of bug. Promoted from a corpus-only check + the LLM-judge
|
|
57
|
-
* helper so it now runs on EVERY verify, for every family. Opaque bodies carry
|
|
58
|
-
* no structured arrays (their faithfulness is byte-verbatim) and are skipped.
|
|
59
|
-
*/
|
|
60
|
-
function roundtripFaithfulnessWarnings(d: ValidDiagram): LayoutWarning[] {
|
|
61
|
-
// Thin I/O wrapper: do the parse → serialize → re-parse, then defer the
|
|
62
|
-
// verdict to the pure (mutation-gated, unit-tested) faithfulnessWarning.
|
|
63
|
-
const before = countStructuralElements(d)
|
|
64
|
-
if (!before) return []
|
|
65
|
-
try {
|
|
66
|
-
const reparsed = parseValidDiagram(serializeMermaid(d))
|
|
67
|
-
if (!reparsed.ok) return faithfulnessWarning(before, null) // total loss
|
|
68
|
-
const after = countStructuralElements(reparsed.value)
|
|
69
|
-
if (!after) return [] // reparsed to an opaque body — the round-trip gate owns that
|
|
70
|
-
return faithfulnessWarning(before, after)
|
|
71
|
-
} catch {
|
|
72
|
-
// Serialization/parse threw — the round-trip-stability gate owns that
|
|
73
|
-
// failure mode; don't double-report it as a faithfulness drop.
|
|
74
|
-
return []
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function verifyMermaid(input: ValidDiagram | string, opts: VerifyOptions = {}): VerifyResult {
|
|
79
|
-
logToolInvocation('verify')
|
|
80
|
-
return withRenderParity(input, verifyStructure(input, opts), opts)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Render-parity gate — generalizes UNRESOLVABLE_SCHEDULE's seam-closing to
|
|
85
|
-
* every family. The agent parser preserves unmodeled syntax verbatim, but the
|
|
86
|
-
* render parser is strict, so a diagram could verify clean and still make
|
|
87
|
-
* `am render` exit 4 (observed live: onboarding agents followed
|
|
88
|
-
* verify-before-commit and shipped unrenderable quadrant/architecture
|
|
89
|
-
* diagrams). A clean verify now proves the canonical source actually renders.
|
|
90
|
-
* Skipped when verify already failed — the render error would only repeat a
|
|
91
|
-
* diagnosis the caller already has to act on — and when the caller
|
|
92
|
-
* suppressed ANY error-severity code: suppression means "I acknowledge this
|
|
93
|
-
* failure class, proceed", and the gate must not resurrect the acknowledged
|
|
94
|
-
* failure under a different name (e.g. suppress UNRESOLVABLE_SCHEDULE on a
|
|
95
|
-
* gantt whose render still throws for exactly that reason).
|
|
96
|
-
*/
|
|
97
|
-
function withRenderParity(input: ValidDiagram | string, result: VerifyResult, opts: VerifyOptions): VerifyResult {
|
|
98
|
-
if (!result.ok || (opts.suppress ?? []).some(code => code === 'RENDER_FAILED' || WARNING_SEVERITY[code] === 'error')) return result
|
|
99
|
-
const source = typeof input === 'string' ? input : serializeMermaid(input)
|
|
100
|
-
try {
|
|
101
|
-
renderMermaidSVG(source)
|
|
102
|
-
return result
|
|
103
|
-
} catch (e) {
|
|
104
|
-
const reason = e instanceof Error ? e.message : String(e)
|
|
105
|
-
return { ...result, ok: false, warnings: [...result.warnings, { code: 'RENDER_FAILED', reason }] }
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function verifyStructure(input: ValidDiagram | string, opts: VerifyOptions = {}): VerifyResult {
|
|
110
|
-
const d = typeof input === 'string' ? unwrap(input) : input
|
|
111
|
-
if (!d) return finalize([{ code: 'EMPTY_DIAGRAM' }], emptyRenderedLayout('flowchart'), opts)
|
|
112
|
-
|
|
113
|
-
const cap = opts.labelCharCap ?? DEFAULT_LABEL_CHAR_CAP
|
|
114
|
-
|
|
115
|
-
// Family-plugin verify dispatcher pass: every registered family's `verify`
|
|
116
|
-
// hook gets a chance to contribute warnings. Runs ahead of per-body branches
|
|
117
|
-
// so plugins can hook into any body kind (structured or opaque). Closes the
|
|
118
|
-
// dead-code gap where `FamilyPlugin.verify` was declared but never invoked.
|
|
119
|
-
// 2C comment policy: in-body comments that structured serialization drops
|
|
120
|
-
// (recorded at parse time) surface here as the Tier 3 COMMENT_DROPPED lint,
|
|
121
|
-
// so the loss is announced rather than silent.
|
|
122
|
-
const metaWarnings: LayoutWarning[] = d.meta.droppedComments?.length
|
|
123
|
-
? [{ code: 'COMMENT_DROPPED', count: d.meta.droppedComments.length, lines: d.meta.droppedComments.map(c => c.line) }]
|
|
124
|
-
: []
|
|
125
|
-
const sourceWarnings = d.kind === 'flowchart' ? flowchartUnsupportedSyntaxWarnings(d.canonicalSource) : []
|
|
126
|
-
const faithfulnessWarnings = roundtripFaithfulnessWarnings(d)
|
|
127
|
-
const pluginWarnings = dedupedConcat(dedupedConcat(dedupedConcat(metaWarnings, dispatchFamilyVerify(d, opts)), sourceWarnings), faithfulnessWarnings)
|
|
128
|
-
|
|
129
|
-
if (d.body.kind === 'sequence') return mergeFinalize(verifySequence(d as ValidDiagram & { body: SequenceBody }, cap, opts), pluginWarnings, opts)
|
|
130
|
-
if (d.body.kind === 'timeline') return mergeFinalize(verifyTimeline(d as ValidDiagram & { body: import('./types.ts').TimelineBody }, cap, opts), pluginWarnings, opts)
|
|
131
|
-
// class + ER: the FamilyPlugin.verify hooks (registered in families-builtin.ts)
|
|
132
|
-
// already produce the per-body warnings. Loop 9 M2 removes the duplicate
|
|
133
|
-
// explicit branches; the dispatcher path + emptyRenderedLayout fall-through
|
|
134
|
-
// does the work. Dedup is unnecessary now (single source of truth) so we
|
|
135
|
-
// emit pluginWarnings directly.
|
|
136
|
-
// class + ER + journey + architecture: the FamilyPlugin.verify hooks produce
|
|
137
|
-
// the per-body warnings (journey added by BUILD-15, architecture by BUILD-17).
|
|
138
|
-
// Gantt adds geometric tripwires (OFF_CANVAS / GROUP_BREACH) over its real
|
|
139
|
-
// layout and surfaces unresolvable schedules (UNRESOLVABLE_SCHEDULE),
|
|
140
|
-
// alongside the body-level plugin warnings — see docs/design/families/gantt.md
|
|
141
|
-
// §Verification.
|
|
142
|
-
if (d.body.kind === 'gantt') {
|
|
143
|
-
const layout = layoutFamilyToRendered(d) ?? emptyRenderedLayout(d.kind)
|
|
144
|
-
const schedFail = ganttScheduleWarning(d)
|
|
145
|
-
const geometric = dedupedConcat(ganttGeometryWarnings(layout), schedFail ? [schedFail] : [])
|
|
146
|
-
return finalize(dedupedConcat(pluginWarnings, geometric), layout, opts)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (d.body.kind === 'class' || d.body.kind === 'er' || d.body.kind === 'journey' || d.body.kind === 'architecture' || d.body.kind === 'xychart' || d.body.kind === 'pie' || d.body.kind === 'quadrant') {
|
|
150
|
-
// QUAL-1: verify.layout is now truthful — the real positioned layout from
|
|
151
|
-
// the family adapters (was emptyRenderedLayout). #33 adds zero-noise
|
|
152
|
-
// class/ER semantic geometry tripwires: relationship endpoints must sit on
|
|
153
|
-
// class/entity box boundaries and boxes must remain on-canvas/non-overlap.
|
|
154
|
-
const layout = layoutFamilyToRendered(d) ?? emptyRenderedLayout(d.kind)
|
|
155
|
-
const familyGeometry = (d.body.kind === 'class' || d.body.kind === 'er')
|
|
156
|
-
? layoutGeometryWarnings(layout, { edgeAnchors: true, nodeOverlaps: true })
|
|
157
|
-
: layoutGeometryWarnings(layout, {
|
|
158
|
-
nodeOverlaps: d.body.kind === 'journey',
|
|
159
|
-
groupContainment: d.body.kind === 'xychart' || d.body.kind === 'quadrant',
|
|
160
|
-
})
|
|
161
|
-
return finalize(dedupedConcat(pluginWarnings, familyGeometry), layout, opts)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// State diagrams (BUILD-19): the StateBody projects to a MermaidGraph via the
|
|
165
|
-
// legacy state parser — the exact graph the renderer lays out — so the full
|
|
166
|
-
// flowchart Tier 1 + Tier 2 geometric path runs unchanged. pluginWarnings
|
|
167
|
-
// (verifyState) add the body-level structural checks on the StateBody itself.
|
|
168
|
-
if (d.body.kind === 'state') {
|
|
169
|
-
const graph = stateBodyToGraph(d.body)
|
|
170
|
-
if (graph.nodes.size === 0) return finalize(dedupedConcat([{ code: 'EMPTY_DIAGRAM' }], pluginWarnings), emptyRenderedLayout(d.kind), opts)
|
|
171
|
-
const { warnings, layout } = verifyGraph(graph, d.kind, cap)
|
|
172
|
-
return finalize(dedupedConcat(warnings, pluginWarnings), layout, opts)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (d.body.kind === 'opaque') {
|
|
176
|
-
const isEmpty = opaqueSourceHasOnlyHeader(d.kind, d.body.source)
|
|
177
|
-
// Universal Tier 1 LABEL_OVERFLOW via family-specific (or generic) label
|
|
178
|
-
// extraction. Closes the gap where opaque-body diagrams (class / ER /
|
|
179
|
-
// journey / xychart / architecture / sequence-with-alt/etc.) never got
|
|
180
|
-
// label-cap checking.
|
|
181
|
-
const plugin = getFamily(d.kind)
|
|
182
|
-
const labels = (plugin?.extractLabels ?? extractLabelsGeneric)(d.body.source)
|
|
183
|
-
const warnings: LayoutWarning[] = isEmpty ? [{ code: 'EMPTY_DIAGRAM' }] : []
|
|
184
|
-
// Systemic silent-opaque signal: a non-empty opaque body means the parser
|
|
185
|
-
// met syntax it does not model and preserved the diagram verbatim, so the
|
|
186
|
-
// `as*` narrower returns null and typed mutation is unavailable. Flowchart
|
|
187
|
-
// and quadrant already emit a MORE SPECIFIC UNSUPPORTED_SYNTAX naming the
|
|
188
|
-
// exact construct (via sourceWarnings / the quadrant verify hook, both in
|
|
189
|
-
// pluginWarnings) — do not double-flag those. This one line generalizes
|
|
190
|
-
// that announcement to the other families (class/state/er/xychart/pie/
|
|
191
|
-
// journey/timeline/architecture/sequence) that used to fall opaque silently.
|
|
192
|
-
if (!isEmpty && !pluginWarnings.some(w => w.code === 'UNSUPPORTED_SYNTAX')) {
|
|
193
|
-
warnings.push({
|
|
194
|
-
code: 'UNSUPPORTED_SYNTAX',
|
|
195
|
-
syntax: `${d.kind}_opaque`,
|
|
196
|
-
message: `This ${d.kind} diagram uses syntax the structured parser does not model; it is preserved verbatim as source. Typed mutation (the ${d.kind} \`as*\` narrower) is unavailable — edit the source directly, or verify/render it as-is.`,
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
const seen = new Set<string>()
|
|
200
|
-
for (const lbl of labels) {
|
|
201
|
-
const w = labelOverflowWarning(lbl.target, lbl.text, cap)
|
|
202
|
-
if (!w) continue
|
|
203
|
-
const key = `${lbl.target}:${lbl.text}`
|
|
204
|
-
if (seen.has(key)) continue
|
|
205
|
-
seen.add(key)
|
|
206
|
-
warnings.push(w)
|
|
207
|
-
}
|
|
208
|
-
// QUAL-1: opaque bodies of renderable families (pie/quadrant always, and
|
|
209
|
-
// class/er/journey/architecture/xychart when unmodeled) still produce a
|
|
210
|
-
// real positioned layout from canonicalSource. layoutFamilyToRendered
|
|
211
|
-
// degrades to an empty layout on render-error, so this never throws.
|
|
212
|
-
const opaqueLayout = d.kind === 'flowchart'
|
|
213
|
-
? layoutOpaqueFlowchart(d)
|
|
214
|
-
: layoutFamilyToRendered(d) ?? emptyRenderedLayout(d.kind)
|
|
215
|
-
// Opaque bodies are preserved-not-rendered: an empty local layout means
|
|
216
|
-
// the syntax is unmodeled, not that the diagram is empty — the isEmpty
|
|
217
|
-
// header-only check above owns genuine emptiness here.
|
|
218
|
-
return finalize(dedupedConcat(warnings, pluginWarnings), opaqueLayout, opts, false)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const graph = d.body.graph
|
|
222
|
-
if (graph.nodes.size === 0) return finalize([{ code: 'EMPTY_DIAGRAM' }], emptyRenderedLayout(d.kind), opts)
|
|
223
|
-
const { warnings: graphWarnings, layout: graphLayout } = verifyGraph(graph, d.kind, cap)
|
|
224
|
-
return finalize(dedupedConcat(graphWarnings, pluginWarnings), graphLayout, opts)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Full Tier 1 (structural) + Tier 2 (geometric) + Tier 3 (lint) verify over a
|
|
229
|
-
* MermaidGraph. Shared by flowchart bodies and state-diagram bodies (which
|
|
230
|
-
* project to a graph via stateBodyToGraph). Returns warnings + the rendered
|
|
231
|
-
* layout; the caller finalizes (suppress + ok flag).
|
|
232
|
-
*/
|
|
233
|
-
function verifyGraph(graph: import('../types.ts').MermaidGraph, kind: ValidDiagram['kind'], cap: number): { warnings: LayoutWarning[]; layout: RenderedLayout } {
|
|
234
|
-
const positioned = layoutGraphSync(graph, {})
|
|
235
|
-
const layout = positionedToRenderedLayout(positioned, kind)
|
|
236
|
-
const warnings: LayoutWarning[] = []
|
|
237
|
-
|
|
238
|
-
// Tier 1 — structural
|
|
239
|
-
for (const edge of graph.edges) {
|
|
240
|
-
const hasSource = graph.nodes.has(edge.source) || Boolean(findSubgraphById(graph.subgraphs, edge.source))
|
|
241
|
-
const hasTarget = graph.nodes.has(edge.target) || Boolean(findSubgraphById(graph.subgraphs, edge.target))
|
|
242
|
-
if (!hasSource || !hasTarget) {
|
|
243
|
-
warnings.push({
|
|
244
|
-
code: 'EDGE_MISANCHORED', edge: `${edge.source}->${edge.target}`,
|
|
245
|
-
from: hasSource ? edge.source : undefined,
|
|
246
|
-
to: hasTarget ? edge.target : undefined,
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
for (const [id, node] of graph.nodes) {
|
|
251
|
-
if (!KNOWN_SHAPES.has(node.shape)) warnings.push({ code: 'UNKNOWN_SHAPE', node: id, shape: String(node.shape) })
|
|
252
|
-
const w = labelOverflowWarning(id, node.label, cap)
|
|
253
|
-
if (w) warnings.push(w)
|
|
254
|
-
}
|
|
255
|
-
for (const edge of graph.edges) {
|
|
256
|
-
const w = edge.label ? labelOverflowWarning(`${edge.source}->${edge.target}`, edge.label, cap) : null
|
|
257
|
-
if (w) warnings.push(w)
|
|
258
|
-
}
|
|
259
|
-
for (const n of positioned.nodes) {
|
|
260
|
-
// Report x and y independently so a node off-canvas on both axes surfaces
|
|
261
|
-
// both, instead of masking the second behind an else-if.
|
|
262
|
-
if (n.x < 0 || n.x + n.width > positioned.width + 1) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'x' })
|
|
263
|
-
if (n.y < 0 || n.y + n.height > positioned.height + 1) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'y' })
|
|
264
|
-
}
|
|
265
|
-
for (const g of positioned.groups) {
|
|
266
|
-
const visit = (group: typeof g) => {
|
|
267
|
-
const sg = findSubgraphById(graph.subgraphs, group.id)
|
|
268
|
-
if (sg) for (const n of positioned.nodes) {
|
|
269
|
-
if (!sg.nodeIds.includes(n.id)) continue
|
|
270
|
-
const inside = n.x >= group.x && n.y >= group.y &&
|
|
271
|
-
n.x + n.width <= group.x + group.width + 0.5 && n.y + n.height <= group.y + group.height + 0.5
|
|
272
|
-
if (!inside) warnings.push({ code: 'GROUP_BREACH', group: group.id, member: n.id })
|
|
273
|
-
}
|
|
274
|
-
for (const c of group.children) visit(c)
|
|
275
|
-
}
|
|
276
|
-
visit(g)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Tier 2 — geometric
|
|
280
|
-
for (let i = 0; i < positioned.nodes.length; i++) {
|
|
281
|
-
for (let j = i + 1; j < positioned.nodes.length; j++) {
|
|
282
|
-
const a = positioned.nodes[i]!, b = positioned.nodes[j]!
|
|
283
|
-
const o = rectIntersection(a, b)
|
|
284
|
-
if (o > 0) warnings.push({ code: 'NODE_OVERLAP', a: a.id, b: b.id, areaPx: Math.round(o) })
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
for (const e of positioned.edges) {
|
|
288
|
-
const c = countSelfCrossings(e.points)
|
|
289
|
-
if (c > 0) warnings.push({ code: 'ROUTE_SELF_CROSS', edge: `${e.source}->${e.target}`, count: c })
|
|
290
|
-
}
|
|
291
|
-
// Route-contract tripwires over FINAL geometry: the layout pipeline already
|
|
292
|
-
// upholds these invariants itself (straight clear lanes, border-anchored
|
|
293
|
-
// containers, on-shape endpoints, labels on their own lines), so any hit
|
|
294
|
-
// here means some pass mutated geometry after route certification.
|
|
295
|
-
// See docs/design/system/route-contracts.md.
|
|
296
|
-
for (const hitch of findRouteHitches(positioned, graph)) {
|
|
297
|
-
warnings.push({ code: 'ROUTE_HITCH', edge: hitch.edge, deviationPx: hitch.deviationPx })
|
|
298
|
-
}
|
|
299
|
-
warnings.push(...auditRouteContracts(positioned, graph))
|
|
300
|
-
|
|
301
|
-
// Tier 3 — advisory lint for common agent mistakes that still parse/render.
|
|
302
|
-
warnings.push(...lintFlowchartGraph(graph))
|
|
303
|
-
|
|
304
|
-
return { warnings, layout }
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function dedupedConcat(a: LayoutWarning[], b: LayoutWarning[]): LayoutWarning[] {
|
|
308
|
-
if (b.length === 0) return a
|
|
309
|
-
const seen = new Set(a.map(warningKey))
|
|
310
|
-
const novel = b.filter(w => !seen.has(warningKey(w)))
|
|
311
|
-
return novel.length === 0 ? a : [...a, ...novel]
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Run the registered FamilyPlugin.verify hook for this diagram's kind.
|
|
316
|
-
* Returns the warnings the plugin produced, or [] when no plugin / no hook.
|
|
317
|
-
*/
|
|
318
|
-
function dispatchFamilyVerify(d: ValidDiagram, opts: VerifyOptions): LayoutWarning[] {
|
|
319
|
-
const plugin = getFamily(d.kind)
|
|
320
|
-
if (!plugin?.verify) return []
|
|
321
|
-
try {
|
|
322
|
-
return plugin.verify(d.body, opts)
|
|
323
|
-
} catch {
|
|
324
|
-
// A faulty plugin shouldn't blow up verifyMermaid. Silent skip is acceptable
|
|
325
|
-
// for an optional hook; the test suite catches bugs in built-in plugins.
|
|
326
|
-
return []
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** finalize() variant that merges an already-finalized result with extra warnings.
|
|
331
|
-
* Loop 9 M10: now delegates fully to dedupedConcat → finalize. Dedupes on
|
|
332
|
-
* (code, target/edge/node) so a plugin verify hook returning a warning
|
|
333
|
-
* identical to one the per-body verify already produced doesn't surface twice.
|
|
334
|
-
* The dispatcher has been live since Loop 7 M1, so this hazard remains real. */
|
|
335
|
-
function mergeFinalize(prev: VerifyResult, extra: LayoutWarning[], opts: VerifyOptions): VerifyResult {
|
|
336
|
-
if (extra.length === 0) return prev
|
|
337
|
-
const merged = dedupedConcat(prev.warnings, extra)
|
|
338
|
-
if (merged === prev.warnings) return prev
|
|
339
|
-
// prev already went through the caller's guardEmptyLayout choice — don't
|
|
340
|
-
// second-guess it here.
|
|
341
|
-
return finalize(merged, prev.layout, opts, false)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function warningKey(w: LayoutWarning): string {
|
|
345
|
-
if (w.code === 'UNSUPPORTED_SYNTAX') return `${w.code}:${w.syntax}:${w.line ?? ''}`
|
|
346
|
-
if ('target' in w) return `${w.code}:${w.target}`
|
|
347
|
-
if ('edge' in w) return `${w.code}:${w.edge}`
|
|
348
|
-
if ('node' in w) return `${w.code}:${w.node}`
|
|
349
|
-
if ('group' in w) return `${w.code}:${w.group}`
|
|
350
|
-
if ('a' in w && 'b' in w) return `${w.code}:${w.a}|${w.b}`
|
|
351
|
-
return w.code
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function lintFlowchartGraph(graph: import('../types.ts').MermaidGraph): LayoutWarning[] {
|
|
355
|
-
const warnings: LayoutWarning[] = []
|
|
356
|
-
const firstBySignature = new Map<string, { edge: string; from: string; to: string; label?: string }>()
|
|
357
|
-
graph.edges.forEach((edge, index) => {
|
|
358
|
-
const signature = JSON.stringify({
|
|
359
|
-
source: edge.source,
|
|
360
|
-
target: edge.target,
|
|
361
|
-
label: edge.label ?? '',
|
|
362
|
-
style: edge.style,
|
|
363
|
-
hasArrowStart: edge.hasArrowStart,
|
|
364
|
-
hasArrowEnd: edge.hasArrowEnd,
|
|
365
|
-
startMarker: edge.startMarker ?? 'arrow',
|
|
366
|
-
endMarker: edge.endMarker ?? 'arrow',
|
|
367
|
-
})
|
|
368
|
-
const id = `${edge.source}->${edge.target}#${index}`
|
|
369
|
-
const first = firstBySignature.get(signature)
|
|
370
|
-
if (first) {
|
|
371
|
-
warnings.push({ code: 'DUPLICATE_EDGE', edge: id, duplicateOf: first.edge, from: edge.source, to: edge.target, label: edge.label })
|
|
372
|
-
} else {
|
|
373
|
-
firstBySignature.set(signature, { edge: id, from: edge.source, to: edge.target, label: edge.label })
|
|
374
|
-
}
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
// ISO 5807 (10.3.1.2) / ANSI X3.5 (4.10.2): when a decision has several
|
|
378
|
-
// exits, EACH exit shall be labeled with its condition value. A diamond
|
|
379
|
-
// with two or more out-edges where any branch is unlabeled is ambiguous.
|
|
380
|
-
const outEdges = new Map<string, { labeled: number; unlabeled: Array<string> }>()
|
|
381
|
-
graph.edges.forEach((edge, index) => {
|
|
382
|
-
const node = graph.nodes.get(edge.source)
|
|
383
|
-
if (!node || node.shape !== 'diamond' || edge.source === edge.target) return
|
|
384
|
-
const entry = outEdges.get(edge.source) ?? { labeled: 0, unlabeled: [] }
|
|
385
|
-
if (edge.label && edge.label.trim().length > 0) entry.labeled++
|
|
386
|
-
else entry.unlabeled.push(`${edge.source}->${edge.target}#${index}`)
|
|
387
|
-
outEdges.set(edge.source, entry)
|
|
388
|
-
})
|
|
389
|
-
for (const [nodeId, entry] of outEdges) {
|
|
390
|
-
if (entry.labeled + entry.unlabeled.length < 2) continue
|
|
391
|
-
for (const edge of entry.unlabeled) {
|
|
392
|
-
warnings.push({ code: 'DECISION_BRANCH_UNLABELED', node: nodeId, edge })
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const ids = Array.from(graph.nodes.keys())
|
|
397
|
-
if (ids.length === 0 || graph.edges.length === 0) return warnings
|
|
398
|
-
const incoming = new Map(ids.map(id => [id, 0]))
|
|
399
|
-
const outgoing = new Map(ids.map(id => [id, [] as string[]]))
|
|
400
|
-
for (const edge of graph.edges) {
|
|
401
|
-
if (!graph.nodes.has(edge.source) || !graph.nodes.has(edge.target)) continue
|
|
402
|
-
incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1)
|
|
403
|
-
outgoing.get(edge.source)!.push(edge.target)
|
|
404
|
-
}
|
|
405
|
-
const roots = ids.filter(id => (incoming.get(id) ?? 0) === 0)
|
|
406
|
-
if (roots.length === 0) return warnings
|
|
407
|
-
const seen = new Set<string>(roots)
|
|
408
|
-
const queue = [...roots]
|
|
409
|
-
for (let i = 0; i < queue.length; i++) {
|
|
410
|
-
for (const next of outgoing.get(queue[i]!) ?? []) {
|
|
411
|
-
if (seen.has(next)) continue
|
|
412
|
-
seen.add(next)
|
|
413
|
-
queue.push(next)
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
for (const id of ids) {
|
|
417
|
-
if (!seen.has(id)) warnings.push({ code: 'UNREACHABLE_NODE', node: id })
|
|
418
|
-
}
|
|
419
|
-
return warnings
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function verifyTimeline(d: ValidDiagram & { body: import('./types.ts').TimelineBody }, cap: number, opts: VerifyOptions): VerifyResult {
|
|
423
|
-
const body = d.body
|
|
424
|
-
const layout = layoutFamilyToRendered(d) ?? emptyRenderedLayout(d.kind)
|
|
425
|
-
const warnings: LayoutWarning[] = []
|
|
426
|
-
const hasContent = body.sections.some(s => s.periods.length > 0) || body.title !== undefined
|
|
427
|
-
if (!hasContent) return finalize([{ code: 'EMPTY_DIAGRAM' }], layout, opts)
|
|
428
|
-
const overflow = (target: string, text: string | undefined) => {
|
|
429
|
-
const w = text !== undefined ? labelOverflowWarning(target, text, cap) : null
|
|
430
|
-
if (w) warnings.push(w)
|
|
431
|
-
}
|
|
432
|
-
overflow('title', body.title)
|
|
433
|
-
for (const s of body.sections) {
|
|
434
|
-
overflow(s.id, s.label)
|
|
435
|
-
for (const p of s.periods) {
|
|
436
|
-
overflow(p.id, p.label)
|
|
437
|
-
for (const e of p.events) overflow(e.id, e.text)
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return finalize(dedupedConcat(warnings, layoutGeometryWarnings(layout, { nodeOverlaps: true, groupContainment: true })), layout, opts)
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function verifySequence(d: ValidDiagram & { body: SequenceBody }, cap: number, opts: VerifyOptions): VerifyResult {
|
|
444
|
-
const body = d.body
|
|
445
|
-
const layout = layoutFamilyToRendered(d) ?? emptyRenderedLayout(d.kind)
|
|
446
|
-
const warnings: LayoutWarning[] = []
|
|
447
|
-
// BUILD-18: a segment-preserving body may carry content only in opaque-block
|
|
448
|
-
// segments (e.g. activation-shorthand messages `A->>+B`, blocks). That is
|
|
449
|
-
// not an empty diagram — it just isn't structurally modeled.
|
|
450
|
-
const hasOpaqueContent = (body.statements ?? []).some(
|
|
451
|
-
s => s.kind === 'opaque-block' && s.lines.some(l => l.trim().length > 0),
|
|
452
|
-
)
|
|
453
|
-
if (body.participants.length === 0 && body.messages.length === 0 && !hasOpaqueContent) {
|
|
454
|
-
return finalize([{ code: 'EMPTY_DIAGRAM' }], layout, opts, false)
|
|
455
|
-
}
|
|
456
|
-
const ids = new Set(body.participants.map(p => p.id))
|
|
457
|
-
body.messages.forEach((m, i) => {
|
|
458
|
-
if (!ids.has(m.from) || !ids.has(m.to)) {
|
|
459
|
-
warnings.push({
|
|
460
|
-
code: 'EDGE_MISANCHORED', edge: `msg#${i}:${m.from}->${m.to}`,
|
|
461
|
-
from: ids.has(m.from) ? m.from : undefined, to: ids.has(m.to) ? m.to : undefined,
|
|
462
|
-
})
|
|
463
|
-
}
|
|
464
|
-
const w = labelOverflowWarning(`msg#${i}:${m.from}->${m.to}`, m.text, cap)
|
|
465
|
-
if (w) warnings.push(w)
|
|
466
|
-
})
|
|
467
|
-
for (const p of body.participants) {
|
|
468
|
-
const w = labelOverflowWarning(p.id, p.label, cap)
|
|
469
|
-
if (w) warnings.push(w)
|
|
470
|
-
}
|
|
471
|
-
// BUILD-18: opaque-block segments (Note/alt/loop/par/title lines) still get
|
|
472
|
-
// universal LABEL_OVERFLOW via the family's label extractor, so the safety
|
|
473
|
-
// check survives the move from whole-body-opaque to structured-with-segments.
|
|
474
|
-
const opaqueLines = (body.statements ?? [])
|
|
475
|
-
.filter((s): s is Extract<typeof s, { kind: 'opaque-block' }> => s.kind === 'opaque-block')
|
|
476
|
-
.flatMap(s => s.lines)
|
|
477
|
-
if (opaqueLines.length > 0) {
|
|
478
|
-
const plugin = getFamily(d.kind)
|
|
479
|
-
const labels = (plugin?.extractLabels ?? extractLabelsGeneric)(opaqueLines.join('\n'))
|
|
480
|
-
const seen = new Set<string>()
|
|
481
|
-
for (const lbl of labels) {
|
|
482
|
-
const w = labelOverflowWarning(lbl.target, lbl.text, cap)
|
|
483
|
-
if (!w) continue
|
|
484
|
-
const key = `${lbl.target}:${lbl.text}`
|
|
485
|
-
if (seen.has(key)) continue
|
|
486
|
-
seen.add(key)
|
|
487
|
-
warnings.push(w)
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
// The empty-layout tripwire only arms when there is no structured content:
|
|
491
|
-
// opaque-only segments still have to lay out SOMETHING (a malformed message
|
|
492
|
-
// like `Alice->>` renders a 0x0 canvas — announce it), whereas a sequence
|
|
493
|
-
// WITH participants whose family layout degrades to empty (e.g.
|
|
494
|
-
// semicolon-packed statements) is a renderer limitation, not emptiness.
|
|
495
|
-
const guardEmpty = body.participants.length === 0 && body.messages.length === 0
|
|
496
|
-
return finalize(dedupedConcat(warnings, layoutGeometryWarnings(layout, { nodeOverlaps: true })), layout, opts, guardEmpty)
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// ---- helpers --------------------------------------------------------------
|
|
500
|
-
|
|
501
|
-
function layoutOpaqueFlowchart(d: ValidDiagram): RenderedLayout {
|
|
502
|
-
try {
|
|
503
|
-
return positionedToRenderedLayout(layoutGraphSync(parseFlowchartLegacy(d.canonicalSource), {}), d.kind)
|
|
504
|
-
} catch {
|
|
505
|
-
return emptyRenderedLayout(d.kind)
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function unwrap(source: string): ValidDiagram | null {
|
|
510
|
-
const r = parseValidDiagram(source)
|
|
511
|
-
return r.ok ? r.value : null
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function finalize(warnings: LayoutWarning[], layout: RenderedLayout, opts: VerifyOptions, guardEmptyLayout = true): VerifyResult {
|
|
515
|
-
const suppress = new Set<WarningCode>(opts.suppress ?? [])
|
|
516
|
-
const kept = warnings.filter(w => !suppress.has(w.code))
|
|
517
|
-
const ok = !kept.some(w => WARNING_SEVERITY[w.code] === 'error')
|
|
518
|
-
// Empty-layout tripwire over the FINAL layout: a 0x0 canvas with no
|
|
519
|
-
// nodes/edges/groups renders as visually nothing, so it must never verify
|
|
520
|
-
// clean (e.g. `sequenceDiagram\n Alice->>` — a malformed message that lays
|
|
521
|
-
// out zero participants). Source that carries content the local layout
|
|
522
|
-
// cannot express stays ok (the upstream-suite bench pins that), so the
|
|
523
|
-
// announcement is UNSUPPORTED_SYNTAX — warning severity, consistent with
|
|
524
|
-
// the ok verdict — never an appended EMPTY_DIAGRAM, whose declared severity
|
|
525
|
-
// is error and would contradict ok:true for callers that gate on it. Truly
|
|
526
|
-
// content-less diagrams keep the explicit, ok-flipping EMPTY_DIAGRAM their
|
|
527
|
-
// verify paths already push. Callers whose empty layout means "unmodeled,
|
|
528
|
-
// preserved" rather than "renders nothing" (opaque bodies) opt out via
|
|
529
|
-
// guardEmptyLayout=false.
|
|
530
|
-
if (guardEmptyLayout && !suppress.has('UNSUPPORTED_SYNTAX')
|
|
531
|
-
&& layout.nodes.length === 0 && layout.edges.length === 0 && layout.groups.length === 0
|
|
532
|
-
&& layout.bounds.w === 0 && layout.bounds.h === 0
|
|
533
|
-
&& !kept.some(w => w.code === 'EMPTY_DIAGRAM')
|
|
534
|
-
&& !kept.some(w => w.code === 'UNSUPPORTED_SYNTAX' && w.syntax === 'empty_layout')) {
|
|
535
|
-
return {
|
|
536
|
-
ok,
|
|
537
|
-
warnings: [...kept, { code: 'UNSUPPORTED_SYNTAX', syntax: 'empty_layout', message: 'The source carries content, but the local layout renders nothing (0x0 canvas with no nodes, edges, or groups).' }],
|
|
538
|
-
layout,
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
return { ok, warnings: kept, layout }
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function rectIntersection(a: { x: number; y: number; width: number; height: number }, b: { x: number; y: number; width: number; height: number }): number {
|
|
545
|
-
const xo = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x))
|
|
546
|
-
const yo = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y))
|
|
547
|
-
return xo * yo
|
|
548
|
-
}
|
|
549
|
-
function countSelfCrossings(points: { x: number; y: number }[]): number {
|
|
550
|
-
if (points.length < 4) return 0
|
|
551
|
-
let count = 0
|
|
552
|
-
for (let i = 0; i < points.length - 1; i++)
|
|
553
|
-
for (let j = i + 2; j < points.length - 1; j++) {
|
|
554
|
-
if (i === 0 && j === points.length - 2) continue
|
|
555
|
-
if (segInt(points[i]!, points[i + 1]!, points[j]!, points[j + 1]!)) count++
|
|
556
|
-
}
|
|
557
|
-
return count
|
|
558
|
-
}
|
|
559
|
-
function segInt(p1: { x: number; y: number }, p2: { x: number; y: number }, p3: { x: number; y: number }, p4: { x: number; y: number }): boolean {
|
|
560
|
-
const d1 = cr(p4.x - p3.x, p4.y - p3.y, p1.x - p3.x, p1.y - p3.y)
|
|
561
|
-
const d2 = cr(p4.x - p3.x, p4.y - p3.y, p2.x - p3.x, p2.y - p3.y)
|
|
562
|
-
const d3 = cr(p2.x - p1.x, p2.y - p1.y, p3.x - p1.x, p3.y - p1.y)
|
|
563
|
-
const d4 = cr(p2.x - p1.x, p2.y - p1.y, p4.x - p1.x, p4.y - p1.y)
|
|
564
|
-
return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))
|
|
565
|
-
}
|
|
566
|
-
function cr(x1: number, y1: number, x2: number, y2: number): number { return x1 * y2 - x2 * y1 }
|
|
567
|
-
|
|
568
|
-
function findSubgraphById(list: import('../types.ts').MermaidSubgraph[], id: string): import('../types.ts').MermaidSubgraph | null {
|
|
569
|
-
for (const sg of list) { if (sg.id === id) return sg; const c = findSubgraphById(sg.children, id); if (c) return c }
|
|
570
|
-
return null
|
|
571
|
-
}
|