agentic-mermaid 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +144 -4
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +25 -32
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/agent-core.d.ts +463 -0
- package/dist/agent-core.js +397 -0
- package/dist/agent.d.ts +73 -1868
- package/dist/agent.js +336 -37325
- package/dist/agentic-mermaid-mcp.js +7 -38399
- package/dist/am.js +33632 -40445
- 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 -33014
- 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 +68 -43
- package/server.json +2 -2
- package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
- package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
- package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
- package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
- package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
- package/AGENT_NATIVE.md +0 -607
- package/Instructions_for_agents.md +0 -89
- package/TODO.md +0 -684
- package/assets/hero.png +0 -0
- package/dist/index-wjRjbsP-.d.ts +0 -1102
- 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 -211
- 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 -82
- 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-experience-curve-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
- package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
- package/docs/design/families/journey-migration-parity.md +0 -439
- package/docs/design/families/journey-section-overlap-after.png +0 -0
- package/docs/design/families/journey-section-overlap-before.png +0 -0
- package/docs/design/families/journey-usage-research.md +0 -423
- package/docs/design/families/journey.md +0 -153
- package/docs/design/families/xychart.md +0 -85
- package/docs/design/system/README.md +0 -45
- package/docs/design/system/abstraction-audit.md +0 -224
- package/docs/design/system/abstraction-recommendations.md +0 -655
- package/docs/design/system/architecture.mmd +0 -29
- package/docs/design/system/architecture.svg +0 -137
- package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
- package/docs/design/system/layout-rubric.md +0 -138
- package/docs/design/system/route-contracts.md +0 -848
- package/docs/design/system/source-preservation-ladder.md +0 -52
- package/docs/design/system/styles-rollout.md +0 -216
- package/docs/design/system/ugly-layouts.md +0 -119
- package/docs/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 -498
- package/docs/layout-characterization/contact-sheet.md +0 -638
- package/docs/layout-characterization/properties.md +0 -281
- package/docs/layout-characterization/visual-quality.md +0 -160
- package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
- package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
- package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
- package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
- package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/journey.svg +0 -98
- package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
- package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
- package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
- package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
- package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
- package/docs/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 -215
- 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 -368
- package/src/agent/er-body.ts +0 -224
- package/src/agent/facts.ts +0 -360
- package/src/agent/families-builtin.ts +0 -920
- package/src/agent/families.ts +0 -232
- package/src/agent/family-layouts.ts +0 -832
- package/src/agent/flowchart-body.ts +0 -372
- package/src/agent/flowchart-unsupported.ts +0 -243
- package/src/agent/font-coverage.ts +0 -288
- package/src/agent/gantt-body.ts +0 -495
- package/src/agent/index.ts +0 -10
- package/src/agent/journey-body.ts +0 -374
- package/src/agent/label-metrics.ts +0 -28
- package/src/agent/layout-to-rendered.ts +0 -63
- package/src/agent/mutate.ts +0 -97
- package/src/agent/mutation-ops.ts +0 -51
- package/src/agent/op-schema.ts +0 -443
- package/src/agent/parse.ts +0 -223
- package/src/agent/pie-body.ts +0 -270
- package/src/agent/png.ts +0 -171
- package/src/agent/quadrant-body.ts +0 -336
- package/src/agent/quality.ts +0 -363
- package/src/agent/readability-audit.ts +0 -92
- package/src/agent/rendered-route-audit.ts +0 -61
- package/src/agent/sequence-body.ts +0 -345
- package/src/agent/serialize.ts +0 -214
- package/src/agent/state-body.ts +0 -589
- package/src/agent/structural-count.ts +0 -150
- package/src/agent/timeline-body.ts +0 -280
- package/src/agent/trace-log.ts +0 -43
- package/src/agent/types.ts +0 -995
- package/src/agent/verify.ts +0 -603
- package/src/agent/xychart-body.ts +0 -472
- package/src/architecture/config.ts +0 -282
- package/src/architecture/layout.ts +0 -496
- package/src/architecture/parser.ts +0 -375
- package/src/architecture/renderer.ts +0 -611
- package/src/architecture/types.ts +0 -113
- package/src/ascii/ansi.ts +0 -423
- package/src/ascii/architecture.ts +0 -210
- package/src/ascii/canvas.ts +0 -443
- package/src/ascii/class-diagram.ts +0 -697
- package/src/ascii/converter.ts +0 -475
- package/src/ascii/draw.ts +0 -1693
- package/src/ascii/edge-bundling.ts +0 -328
- package/src/ascii/edge-routing.ts +0 -330
- package/src/ascii/er-diagram.ts +0 -439
- package/src/ascii/gantt.ts +0 -276
- package/src/ascii/grid.ts +0 -747
- package/src/ascii/index.ts +0 -196
- package/src/ascii/journey.ts +0 -169
- package/src/ascii/meta.ts +0 -244
- package/src/ascii/multiline-utils.ts +0 -80
- package/src/ascii/pathfinder.ts +0 -301
- package/src/ascii/pie.ts +0 -67
- package/src/ascii/quadrant.ts +0 -158
- package/src/ascii/reverse.ts +0 -216
- package/src/ascii/sequence.ts +0 -499
- package/src/ascii/shapes/circle.ts +0 -27
- package/src/ascii/shapes/corners.ts +0 -143
- package/src/ascii/shapes/diamond.ts +0 -27
- package/src/ascii/shapes/hexagon.ts +0 -27
- package/src/ascii/shapes/index.ts +0 -106
- package/src/ascii/shapes/rectangle.ts +0 -168
- package/src/ascii/shapes/rounded.ts +0 -27
- package/src/ascii/shapes/special.ts +0 -332
- package/src/ascii/shapes/stadium.ts +0 -110
- package/src/ascii/shapes/state.ts +0 -192
- package/src/ascii/shapes/types.ts +0 -73
- package/src/ascii/timeline.ts +0 -141
- package/src/ascii/types.ts +0 -312
- package/src/ascii/validate.ts +0 -120
- package/src/ascii/width.ts +0 -81
- package/src/ascii/wrap.ts +0 -131
- package/src/ascii/xychart.ts +0 -919
- package/src/browser.ts +0 -29
- package/src/class/layout.ts +0 -240
- package/src/class/parser.ts +0 -332
- package/src/class/renderer.ts +0 -638
- package/src/class/types.ts +0 -129
- package/src/cli/agent-instructions.ts +0 -1
- package/src/cli/am-bin.ts +0 -6
- package/src/cli/exit-codes.ts +0 -18
- package/src/cli/index.ts +0 -1132
- package/src/cli/init-agent.ts +0 -147
- package/src/cli/run-entrypoint.ts +0 -10
- package/src/color-resolver.ts +0 -152
- package/src/elk-instance.ts +0 -114
- package/src/er/layout.ts +0 -187
- package/src/er/parser.ts +0 -226
- package/src/er/renderer.ts +0 -792
- package/src/er/types.ts +0 -99
- package/src/family-rubric.ts +0 -425
- package/src/font-manifest.ts +0 -31
- package/src/gantt/layout.ts +0 -365
- package/src/gantt/parser.ts +0 -282
- package/src/gantt/pipeline.ts +0 -27
- package/src/gantt/renderer.ts +0 -414
- package/src/gantt/schedule.ts +0 -513
- package/src/gantt/types.ts +0 -255
- package/src/index.ts +0 -368
- package/src/journey/layout.ts +0 -554
- package/src/journey/parse-core.ts +0 -251
- package/src/journey/parser.ts +0 -101
- package/src/journey/renderer.ts +0 -917
- package/src/journey/types.ts +0 -144
- package/src/layout/geometry.ts +0 -182
- package/src/layout/pass.ts +0 -142
- package/src/layout/passes/index.ts +0 -3916
- package/src/layout-engine.ts +0 -1565
- package/src/layout-rubric.ts +0 -659
- package/src/mcp/artifacts.ts +0 -137
- package/src/mcp/dynamic-harness.ts +0 -31
- package/src/mcp/dynamic-modules.d.ts +0 -7
- package/src/mcp/facade.ts +0 -608
- package/src/mcp/harness-runtime.ts +0 -203
- package/src/mcp/hosted-server.ts +0 -468
- package/src/mcp/mcp-bin.ts +0 -3
- package/src/mcp/mcp-cli.ts +0 -82
- package/src/mcp/protocol.ts +0 -13
- package/src/mcp/sandbox.ts +0 -195
- package/src/mcp/sdk-decl.ts +0 -391
- package/src/mcp/server.ts +0 -362
- package/src/mcp/tool-surface.ts +0 -146
- package/src/mermaid-source.ts +0 -655
- package/src/multiline-utils.ts +0 -228
- package/src/parser.ts +0 -1031
- package/src/pie/layout.ts +0 -168
- package/src/pie/parser.ts +0 -136
- package/src/pie/renderer.ts +0 -234
- package/src/pie/types.ts +0 -82
- package/src/quadrant/layout.ts +0 -191
- package/src/quadrant/parser.ts +0 -183
- package/src/quadrant/renderer.ts +0 -322
- package/src/quadrant/types.ts +0 -113
- package/src/render-family-hooks.ts +0 -244
- package/src/renderer.ts +0 -1099
- package/src/route-contracts.ts +0 -2723
- package/src/scene/backend.ts +0 -76
- package/src/scene/fidelity.ts +0 -183
- package/src/scene/hybrid-backend.ts +0 -174
- package/src/scene/ir.ts +0 -181
- package/src/scene/marks.ts +0 -127
- package/src/scene/rough-backend.ts +0 -378
- package/src/scene/seed.ts +0 -46
- package/src/scene/style-registry.ts +0 -556
- package/src/sequence/layout.ts +0 -456
- package/src/sequence/parser.ts +0 -255
- package/src/sequence/renderer.ts +0 -604
- package/src/sequence/types.ts +0 -154
- package/src/shape-clipping.ts +0 -351
- package/src/shared/accessibility-directives.ts +0 -22
- package/src/shared/batched.ts +0 -61
- package/src/shared/color-math.ts +0 -93
- package/src/shared/svg-a11y.ts +0 -29
- package/src/shared/syntax-error.ts +0 -17
- package/src/shared/unicode-ranges.ts +0 -135
- package/src/styles.ts +0 -318
- package/src/svg-paths.ts +0 -28
- package/src/text-metrics.ts +0 -248
- package/src/theme.ts +0 -710
- package/src/timeline/layout.ts +0 -381
- package/src/timeline/parser.ts +0 -215
- package/src/timeline/renderer.ts +0 -538
- package/src/timeline/types.ts +0 -105
- package/src/types.ts +0 -462
- package/src/website-worker-modules.d.ts +0 -18
- package/src/xychart/axis-utils.ts +0 -109
- package/src/xychart/colors.ts +0 -124
- package/src/xychart/config.ts +0 -88
- package/src/xychart/layout.ts +0 -656
- package/src/xychart/parser.ts +0 -504
- package/src/xychart/renderer.ts +0 -640
- package/src/xychart/types.ts +0 -282
package/src/agent/op-schema.ts
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// op-schema: shape validation for every family's structured mutation ops.
|
|
3
|
-
//
|
|
4
|
-
// The typed `mutate` surface is guaranteed by the compiler — but the MCP and
|
|
5
|
-
// CLI boundaries hand it ops as untyped JSON, where those guarantees evaporate:
|
|
6
|
-
// a model that writes `{kind:'add_class', name:'Duck'}` (using `name` where
|
|
7
|
-
// `id` is expected) slips a shapeless object past the type system, and the
|
|
8
|
-
// mutator silently produces `class undefined`. This layer restores the missing
|
|
9
|
-
// check at the one place untyped ops arrive: it validates op SHAPE (field
|
|
10
|
-
// presence, primitive type, enum membership) BEFORE the mutator runs, and
|
|
11
|
-
// rejects a malformed op with a PRESCRIPTIVE INVALID_OP error that names the
|
|
12
|
-
// offending field and lists the valid ones (with a nearest-match suggestion
|
|
13
|
-
// for an obvious typo).
|
|
14
|
-
//
|
|
15
|
-
// Scope: shape only. Semantic checks — does the referenced class exist, is the
|
|
16
|
-
// id already taken, is the index in range — stay in the existing mutators. The
|
|
17
|
-
// two layers compose: shape first (here), semantics second (the mutator). The
|
|
18
|
-
// field lists below are transcribed from the *MutationOp union types in
|
|
19
|
-
// types.ts, so a valid typed op always passes; the whole unit suite exercising
|
|
20
|
-
// the mutators through the checked core is the proof (see apply.ts).
|
|
21
|
-
// ============================================================================
|
|
22
|
-
|
|
23
|
-
import type { NodeShape, EdgeStyle } from '../types.ts'
|
|
24
|
-
import type {
|
|
25
|
-
SequenceMessageStyle, ClassRelationKind, ErCardinality, ArchitectureSide, GanttBodyTaskTag,
|
|
26
|
-
} from './types.ts'
|
|
27
|
-
import { unknownOpMessage } from './mutation-ops.ts'
|
|
28
|
-
import type { MutableFamilyId } from './mutation-ops.ts'
|
|
29
|
-
|
|
30
|
-
export type OpFamily = MutableFamilyId
|
|
31
|
-
|
|
32
|
-
/** Primitive kinds a field may hold. `enum` carries an explicit value list;
|
|
33
|
-
* `nullable` lets an enum/primitive also accept `null` (e.g. set_task_status). */
|
|
34
|
-
type FieldType =
|
|
35
|
-
| { type: 'string' }
|
|
36
|
-
| { type: 'number' }
|
|
37
|
-
| { type: 'boolean' }
|
|
38
|
-
| { type: 'string-or-null' }
|
|
39
|
-
| { type: 'enum'; values: readonly string[]; nullable?: boolean }
|
|
40
|
-
| { type: 'string-array' }
|
|
41
|
-
| { type: 'number-array' }
|
|
42
|
-
| { type: 'object-or-null' } // a nested record (deep shape validated by the mutator)
|
|
43
|
-
|
|
44
|
-
/** A field type plus its required flag. Intersection (not `extends`) because
|
|
45
|
-
* FieldType is a union — an interface cannot extend a union. */
|
|
46
|
-
type FieldSpec = FieldType & {
|
|
47
|
-
/** required = must be present and non-undefined. Optional fields may be omitted. */
|
|
48
|
-
required: boolean
|
|
49
|
-
/** Optional human note surfaced in discovery: a value constraint the MUTATOR
|
|
50
|
-
* (not this shape layer) enforces (e.g. "1..5"), or the default applied when
|
|
51
|
-
* an optional field is omitted (e.g. "default: rectangle"). Descriptive only
|
|
52
|
-
* — the mutator remains the single enforcement point. */
|
|
53
|
-
note?: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface OpSpec {
|
|
57
|
-
/** field-name -> spec. `kind` is implicit and never listed. */
|
|
58
|
-
fields: Record<string, FieldSpec>
|
|
59
|
-
/** Optional cross-field rule: at least one of these must be present (e.g.
|
|
60
|
-
* architecture remove_edge accepts `index` OR `id`). */
|
|
61
|
-
requireOneOf?: readonly string[]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Enum value lists, kept in sync with the type aliases they mirror.
|
|
65
|
-
const NODE_SHAPES: readonly NodeShape[] = [
|
|
66
|
-
'rectangle', 'service', 'rounded', 'diamond', 'stadium', 'circle', 'subroutine',
|
|
67
|
-
'doublecircle', 'hexagon', 'cylinder', 'asymmetric', 'trapezoid', 'trapezoid-alt',
|
|
68
|
-
'lean-r', 'lean-l', 'state-start', 'state-end',
|
|
69
|
-
]
|
|
70
|
-
const EDGE_STYLES: readonly EdgeStyle[] = ['solid', 'dotted', 'thick', 'invisible']
|
|
71
|
-
const SEQUENCE_MESSAGE_STYLES: readonly SequenceMessageStyle[] = ['sync', 'reply', 'async', 'async-dashed', 'lost', 'lost-dashed']
|
|
72
|
-
const PARTICIPANT_KINDS = ['participant', 'actor'] as const
|
|
73
|
-
const CLASS_REL_KINDS: readonly ClassRelationKind[] = ['inheritance', 'composition', 'aggregation', 'association', 'dependency', 'realization', 'link-solid', 'link-dashed']
|
|
74
|
-
const ER_CARDINALITIES: readonly ErCardinality[] = ['one-only', 'zero-or-one', 'zero-or-many', 'one-or-many']
|
|
75
|
-
const ARCH_SIDES: readonly ArchitectureSide[] = ['L', 'R', 'T', 'B']
|
|
76
|
-
const SERIES_KINDS = ['bar', 'line'] as const
|
|
77
|
-
const AXIS_KINDS = ['x', 'y'] as const
|
|
78
|
-
const GANTT_STATUSES = ['active', 'done', 'crit'] as const
|
|
79
|
-
const _GANTT_TAGS: readonly GanttBodyTaskTag[] = ['active', 'done', 'crit', 'milestone', 'vert'] // tags are shape-checked as string[]; deep enum check stays in the mutator
|
|
80
|
-
|
|
81
|
-
// Field-spec shorthands.
|
|
82
|
-
const str = (required = true): FieldSpec => ({ type: 'string', required })
|
|
83
|
-
const num = (required = true): FieldSpec => ({ type: 'number', required })
|
|
84
|
-
const bool = (required = true): FieldSpec => ({ type: 'boolean', required })
|
|
85
|
-
const strOrNull = (required = true): FieldSpec => ({ type: 'string-or-null', required })
|
|
86
|
-
const strArr = (required = true): FieldSpec => ({ type: 'string-array', required })
|
|
87
|
-
const numArr = (required = true): FieldSpec => ({ type: 'number-array', required })
|
|
88
|
-
const objOrNull = (required = true): FieldSpec => ({ type: 'object-or-null', required })
|
|
89
|
-
const oneOf = (values: readonly string[], required = true, nullable = false): FieldSpec => ({ type: 'enum', values, nullable, required })
|
|
90
|
-
/** Attach a discovery note (mutator-enforced constraint or omit-default) to a
|
|
91
|
-
* field spec without changing its shape validation. */
|
|
92
|
-
const withNote = (spec: FieldSpec, note: string): FieldSpec => ({ ...spec, note })
|
|
93
|
-
|
|
94
|
-
// ---- Per-op schemas (transcribed from the *MutationOp unions in types.ts) ---
|
|
95
|
-
|
|
96
|
-
const FLOWCHART_SCHEMA: Record<string, OpSpec> = {
|
|
97
|
-
add_node: { fields: { id: str(), label: str(), shape: withNote(oneOf(NODE_SHAPES, false), 'default: rectangle'), parent: str(false) } },
|
|
98
|
-
remove_node: { fields: { id: str() } },
|
|
99
|
-
rename_node: { fields: { from: str(), to: str() } },
|
|
100
|
-
set_label: { fields: { target: str(), label: str() } },
|
|
101
|
-
add_edge: { fields: { from: str(), to: str(), label: str(false), style: withNote(oneOf(EDGE_STYLES, false), 'default: solid') } },
|
|
102
|
-
remove_edge: { fields: { id: str() } },
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const STATE_SCHEMA: Record<string, OpSpec> = {
|
|
106
|
-
add_state: { fields: { id: str(), label: strOrNull(false), parent: strOrNull(false) } },
|
|
107
|
-
remove_state: { fields: { id: str() } },
|
|
108
|
-
rename_state: { fields: { from: str(), to: str() } },
|
|
109
|
-
set_state_label: { fields: { id: str(), label: strOrNull() } },
|
|
110
|
-
add_transition: { fields: { from: str(), to: str(), label: strOrNull(false), parent: strOrNull(false) } },
|
|
111
|
-
remove_transition: { fields: { index: num(false), from: str(false), to: str(false), parent: strOrNull(false) }, requireOneOf: ['index', 'from', 'to'] },
|
|
112
|
-
set_transition_label: { fields: { index: num(false), from: str(false), to: str(false), label: strOrNull(), parent: strOrNull(false) }, requireOneOf: ['index', 'from', 'to'] },
|
|
113
|
-
make_composite: { fields: { id: str(), members: strArr(), label: strOrNull(false) } },
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const SEQUENCE_SCHEMA: Record<string, OpSpec> = {
|
|
117
|
-
add_participant: { fields: { id: str(), label: str(false), participantKind: oneOf(PARTICIPANT_KINDS, false) } },
|
|
118
|
-
remove_participant: { fields: { id: str() } },
|
|
119
|
-
add_message: { fields: { from: str(), to: str(), text: str(), style: withNote(oneOf(SEQUENCE_MESSAGE_STYLES, false), 'default: sync') } },
|
|
120
|
-
remove_message: { fields: { index: num() } },
|
|
121
|
-
set_message_text: { fields: { index: num(), text: str() } },
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const TIMELINE_SCHEMA: Record<string, OpSpec> = {
|
|
125
|
-
set_title: { fields: { title: strOrNull() } },
|
|
126
|
-
add_section: { fields: { label: str() } },
|
|
127
|
-
remove_section: { fields: { index: num() } },
|
|
128
|
-
set_section_label: { fields: { index: num(), label: str() } },
|
|
129
|
-
add_period: { fields: { sectionIndex: num(), label: str(), events: strArr(false) } },
|
|
130
|
-
remove_period: { fields: { sectionIndex: num(), periodIndex: num() } },
|
|
131
|
-
set_period_label: { fields: { sectionIndex: num(), periodIndex: num(), label: str() } },
|
|
132
|
-
add_event: { fields: { sectionIndex: num(), periodIndex: num(), text: str() } },
|
|
133
|
-
remove_event: { fields: { sectionIndex: num(), periodIndex: num(), eventIndex: num() } },
|
|
134
|
-
set_event_text: { fields: { sectionIndex: num(), periodIndex: num(), eventIndex: num(), text: str() } },
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const CLASS_SCHEMA: Record<string, OpSpec> = {
|
|
138
|
-
set_title: { fields: { title: strOrNull() } },
|
|
139
|
-
add_class: { fields: { id: str(), label: str(false), members: strArr(false) } },
|
|
140
|
-
remove_class: { fields: { id: str() } },
|
|
141
|
-
rename_class: { fields: { from: str(), to: str() } },
|
|
142
|
-
add_member: { fields: { class: str(), text: str() } },
|
|
143
|
-
remove_member: { fields: { class: str(), index: num() } },
|
|
144
|
-
add_relation: { fields: { from: str(), to: str(), relKind: oneOf(CLASS_REL_KINDS), label: str(false) } },
|
|
145
|
-
remove_relation: { fields: { index: num() } },
|
|
146
|
-
add_note: { fields: { text: str(), for: str(false) } },
|
|
147
|
-
remove_note: { fields: { index: num() } },
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const ER_SCHEMA: Record<string, OpSpec> = {
|
|
151
|
-
add_entity: { fields: { id: str(), attributes: strArr(false) } },
|
|
152
|
-
remove_entity: { fields: { id: str() } },
|
|
153
|
-
rename_entity: { fields: { from: str(), to: str() } },
|
|
154
|
-
add_attribute: { fields: { entity: str(), text: str() } },
|
|
155
|
-
remove_attribute: { fields: { entity: str(), index: num() } },
|
|
156
|
-
add_relation: { fields: { from: str(), to: str(), leftCard: oneOf(ER_CARDINALITIES), rightCard: oneOf(ER_CARDINALITIES), dashed: bool(false), label: str(false) } },
|
|
157
|
-
remove_relation: { fields: { index: num() } },
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const JOURNEY_SCHEMA: Record<string, OpSpec> = {
|
|
161
|
-
set_title: { fields: { title: strOrNull() } },
|
|
162
|
-
add_section: { fields: { label: str(), index: withNote(num(false), 'insert position; omit to append') } },
|
|
163
|
-
remove_section: { fields: { index: num() } },
|
|
164
|
-
set_section_label: { fields: { index: num(), label: str() } },
|
|
165
|
-
add_task: { fields: { sectionIndex: num(), text: str(), score: withNote(num(), 'integer 1..5'), actors: strArr(false), index: withNote(num(false), 'insert position; omit to append') } },
|
|
166
|
-
remove_task: { fields: { sectionIndex: num(), taskIndex: num() } },
|
|
167
|
-
set_task_text: { fields: { sectionIndex: num(), taskIndex: num(), text: str() } },
|
|
168
|
-
set_task_score: { fields: { sectionIndex: num(), taskIndex: num(), score: withNote(num(), 'integer 1..5') } },
|
|
169
|
-
set_task_actors: { fields: { sectionIndex: num(), taskIndex: num(), actors: strArr() } },
|
|
170
|
-
rename_actor: { fields: { from: str(), to: str() } },
|
|
171
|
-
move_task: { fields: { fromSection: num(), fromIndex: num(), toSection: num(), toIndex: withNote(num(), 'insert position in the target section, applied after removal') } },
|
|
172
|
-
move_section: { fields: { from: num(), to: num() } },
|
|
173
|
-
set_accessibility_title: { fields: { title: strOrNull() } },
|
|
174
|
-
set_accessibility_description: { fields: { description: strOrNull() } },
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const ARCHITECTURE_SCHEMA: Record<string, OpSpec> = {
|
|
178
|
-
add_service: { fields: { id: str(), label: str(false), icon: strOrNull(false), group: strOrNull(false) } },
|
|
179
|
-
remove_service: { fields: { id: str() } },
|
|
180
|
-
rename_service: { fields: { from: str(), to: str() } },
|
|
181
|
-
set_service_label: { fields: { id: str(), label: str() } },
|
|
182
|
-
set_service_icon: { fields: { id: str(), icon: strOrNull() } },
|
|
183
|
-
move_service: { fields: { id: str(), group: strOrNull() } },
|
|
184
|
-
add_group: { fields: { id: str(), label: str(false), icon: strOrNull(false), parent: strOrNull(false) } },
|
|
185
|
-
remove_group: { fields: { id: str() } },
|
|
186
|
-
add_edge: { fields: { from: str(), to: str(), fromSide: oneOf(ARCH_SIDES), toSide: oneOf(ARCH_SIDES), label: strOrNull(false), hasArrowStart: bool(false), hasArrowEnd: bool(false) } },
|
|
187
|
-
remove_edge: { fields: { index: num(false), id: str(false) }, requireOneOf: ['index', 'id'] },
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const XYCHART_SCHEMA: Record<string, OpSpec> = {
|
|
191
|
-
set_title: { fields: { title: strOrNull() } },
|
|
192
|
-
set_x_axis: { fields: { axis: objOrNull() } },
|
|
193
|
-
set_y_axis: { fields: { axis: objOrNull() } },
|
|
194
|
-
add_series: { fields: { kind2: withNote(oneOf(SERIES_KINDS), 'series type — the field is named kind2, not kind'), name: strOrNull(false), values: numArr() } },
|
|
195
|
-
remove_series: { fields: { index: num() } },
|
|
196
|
-
set_series_values: { fields: { index: num(), values: numArr() } },
|
|
197
|
-
set_series_name: { fields: { index: num(), name: strOrNull() } },
|
|
198
|
-
reorder_series: { fields: { from: num(), to: num() } },
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const PIE_SCHEMA: Record<string, OpSpec> = {
|
|
202
|
-
set_title: { fields: { title: strOrNull() } },
|
|
203
|
-
set_show_data: { fields: { showData: bool() } },
|
|
204
|
-
add_slice: { fields: { label: str(), value: withNote(num(), '> 0, finite') } },
|
|
205
|
-
remove_slice: { fields: { label: str() } },
|
|
206
|
-
rename_slice: { fields: { from: str(), to: str() } },
|
|
207
|
-
set_slice_value: { fields: { label: str(), value: withNote(num(), '> 0, finite') } },
|
|
208
|
-
reorder_slice: { fields: { from: num(), to: num() } },
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const QUADRANT_SCHEMA: Record<string, OpSpec> = {
|
|
212
|
-
set_title: { fields: { title: strOrNull() } },
|
|
213
|
-
set_axis_labels: { fields: { axis: oneOf(AXIS_KINDS), near: strOrNull(), far: strOrNull(false) } },
|
|
214
|
-
set_quadrant_label: { fields: { quadrant: num(), label: strOrNull() } },
|
|
215
|
-
add_point: { fields: { label: str(), x: withNote(num(), '0..1'), y: withNote(num(), '0..1') } },
|
|
216
|
-
remove_point: { fields: { label: str() } },
|
|
217
|
-
move_point: { fields: { label: str(), x: withNote(num(), '0..1'), y: withNote(num(), '0..1') } },
|
|
218
|
-
rename_point: { fields: { from: str(), to: str() } },
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const GANTT_SCHEMA: Record<string, OpSpec> = {
|
|
222
|
-
set_title: { fields: { title: strOrNull() } },
|
|
223
|
-
add_section: { fields: { label: str() } },
|
|
224
|
-
rename_section: { fields: { index: num(), label: str() } },
|
|
225
|
-
remove_section: { fields: { index: num() } },
|
|
226
|
-
add_task: { fields: { sectionIndex: num(), label: str(), taskId: str(false), tags: strArr(false), start: withNote(str(false), 'date, or "after <taskId>"'), end: withNote(str(), 'date, a duration like "3d", or "until <taskId>"') } },
|
|
227
|
-
remove_task: { fields: { sectionIndex: num(), taskIndex: num() } },
|
|
228
|
-
rename_task: { fields: { sectionIndex: num(), taskIndex: num(), label: str() } },
|
|
229
|
-
set_task_status: { fields: { sectionIndex: num(), taskIndex: num(), status: oneOf(GANTT_STATUSES, true, true) } },
|
|
230
|
-
set_task_dates: { fields: { sectionIndex: num(), taskIndex: num(), start: strOrNull(false), end: str(false) } },
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const SCHEMAS: Record<OpFamily, Record<string, OpSpec>> = {
|
|
234
|
-
flowchart: FLOWCHART_SCHEMA,
|
|
235
|
-
state: STATE_SCHEMA,
|
|
236
|
-
sequence: SEQUENCE_SCHEMA,
|
|
237
|
-
timeline: TIMELINE_SCHEMA,
|
|
238
|
-
class: CLASS_SCHEMA,
|
|
239
|
-
er: ER_SCHEMA,
|
|
240
|
-
journey: JOURNEY_SCHEMA,
|
|
241
|
-
architecture: ARCHITECTURE_SCHEMA,
|
|
242
|
-
xychart: XYCHART_SCHEMA,
|
|
243
|
-
pie: PIE_SCHEMA,
|
|
244
|
-
quadrant: QUADRANT_SCHEMA,
|
|
245
|
-
gantt: GANTT_SCHEMA,
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** True when `family` has a shape schema (i.e. is a mutable structured family). */
|
|
249
|
-
export function hasOpSchema(family: string): family is OpFamily {
|
|
250
|
-
return family in SCHEMAS
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ---- Prescriptive error ---------------------------------------------------
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Structured, model-actionable validation error. `code` is always INVALID_OP
|
|
257
|
-
* (so it flows through the existing MutationError channel unchanged), but the
|
|
258
|
-
* payload names the exact problem and enumerates the legal fields so a weak
|
|
259
|
-
* model can self-correct in one turn.
|
|
260
|
-
*/
|
|
261
|
-
export interface OpValidationError {
|
|
262
|
-
code: 'INVALID_OP'
|
|
263
|
-
/** 'expected_single_op' | 'unknown_kind' | 'unknown_field' | 'missing_field' | 'wrong_type' | 'require_one_of' */
|
|
264
|
-
reason: string
|
|
265
|
-
message: string
|
|
266
|
-
family: OpFamily
|
|
267
|
-
opKind?: string
|
|
268
|
-
/** the offending field, when applicable */
|
|
269
|
-
field?: string
|
|
270
|
-
/** valid field names for this op (or valid op kinds, for unknown_kind) */
|
|
271
|
-
validFields?: string[]
|
|
272
|
-
/** nearest valid name to a typo'd field/kind, when an obvious match exists */
|
|
273
|
-
didYouMean?: string
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function levenshtein(a: string, b: string): number {
|
|
277
|
-
const m = a.length, n = b.length
|
|
278
|
-
const d = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)])
|
|
279
|
-
for (let j = 0; j <= n; j++) d[0]![j] = j
|
|
280
|
-
for (let i = 1; i <= m; i++)
|
|
281
|
-
for (let j = 1; j <= n; j++)
|
|
282
|
-
d[i]![j] = Math.min(d[i - 1]![j]! + 1, d[i]![j - 1]! + 1, d[i - 1]![j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1))
|
|
283
|
-
return d[m]![n]!
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Nearest candidate within an edit-distance threshold (typo tolerance). */
|
|
287
|
-
function nearest(target: string, candidates: string[]): string | undefined {
|
|
288
|
-
let best: string | undefined
|
|
289
|
-
let bestD = Infinity
|
|
290
|
-
for (const c of candidates) {
|
|
291
|
-
const dist = levenshtein(target.toLowerCase(), c.toLowerCase())
|
|
292
|
-
if (dist < bestD) { bestD = dist; best = c }
|
|
293
|
-
}
|
|
294
|
-
// Only suggest when the match is close (<= ~40% of the longer string).
|
|
295
|
-
return best !== undefined && bestD <= Math.max(2, Math.ceil(best.length * 0.4)) ? best : undefined
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function typeName(f: FieldType): string {
|
|
299
|
-
switch (f.type) {
|
|
300
|
-
case 'string': return 'string'
|
|
301
|
-
case 'number': return 'number'
|
|
302
|
-
case 'boolean': return 'boolean'
|
|
303
|
-
case 'string-or-null': return 'string | null'
|
|
304
|
-
case 'string-array': return 'string[]'
|
|
305
|
-
case 'number-array': return 'number[]'
|
|
306
|
-
case 'object-or-null': return 'object | null'
|
|
307
|
-
case 'enum': return `one of ${f.values.map(v => JSON.stringify(v)).join(', ')}${f.nullable ? ' | null' : ''}`
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function matchesType(spec: FieldType, value: unknown): boolean {
|
|
312
|
-
switch (spec.type) {
|
|
313
|
-
case 'string': return typeof value === 'string'
|
|
314
|
-
case 'number': return typeof value === 'number' && Number.isFinite(value)
|
|
315
|
-
case 'boolean': return typeof value === 'boolean'
|
|
316
|
-
case 'string-or-null': return value === null || typeof value === 'string'
|
|
317
|
-
case 'string-array': return Array.isArray(value) && value.every(v => typeof v === 'string')
|
|
318
|
-
case 'number-array': return Array.isArray(value) && value.every(v => typeof v === 'number' && Number.isFinite(v))
|
|
319
|
-
case 'object-or-null': return value === null || (typeof value === 'object' && !Array.isArray(value))
|
|
320
|
-
case 'enum': return (spec.nullable === true && value === null) || (typeof value === 'string' && spec.values.includes(value))
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Validate one op's SHAPE against its family schema. Returns null when valid.
|
|
326
|
-
* The one and only shape choke point: every untyped edit path (declarative
|
|
327
|
-
* applyOps / buildChecked and the Code Mode facade) calls this — through
|
|
328
|
-
* mutateChecked — before touching a mutator.
|
|
329
|
-
*/
|
|
330
|
-
export function validateOp(family: OpFamily, op: unknown): OpValidationError | null {
|
|
331
|
-
const schema = SCHEMAS[family]
|
|
332
|
-
// An op array is the common "apply these ops" slip — give it a dedicated
|
|
333
|
-
// reason so a caller can branch on it, and a message that names the batch fix.
|
|
334
|
-
if (Array.isArray(op)) {
|
|
335
|
-
return { code: 'INVALID_OP', reason: 'expected_single_op', family, message: unknownOpMessage(family, op), validFields: Object.keys(schema) }
|
|
336
|
-
}
|
|
337
|
-
if (!op || typeof op !== 'object') {
|
|
338
|
-
return { code: 'INVALID_OP', reason: 'unknown_kind', family, message: unknownOpMessage(family, op), validFields: Object.keys(schema) }
|
|
339
|
-
}
|
|
340
|
-
const rec = op as Record<string, unknown>
|
|
341
|
-
const kind = rec.kind
|
|
342
|
-
if (typeof kind !== 'string' || !(kind in schema)) {
|
|
343
|
-
const validKinds = Object.keys(schema)
|
|
344
|
-
const dym = typeof kind === 'string' ? nearest(kind, validKinds) : undefined
|
|
345
|
-
// Reuse the mutator's own prescriptive phrasing ("valid ops: …") so the
|
|
346
|
-
// message is identical no matter which layer catches the unknown kind; add
|
|
347
|
-
// a typo suggestion on top.
|
|
348
|
-
return {
|
|
349
|
-
code: 'INVALID_OP', reason: 'unknown_kind', family, opKind: typeof kind === 'string' ? kind : undefined,
|
|
350
|
-
message: `${unknownOpMessage(family, op)}${dym ? ` Did you mean "${dym}"?` : ''}`,
|
|
351
|
-
validFields: validKinds, didYouMean: dym,
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const spec = schema[kind]!
|
|
356
|
-
const validFields = Object.keys(spec.fields)
|
|
357
|
-
|
|
358
|
-
// 1) Unknown / misnamed fields (this catches the `name`-instead-of-`id` bug).
|
|
359
|
-
for (const key of Object.keys(rec)) {
|
|
360
|
-
if (key === 'kind') continue
|
|
361
|
-
if (!(key in spec.fields)) {
|
|
362
|
-
const dym = nearest(key, validFields)
|
|
363
|
-
return {
|
|
364
|
-
code: 'INVALID_OP', reason: 'unknown_field', family, opKind: kind, field: key,
|
|
365
|
-
message: `Unknown field "${key}" for ${family} op "${kind}".${dym ? ` Did you mean "${dym}"?` : ''} Valid fields: ${validFields.join(', ')}.`,
|
|
366
|
-
validFields, didYouMean: dym,
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// 2) Missing required fields.
|
|
372
|
-
for (const [name, fspec] of Object.entries(spec.fields)) {
|
|
373
|
-
if (!fspec.required) continue
|
|
374
|
-
if (rec[name] === undefined) {
|
|
375
|
-
return {
|
|
376
|
-
code: 'INVALID_OP', reason: 'missing_field', family, opKind: kind, field: name,
|
|
377
|
-
message: `Missing required field "${name}" (${typeName(fspec)}) for ${family} op "${kind}". Fields: ${validFields.join(', ')}.`,
|
|
378
|
-
validFields,
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// 3) requireOneOf (e.g. remove_edge needs index OR id).
|
|
384
|
-
if (spec.requireOneOf && !spec.requireOneOf.some(f => rec[f] !== undefined)) {
|
|
385
|
-
return {
|
|
386
|
-
code: 'INVALID_OP', reason: 'require_one_of', family, opKind: kind,
|
|
387
|
-
message: `${family} op "${kind}" requires at least one of: ${spec.requireOneOf.join(', ')}.`,
|
|
388
|
-
validFields,
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// 4) Present fields must match their declared type.
|
|
393
|
-
for (const [name, fspec] of Object.entries(spec.fields)) {
|
|
394
|
-
if (rec[name] === undefined) continue
|
|
395
|
-
if (!matchesType(fspec, rec[name])) {
|
|
396
|
-
return {
|
|
397
|
-
code: 'INVALID_OP', reason: 'wrong_type', family, opKind: kind, field: name,
|
|
398
|
-
message: `Field "${name}" of ${family} op "${kind}" must be ${typeName(fspec)}, got ${JSON.stringify(rec[name])}.`,
|
|
399
|
-
validFields,
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return null
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/** Machine-readable op menu for a family — { opKind: [fieldName, …] } — the
|
|
408
|
-
* basis for tool-discovery output. Optional fields are marked with a trailing
|
|
409
|
-
* `?`; the op's `kind` is implicit and omitted. */
|
|
410
|
-
export function opMenu(family: OpFamily): Record<string, string[]> {
|
|
411
|
-
const out: Record<string, string[]> = {}
|
|
412
|
-
for (const [kind, spec] of Object.entries(SCHEMAS[family])) {
|
|
413
|
-
out[kind] = Object.entries(spec.fields).map(([n, f]) => (f.required ? n : `${n}?`))
|
|
414
|
-
}
|
|
415
|
-
return out
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** One field of an op, as a model needs it to fill the op correctly: the name,
|
|
419
|
-
* whether it is required, and a human type that spells out enum values inline
|
|
420
|
-
* (e.g. `one of "inheritance", "composition", …`). */
|
|
421
|
-
export interface OpFieldDoc { name: string; required: boolean; type: string; note?: string }
|
|
422
|
-
|
|
423
|
-
/** Full field shapes for every op of a family — the thing a model must know to
|
|
424
|
-
* author a correct op without guessing (which the prescriptive INVALID_OP error
|
|
425
|
-
* only teaches AFTER a wrong guess). Surfaced in `am capabilities --json` and
|
|
426
|
-
* the declarative MCP tool descriptions so field names, required-ness, and enum
|
|
427
|
-
* vocabularies are discoverable up front. */
|
|
428
|
-
export function describeOps(family: OpFamily): Record<string, OpFieldDoc[]> {
|
|
429
|
-
const out: Record<string, OpFieldDoc[]> = {}
|
|
430
|
-
for (const [kind, spec] of Object.entries(SCHEMAS[family])) {
|
|
431
|
-
out[kind] = Object.entries(spec.fields).map(([name, f]) => ({ name, required: f.required, type: typeName(f), ...(f.note !== undefined ? { note: f.note } : {}) }))
|
|
432
|
-
}
|
|
433
|
-
return out
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/** Compact one-line signatures for a family's ops — `add_relation(from, to,
|
|
437
|
-
* relKind, label?)` — for embedding in a tool description. Optional fields carry
|
|
438
|
-
* a trailing `?`; enum values are left to `describeOps`/the error to keep the
|
|
439
|
-
* menu short. */
|
|
440
|
-
export function opSignatures(family: OpFamily): string[] {
|
|
441
|
-
const menu = opMenu(family)
|
|
442
|
-
return Object.entries(menu).map(([kind, fields]) => `${kind}(${fields.join(', ')})`)
|
|
443
|
-
}
|
package/src/agent/parse.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// parseMermaid: source → ValidDiagram (multi-error).
|
|
3
|
-
//
|
|
4
|
-
// v4 sequence fidelity: parseSequenceBody returns null if it encounters ANY
|
|
5
|
-
// non-blank, non-comment line it doesn't fully understand. parse then falls
|
|
6
|
-
// back to an opaque body (lossless round-trip via canonicalSource) rather
|
|
7
|
-
// than silently dropping the unrecognized construct.
|
|
8
|
-
// ============================================================================
|
|
9
|
-
|
|
10
|
-
import { normalizeMermaidSource, detectLooseDiagramTypeFromFirstLine } from '../mermaid-source.ts'
|
|
11
|
-
import { getFamily } from './families.ts'
|
|
12
|
-
import './families-builtin.ts' // registers built-in family parse/serialize/mutate hooks
|
|
13
|
-
import { serializeMermaid } from './serialize.ts'
|
|
14
|
-
import type {
|
|
15
|
-
ValidDiagram, ParseError, Result, DiagramKind, ValidDiagramMeta,
|
|
16
|
-
SourceMap, InitDirective, SourceComment,
|
|
17
|
-
} from './types.ts'
|
|
18
|
-
import { ok, err } from './types.ts'
|
|
19
|
-
|
|
20
|
-
// Re-exports for callers/tests that used the previous in-tree parser homes.
|
|
21
|
-
export { parseSequenceBody } from './sequence-body.ts'
|
|
22
|
-
export { parseTimelineBody } from './timeline-body.ts'
|
|
23
|
-
|
|
24
|
-
const FRONTMATTER_REGEX = /^?\s*---\s*\r?\n([\s\S]*?)\r?\n\s*---\s*(?:\r?\n|$)/
|
|
25
|
-
const INIT_DIRECTIVE_REGEX = /^\s*%%\{\s*(?:init|initialize)\s*:\s*([\s\S]*?)\}\s*%%\s*(?:\r?\n|$)?/gm
|
|
26
|
-
const COMMENT_LINE_REGEX = /^\s*%%(?!\{)\s*(.*)$/
|
|
27
|
-
const ACC_TITLE_REGEX = /^\s*accTitle\s*:\s*(.+)$/i
|
|
28
|
-
const ACC_DESCR_INLINE_REGEX = /^\s*accDescr\s*:\s*(.+)$/i
|
|
29
|
-
const ACC_DESCR_BLOCK_START = /^\s*accDescr\s*\{\s*$/i
|
|
30
|
-
|
|
31
|
-
export function parseMermaid(source: string): Result<ValidDiagram, ParseError[]> {
|
|
32
|
-
const errors: ParseError[] = []
|
|
33
|
-
// Normalize once and reuse its frontmatter in extractMeta, rather than
|
|
34
|
-
// re-running the full preprocess a second time just for frontmatter.
|
|
35
|
-
const normalized = normalizeMermaidSource(source)
|
|
36
|
-
const meta = extractMeta(source, normalized.frontmatter)
|
|
37
|
-
const canonicalSource = normalized.text
|
|
38
|
-
// For opaque bodies, preserve original indentation/blank lines so the
|
|
39
|
-
// serializer can re-emit the untouched body. canonicalSource at the
|
|
40
|
-
// ValidDiagram level remains the normalized (line-trimmed) form for
|
|
41
|
-
// back-compat with the existing flowchart/legacy paths.
|
|
42
|
-
const opaqueSource = normalized.body
|
|
43
|
-
|
|
44
|
-
if (normalized.lines.length === 0) {
|
|
45
|
-
errors.push({ code: 'EMPTY', message: 'Empty diagram' })
|
|
46
|
-
return err(errors)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const header = normalized.lines[0]!.toLowerCase()
|
|
50
|
-
const kind = detectKind(header)
|
|
51
|
-
if (!kind) {
|
|
52
|
-
errors.push({ code: 'UNKNOWN_HEADER', message: `Unrecognized header: "${normalized.lines[0]}"`, line: 1 })
|
|
53
|
-
return err(errors)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// BUILD-3: every family dispatches through its FamilyPlugin.parse hook —
|
|
57
|
-
// structured-or-opaque for the narrow families, error semantics for
|
|
58
|
-
// flowchart/state. Families without a hook stay source-level.
|
|
59
|
-
const plugin = getFamily(kind)
|
|
60
|
-
if (plugin?.parse) {
|
|
61
|
-
const parsed = plugin.parse(normalized.lines, opaqueSource, meta, canonicalSource)
|
|
62
|
-
if (!parsed.ok) {
|
|
63
|
-
errors.push(...parsed.error)
|
|
64
|
-
return err(errors)
|
|
65
|
-
}
|
|
66
|
-
const sourceMap = plugin.buildSourceMap?.(parsed.value, canonicalSource) ?? emptySourceMap()
|
|
67
|
-
const diagram: ValidDiagram = { kind, meta, body: parsed.value, source: sourceMap, canonicalSource }
|
|
68
|
-
markDroppedComments(diagram, normalized.body)
|
|
69
|
-
return ok(diagram)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return ok<ValidDiagram>({
|
|
73
|
-
kind, meta, body: { kind: 'opaque', family: kind, source: opaqueSource }, source: emptySourceMap(), canonicalSource,
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 2C comment policy: structured bodies serialize to canonical source, which
|
|
79
|
-
* does not model `%%` comment lines. Rather than dropping them *silently*,
|
|
80
|
-
* diff the parsed comments against what actually survives serialization
|
|
81
|
-
* (wrapper comments ride along verbatim via meta.wrapperSource; sequence
|
|
82
|
-
* opaque segments may preserve in-body comments) and record the casualties so
|
|
83
|
-
* verify can surface the Tier 3 COMMENT_DROPPED lint. Opaque bodies preserve
|
|
84
|
-
* everything and never reach here.
|
|
85
|
-
*/
|
|
86
|
-
function markDroppedComments(diagram: ValidDiagram, sourceBody: string): void {
|
|
87
|
-
const comments = diagram.meta.comments
|
|
88
|
-
if (diagram.body.kind === 'opaque' || comments.length === 0) return
|
|
89
|
-
|
|
90
|
-
const sourceLines = sourceBody.split(/\r?\n/).map(line => line.trim())
|
|
91
|
-
const serializedLines = serializeMermaid(diagram).split(/\r?\n/).map(line => line.trim())
|
|
92
|
-
const keptSourceLines = longestCommonSubsequenceIndices(sourceLines, serializedLines)
|
|
93
|
-
const keptCommentLines = new Set<number>()
|
|
94
|
-
for (const sourceIndex of keptSourceLines) {
|
|
95
|
-
if (COMMENT_LINE_REGEX.test(sourceLines[sourceIndex]!)) keptCommentLines.add(sourceIndex + 1)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const dropped: SourceComment[] = comments.filter(comment => !keptCommentLines.has(comment.line))
|
|
99
|
-
if (dropped.length > 0) diagram.meta.droppedComments = dropped
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function longestCommonSubsequenceIndices(a: string[], b: string[]): number[] {
|
|
103
|
-
const dp: number[][] = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0))
|
|
104
|
-
for (let i = a.length - 1; i >= 0; i--) {
|
|
105
|
-
for (let j = b.length - 1; j >= 0; j--) {
|
|
106
|
-
dp[i]![j] = a[i] === b[j]
|
|
107
|
-
? dp[i + 1]![j + 1]! + 1
|
|
108
|
-
: Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const indices: number[] = []
|
|
112
|
-
let i = 0
|
|
113
|
-
let j = 0
|
|
114
|
-
while (i < a.length && j < b.length) {
|
|
115
|
-
if (a[i] === b[j]) {
|
|
116
|
-
indices.push(i)
|
|
117
|
-
i++
|
|
118
|
-
j++
|
|
119
|
-
} else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {
|
|
120
|
-
i++
|
|
121
|
-
} else {
|
|
122
|
-
j++
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return indices
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ---- Family detection ----------------------------------------------------
|
|
129
|
-
// Use the shared renderer detector for routed families; the agent layer only
|
|
130
|
-
// splits state diagrams out from the renderer's flowchart route because state
|
|
131
|
-
// and flowchart share the same legacy graph body but remain distinct families.
|
|
132
|
-
|
|
133
|
-
function detectKind(header: string): DiagramKind | null {
|
|
134
|
-
if (/^statediagram(?:-v2)?\s*$/i.test(header)) return 'state'
|
|
135
|
-
const routed = detectLooseDiagramTypeFromFirstLine(header)
|
|
136
|
-
if (!routed) return null
|
|
137
|
-
return routed
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ---- Meta extraction -----------------------------------------------------
|
|
141
|
-
|
|
142
|
-
function extractMeta(rawSource: string, frontmatter?: Record<string, unknown>): ValidDiagramMeta {
|
|
143
|
-
const meta: ValidDiagramMeta = { initDirectives: [], comments: [], accessibility: {} }
|
|
144
|
-
|
|
145
|
-
const wrapperSource = extractWrapperSource(rawSource)
|
|
146
|
-
if (wrapperSource !== undefined) meta.wrapperSource = wrapperSource
|
|
147
|
-
|
|
148
|
-
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
149
|
-
meta.frontmatter = frontmatter as ValidDiagramMeta['frontmatter']
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const directiveRegex = new RegExp(INIT_DIRECTIVE_REGEX.source, 'gm')
|
|
153
|
-
let m: RegExpExecArray | null
|
|
154
|
-
while ((m = directiveRegex.exec(rawSource)) !== null) {
|
|
155
|
-
meta.initDirectives.push({ raw: m[0], parsed: tryParseInitObject((m[1] ?? '').trim()) as InitDirective['parsed'] })
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const stripped = rawSource
|
|
159
|
-
.replace(FRONTMATTER_REGEX, '')
|
|
160
|
-
.replace(new RegExp(INIT_DIRECTIVE_REGEX.source, 'gm'), '')
|
|
161
|
-
const lines = stripped.split(/\r?\n/)
|
|
162
|
-
let inAccDescr = false
|
|
163
|
-
let buf: string[] = []
|
|
164
|
-
for (let i = 0; i < lines.length; i++) {
|
|
165
|
-
const line = lines[i]!
|
|
166
|
-
if (inAccDescr) {
|
|
167
|
-
if (/^\s*\}\s*$/.test(line)) { meta.accessibility.descr = buf.join('\n').trim(); inAccDescr = false; buf = [] }
|
|
168
|
-
else buf.push(line)
|
|
169
|
-
continue
|
|
170
|
-
}
|
|
171
|
-
if (ACC_DESCR_BLOCK_START.test(line)) { inAccDescr = true; continue }
|
|
172
|
-
const at = line.match(ACC_TITLE_REGEX)
|
|
173
|
-
if (at) { meta.accessibility.title = at[1]!.trim(); continue }
|
|
174
|
-
const ad = line.match(ACC_DESCR_INLINE_REGEX)
|
|
175
|
-
if (ad) { meta.accessibility.descr = ad[1]!.trim(); continue }
|
|
176
|
-
const cm = line.match(COMMENT_LINE_REGEX)
|
|
177
|
-
if (cm) meta.comments.push({ text: cm[1]!, line: i + 1 })
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return meta
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The leading source wrapper, byte-verbatim: an optional frontmatter block
|
|
185
|
-
* followed by any run of blank lines, `%%` comments, and `%%{init}%%`
|
|
186
|
-
* directives, up to (excluding) the diagram header line. This is what
|
|
187
|
-
* serializeMermaid re-emits untouched by default (1C wrapper policy).
|
|
188
|
-
*/
|
|
189
|
-
function extractWrapperSource(rawSource: string): string | undefined {
|
|
190
|
-
let pos = 0
|
|
191
|
-
const fm = rawSource.match(FRONTMATTER_REGEX)
|
|
192
|
-
if (fm) pos = fm[0].length
|
|
193
|
-
const directiveAtStart = new RegExp(INIT_DIRECTIVE_REGEX.source)
|
|
194
|
-
for (;;) {
|
|
195
|
-
const rest = rawSource.slice(pos)
|
|
196
|
-
if (rest.length === 0) break
|
|
197
|
-
const dm = rest.match(directiveAtStart)
|
|
198
|
-
if (dm && dm.index === 0 && dm[0].length > 0) { pos += dm[0].length; continue }
|
|
199
|
-
const lineEnd = rest.indexOf('\n')
|
|
200
|
-
const line = lineEnd === -1 ? rest : rest.slice(0, lineEnd)
|
|
201
|
-
if (/^\s*$/.test(line) || COMMENT_LINE_REGEX.test(line)) {
|
|
202
|
-
pos += lineEnd === -1 ? rest.length : lineEnd + 1
|
|
203
|
-
continue
|
|
204
|
-
}
|
|
205
|
-
break
|
|
206
|
-
}
|
|
207
|
-
return pos > 0 ? rawSource.slice(0, pos) : undefined
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function tryParseInitObject(inner: string): Record<string, unknown> {
|
|
211
|
-
try { return JSON.parse(inner) as Record<string, unknown> }
|
|
212
|
-
catch {
|
|
213
|
-
try {
|
|
214
|
-
const quoted = inner.replace(/([{,]\s*)([A-Za-z_$][\w$]*)\s*:/g, '$1"$2":')
|
|
215
|
-
return JSON.parse(quoted) as Record<string, unknown>
|
|
216
|
-
} catch { return {} }
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function emptySourceMap(): SourceMap {
|
|
221
|
-
return { nodes: new Map(), edges: new Map(), groups: new Map(), labels: new Map() }
|
|
222
|
-
}
|
|
223
|
-
|