| { |
| "$schema": "https://json-schema.org/draft/2020-12/schema", |
| "$id": "https://excali-ft/dsl.schema.json", |
| "title": "Excalidraw Diagram DSL", |
| "description": "Compact, semantic-only description of a diagram. Carries planning decisions (what exists, what connects, what type). All geometry, sizing, IDs, colors, and Excalidraw format fields are computed deterministically by the converter and MUST NOT appear here.", |
| "type": "object", |
| "additionalProperties": false, |
| "required": ["diagram", "title", "nodes"], |
| "properties": { |
| "diagram": { |
| "description": "Selects the converter strategy and default layout.", |
| "type": "string", |
| "enum": [ |
| "system_architecture", |
| "flowchart", |
| "er_diagram", |
| "sequence_diagram", |
| "data_pipeline", |
| "timeline", |
| "mind_map", |
| "mobile_wireframe" |
| ] |
| }, |
| "title": { |
| "description": "Human-readable diagram title, rendered as a heading text element.", |
| "type": "string", |
| "minLength": 1, |
| "maxLength": 80 |
| }, |
| "direction": { |
| "description": "Primary flow/layout direction. Optional; the converter applies a per-diagram default when omitted (e.g. TB for flowchart, LR for architecture, radial for mind_map).", |
| "type": "string", |
| "enum": ["LR", "RL", "TB", "BT", "radial"] |
| }, |
| "nodes": { |
| "description": "The boxes/entities/participants/milestones. Internal `id`s are reference handles only and are never rendered.", |
| "type": "array", |
| "minItems": 1, |
| "maxItems": 40, |
| "items": { "$ref": "#/$defs/node" } |
| }, |
| "edges": { |
| "description": "Connections between nodes. For sequence_diagram, array order is message order. May be empty (e.g. some timelines/mind maps without explicit links).", |
| "type": "array", |
| "maxItems": 80, |
| "items": { "$ref": "#/$defs/edge" }, |
| "default": [] |
| }, |
| "groups": { |
| "description": "Optional clusters/swimlanes/screens. A node joins a group via node.group = group.id.", |
| "type": "array", |
| "maxItems": 12, |
| "items": { "$ref": "#/$defs/group" }, |
| "default": [] |
| }, |
| "meta": { |
| "description": "Diagram-level escape hatch for type-specific knobs. Keep empty unless a type below defines a key.", |
| "type": "object", |
| "additionalProperties": true, |
| "default": {} |
| } |
| }, |
|
|
| "$defs": { |
| "id": { |
| "description": "Stable snake_case identifier, unique within its collection. Reference handle only — not rendered.", |
| "type": "string", |
| "pattern": "^[a-z][a-z0-9_]*$", |
| "minLength": 1, |
| "maxLength": 40 |
| }, |
| "label": { |
| "description": "Visible text. Non-empty, single line preferred.", |
| "type": "string", |
| "minLength": 1, |
| "maxLength": 60 |
| }, |
| "role": { |
| "description": "Semantic kind of the node. The converter maps role -> {shape, fill color, stroke color}. The model never picks shapes or colors directly.", |
| "type": "string", |
| "enum": [ |
| "actor", |
| "client", |
| "service", |
| "gateway", |
| "database", |
| "cache", |
| "queue", |
| "worker", |
| "storage", |
| "external", |
| "monitoring", |
| "auth", |
| "start", |
| "end", |
| "process", |
| "decision", |
| "io", |
| "source", |
| "transform", |
| "sink", |
| "entity", |
| "participant", |
| "milestone", |
| "root", |
| "branch", |
| "leaf", |
| "screen", |
| "ui_element", |
| "component", |
| "generic" |
| ] |
| }, |
| "node": { |
| "type": "object", |
| "additionalProperties": false, |
| "required": ["id", "label", "role"], |
| "properties": { |
| "id": { "$ref": "#/$defs/id" }, |
| "label": { "$ref": "#/$defs/label" }, |
| "role": { "$ref": "#/$defs/role" }, |
| "group": { |
| "description": "Optional group membership; must equal some groups[].id.", |
| "$ref": "#/$defs/id" |
| }, |
| "color": { |
| "description": "OPTIONAL explicit override only when a human would intentionally choose a color (e.g. emphasis). Otherwise omit and let the converter derive color from role.", |
| "type": "string", |
| "enum": ["red", "orange", "yellow", "green", "blue", "violet", "gray", "black"] |
| }, |
| "meta": { |
| "description": "Type-specific node data. See per-diagram rules in DSL_SPEC.md. Examples: er_diagram fields, timeline date, wireframe ui kind.", |
| "type": "object", |
| "additionalProperties": false, |
| "properties": { |
| "fields": { |
| "description": "er_diagram only: entity columns.", |
| "type": "array", |
| "maxItems": 20, |
| "items": { |
| "type": "object", |
| "additionalProperties": false, |
| "required": ["name"], |
| "properties": { |
| "name": { "type": "string", "minLength": 1, "maxLength": 40 }, |
| "type": { "type": "string", "maxLength": 30 }, |
| "key": { "type": "string", "enum": ["pk", "fk", "none"], "default": "none" } |
| } |
| } |
| }, |
| "date": { |
| "description": "timeline only: ISO-ish date or short label used for ordering and display.", |
| "type": "string", |
| "maxLength": 30 |
| }, |
| "kind": { |
| "description": "mobile_wireframe only: the UI element kind.", |
| "type": "string", |
| "enum": ["header", "nav", "text", "input", "button", "list", "image", "card", "tab", "footer"] |
| } |
| } |
| } |
| } |
| }, |
| "edge": { |
| "type": "object", |
| "additionalProperties": false, |
| "required": ["from", "to"], |
| "properties": { |
| "from": { |
| "description": "Source node id. Must reference an existing node.", |
| "$ref": "#/$defs/id" |
| }, |
| "to": { |
| "description": "Target node id. Must reference an existing node.", |
| "$ref": "#/$defs/id" |
| }, |
| "label": { |
| "description": "Optional edge label. For flowchart decision branches use 'yes'/'no'.", |
| "type": "string", |
| "maxLength": 40 |
| }, |
| "style": { |
| "description": "Line style. Default solid.", |
| "type": "string", |
| "enum": ["solid", "dashed", "dotted"], |
| "default": "solid" |
| }, |
| "meta": { |
| "type": "object", |
| "additionalProperties": false, |
| "properties": { |
| "cardinality": { |
| "description": "er_diagram only: relationship cardinality.", |
| "type": "string", |
| "enum": ["1:1", "1:N", "N:1", "N:M"] |
| }, |
| "kind": { |
| "description": "sequence_diagram only: message kind.", |
| "type": "string", |
| "enum": ["sync", "async", "return"], |
| "default": "sync" |
| } |
| } |
| } |
| } |
| }, |
| "group": { |
| "type": "object", |
| "additionalProperties": false, |
| "required": ["id", "label"], |
| "properties": { |
| "id": { "$ref": "#/$defs/id" }, |
| "label": { "$ref": "#/$defs/label" } |
| } |
| } |
| }, |
|
|
| "allOf": [ |
| { |
| "description": "ER diagrams: relationships should carry cardinality.", |
| "if": { "properties": { "diagram": { "const": "er_diagram" } } }, |
| "then": { |
| "properties": { |
| "edges": { |
| "items": { |
| "properties": { |
| "meta": { "required": ["cardinality"] } |
| }, |
| "required": ["meta"] |
| } |
| } |
| } |
| } |
| }, |
| { |
| "description": "Timelines: every milestone node should carry a date.", |
| "if": { "properties": { "diagram": { "const": "timeline" } } }, |
| "then": { |
| "properties": { |
| "nodes": { |
| "items": { |
| "properties": { |
| "meta": { "required": ["date"] } |
| }, |
| "required": ["meta"] |
| } |
| } |
| } |
| } |
| }, |
| { |
| "description": "Mobile wireframes: every node needs a UI kind.", |
| "if": { "properties": { "diagram": { "const": "mobile_wireframe" } } }, |
| "then": { |
| "properties": { |
| "nodes": { |
| "items": { |
| "properties": { |
| "meta": { "required": ["kind"] } |
| }, |
| "required": ["meta"] |
| } |
| } |
| } |
| } |
| } |
| ] |
| } |
|
|