diff --git a/.changeset/sequence-activation-bars.md b/.changeset/sequence-activation-bars.md new file mode 100644 index 00000000..4858729b --- /dev/null +++ b/.changeset/sequence-activation-bars.md @@ -0,0 +1,7 @@ +--- +"@sketchi/cli": minor +--- + +Sequence diagrams from `sketchi create` and `sketchi generate` now draw activation bars. A message of type `return` answers the latest open call between the two participants, and the callee's lifeline shows a bar from that call to its return. Re-entrant calls nest, and calls nobody answers draw no bar. Messages attach to the bars in the PNG and Excalidraw exports. + +Activation bars use ids under `:lifeline:`, so a participant id that starts with another participant's id followed by `:lifeline` (for example `api:lifeline:v2` next to `api`) is now rejected. Before, only an exact `api:lifeline` collided. The repair hint is now "Rename the participant so its id does not start with another participant id followed by :lifeline." diff --git a/apps/playground/src/server/codemode/mcp-docs/catalog.ts b/apps/playground/src/server/codemode/mcp-docs/catalog.ts index a8b71ffd..44cb4cd9 100644 --- a/apps/playground/src/server/codemode/mcp-docs/catalog.ts +++ b/apps/playground/src/server/codemode/mcp-docs/catalog.ts @@ -433,6 +433,7 @@ export const catalog: CatalogEntry[] = [ "sequence", "participant", "lifeline", + "activation", "message", "chronology", "interaction", @@ -442,6 +443,7 @@ export const catalog: CatalogEntry[] = [ "buildSequenceDiagram accepts a semantic SequenceDiagramSpec with ordered participants and chronological messages.", "Use a sequence diagram when time-ordered interactions between actors or systems are the main idea. Use buildFlowchart for decisions and process paths; use buildMindmap for nested topic hierarchy.", "Participant order is preserved left-to-right. Message order is preserved top-to-bottom.", + 'A message with type "return" from the callee back to its caller answers the latest open call between them; Sketchi draws the callee\'s activation bar from that call to its return, nesting re-entrant calls. Calls nobody answers draw no bar.', "Every message source and target must reference a declared participant id. Self-referential messages are rejected with structured issues and repair hints.", 'Request envelope: { spec: SequenceDiagramSpec, options?: { artifactFormats?: ["scene", "excalidraw", "png"], inlineArtifacts?: ["scene", "excalidraw"], minQualityScore?: number } }.', "Request scene, Excalidraw, and PNG for full artifact delivery; inline Excalidraw when the harness needs importable JSON in the response.", diff --git a/apps/playground/src/server/codemode/mcp-docs/code-mode-types.generated.txt b/apps/playground/src/server/codemode/mcp-docs/code-mode-types.generated.txt index 005eed85..0b17728a 100644 --- a/apps/playground/src/server/codemode/mcp-docs/code-mode-types.generated.txt +++ b/apps/playground/src/server/codemode/mcp-docs/code-mode-types.generated.txt @@ -25,15 +25,15 @@ type BuildSequenceDiagramRequest = { "requestId"?: string; "spec": { "id"?: stri type BuildSequenceDiagramResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "id": string; "title": string; "participants": Array<{ "id": string; "label": string; "kind"?: string; }>; "messages": Array<{ "id": string; "source": string; "target": string; "label": string; "type"?: "message" | "return"; "style"?: "solid" | "dashed"; }>; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality": { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_sequence" | "quality_failed" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "id": string; "title": string; "participants": Array<{ "id": string; "label": string; "kind"?: string; }>; "messages": Array<{ "id": string; "source": string; "target": string; "label": string; "type"?: "message" | "return"; "style"?: "solid" | "dashed"; }>; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality"?: { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); -type CreateCanvasRequest = { "requestId"?: string; "spec": { "kind": "canvas"; "version": 1; "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: ("sequence-lifeline"); "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers"?: Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts"?: Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder"?: Array; }; "options"?: { "artifactFormats"?: Array<"excalidraw" | "scene" | "png">; "inlineArtifacts"?: Array<"excalidraw" | "scene">; "minQualityScore"?: number; }; }; +type CreateCanvasRequest = { "requestId"?: string; "spec": { "kind": "canvas"; "version": 1; "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers"?: Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts"?: Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder"?: Array; }; "options"?: { "artifactFormats"?: Array<"excalidraw" | "scene" | "png">; "inlineArtifacts"?: Array<"excalidraw" | "scene">; "minQualityScore"?: number; }; }; -type CreateCanvasResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "kind": "canvas"; "version": (number | "Infinity" | "-Infinity" | "NaN"); "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: ("sequence-lifeline"); "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers": Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts": Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder": Array; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_canvas" | "limit_exceeded" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "kind": "canvas"; "version": (number | "Infinity" | "-Infinity" | "NaN"); "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: ("sequence-lifeline"); "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers": Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts": Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder": Array; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); +type CreateCanvasResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "kind": "canvas"; "version": (number | "Infinity" | "-Infinity" | "NaN"); "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers": Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts": Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder": Array; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_canvas" | "limit_exceeded" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "kind": "canvas"; "version": (number | "Infinity" | "-Infinity" | "NaN"); "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers": Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts": Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder": Array; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); type GetArtifactRequest = { "artifactId": string; "format"?: "excalidraw" | "scene" | "png"; "inline"?: boolean; }; type GetArtifactResult = ({ "ok": true; "artifactId": string; "diagramId": string; "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); "provenance"?: { "sourceArtifactId": string; }; } | { "ok": false; "status": "invalid_input" | "not_found" | "format_unavailable" | "expired" | "storage_failed"; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); -type ApplyDiagramPatchRequest = { "requestId"?: string; "source": ({ "artifactId": string; "format"?: "scene"; } | { "scene": { "kind": "canvas"; "version": 1; "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: ("sequence-lifeline"); "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers"?: Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts"?: Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder"?: Array; }; }); "operations": Array<({ "op": "setDefaultStyle"; "style": { "strokeColor"?: string; "fillColor"?: string; "textColor"?: string; "backgroundColor"?: string; }; } | { "op": "setStyle"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "style": { "strokeColor"?: string; "fillColor"?: string; "textColor"?: string; "backgroundColor"?: string; }; } | { "op": "setShape"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "shape": "rectangle" | "diamond" | "ellipse" | "circle" | "polygon"; } | { "op": "translate"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "dx": number; "dy": number; } | { "op": "replaceText"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "text": string; } | { "op": "rerouteEdges"; "selector"?: { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; } | { "op": "insert"; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: ("sequence-lifeline"); "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "beforeId"?: string; "afterId"?: string; } | { "op": "remove"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; } | { "op": "replace"; "id": string; "element": ({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: ("sequence-lifeline"); "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; }); } | { "op": "reorder"; "ids": Array; "beforeId"?: string; "afterId"?: string; } | { "op": "group"; "ids": Array; "groupId": string; } | { "op": "ungroup"; "ids": Array; "groupId"?: string; })>; "options"?: { "artifactFormats"?: Array<"excalidraw" | "scene" | "png">; "inlineArtifacts"?: Array<"excalidraw" | "scene">; "preserveConnectivity"?: boolean; }; "intent"?: string; }; +type ApplyDiagramPatchRequest = { "requestId"?: string; "source": ({ "artifactId": string; "format"?: "scene"; } | { "scene": { "kind": "canvas"; "version": 1; "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers"?: Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts"?: Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder"?: Array; }; }); "operations": Array<({ "op": "setDefaultStyle"; "style": { "strokeColor"?: string; "fillColor"?: string; "textColor"?: string; "backgroundColor"?: string; }; } | { "op": "setStyle"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "style": { "strokeColor"?: string; "fillColor"?: string; "textColor"?: string; "backgroundColor"?: string; }; } | { "op": "setShape"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "shape": "rectangle" | "diamond" | "ellipse" | "circle" | "polygon"; } | { "op": "translate"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "dx": number; "dy": number; } | { "op": "replaceText"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "text": string; } | { "op": "rerouteEdges"; "selector"?: { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; } | { "op": "insert"; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "beforeId"?: string; "afterId"?: string; } | { "op": "remove"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; } | { "op": "replace"; "id": string; "element": ({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; }); } | { "op": "reorder"; "ids": Array; "beforeId"?: string; "afterId"?: string; } | { "op": "group"; "ids": Array; "groupId": string; } | { "op": "ungroup"; "ids": Array; "groupId"?: string; })>; "options"?: { "artifactFormats"?: Array<"excalidraw" | "scene" | "png">; "inlineArtifacts"?: Array<"excalidraw" | "scene">; "preserveConnectivity"?: boolean; }; "intent"?: string; }; type ApplyDiagramPatchResult = ({ "ok": true; "status": "accepted"; "patchId": string; "requestId"?: string; "sourceArtifactId"?: string; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "source_unavailable" | "target_not_found" | "unsupported_operation" | "connectivity_changed" | "render_failed" | "export_failed" | "storage_failed"; "patchId"?: string; "requestId"?: string; "sourceArtifactId"?: string; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); diff --git a/packages/diagram/agent/src/lib/code-mode/contract.ts b/packages/diagram/agent/src/lib/code-mode/contract.ts index a5bdbc17..afac2fdd 100644 --- a/packages/diagram/agent/src/lib/code-mode/contract.ts +++ b/packages/diagram/agent/src/lib/code-mode/contract.ts @@ -3,6 +3,7 @@ import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/sp import { CANVAS_LIMITS, CANVAS_NODE_ICON, + CANVAS_RENDERER_ROLES, CANVAS_SPEC_VERSION, SEQUENCE_MESSAGE_STYLES, SEQUENCE_MESSAGE_TYPES, @@ -906,7 +907,7 @@ export class NodeSceneElement extends Schema.Class("NodeSceneE nodeId: RequiredNonEmptyString, kind: optionalContract(NonEmptyString), icon: optionalContract(CanvasNodeIcon).pipe(Schema.mutableKey), - rendererRole: optionalContract(literals(["sequence-lifeline"])), + rendererRole: optionalContract(literals(CANVAS_RENDERER_ROLES)), shape: literals(["rectangle", "ellipse", "diamond", "circle", "polygon"]).pipe( Schema.mutableKey, ), diff --git a/packages/diagram/agent/src/lib/code-mode/runtime.test.ts b/packages/diagram/agent/src/lib/code-mode/runtime.test.ts index 4a5bf325..19889217 100644 --- a/packages/diagram/agent/src/lib/code-mode/runtime.test.ts +++ b/packages/diagram/agent/src/lib/code-mode/runtime.test.ts @@ -1809,6 +1809,101 @@ describe("Code Mode runtime", () => { }); }); + it("keeps activation bars and their bound messages through an inline-scene patch", async () => { + const runtime = createTestRuntime(); + const spec = checkoutSequenceSpec(); + const built = await runtime.buildSequenceDiagram({ + spec: { + ...spec, + messages: [ + ...spec.messages.slice(0, 2), + { source: "payments", target: "store", label: "Approved", type: "return" }, + { source: "store", target: "customer", label: "Receipt", type: "return" }, + ], + }, + options: { artifactFormats: ["scene", "excalidraw"], inlineArtifacts: ["scene"] }, + }); + if (!built.ok) throw new Error("Expected accepted sequence diagram."); + const sourceScene = parseInlineScene( + built.artifact.formats.find(({ format }) => format === "scene")?.inline, + ); + const bars = (scene: typeof sourceScene) => + scene.elements.flatMap((element) => + element.type === "node" && element.rendererRole === "sequence-activation" + ? [element.nodeId] + : [], + ); + expect(bars(sourceScene)).toEqual([ + "store:lifeline:activation:message-1-customer-store", + "payments:lifeline:activation:message-2-store-payments", + ]); + + const patched = await runtime.applyDiagramPatch({ + source: { scene: sourceScene }, + operations: [{ op: "setDefaultStyle", style: {} }], + options: { artifactFormats: ["scene", "excalidraw"], inlineArtifacts: ["scene"] }, + }); + expectPatchOk(patched); + const patchedScene = parseInlineScene( + patched.artifact.formats.find(({ format }) => format === "scene")?.inline, + ); + expect(bars(patchedScene)).toEqual(bars(sourceScene)); + expect( + patchedScene.elements.find( + (element) => element.type === "arrow" && element.edgeId === "message-1-customer-store", + ), + ).toMatchObject({ targetNodeId: "store:lifeline:activation:message-1-customer-store" }); + }); + + it("keeps doubly nested activation bars through an inline-scene patch", async () => { + const runtime = createTestRuntime(); + const built = await runtime.buildSequenceDiagram({ + spec: { + title: "Deep nesting", + participants: [ + { id: "a", label: "A" }, + { id: "b", label: "B" }, + { id: "c", label: "C" }, + ], + messages: [ + { id: "q1", source: "a", target: "b", label: "First" }, + { id: "q2", source: "c", target: "b", label: "Second" }, + { id: "q3", source: "a", target: "b", label: "Third" }, + { id: "r3", source: "b", target: "a", label: "Third done", type: "return" }, + { id: "r2", source: "b", target: "c", label: "Second done", type: "return" }, + { id: "r1", source: "b", target: "a", label: "First done", type: "return" }, + ], + }, + options: { artifactFormats: ["scene", "excalidraw"], inlineArtifacts: ["scene"] }, + }); + if (!built.ok) throw new Error("Expected accepted sequence diagram."); + const sourceScene = parseInlineScene( + built.artifact.formats.find(({ format }) => format === "scene")?.inline, + ); + + const patched = await runtime.applyDiagramPatch({ + source: { scene: sourceScene }, + operations: [{ op: "setDefaultStyle", style: {} }], + options: { artifactFormats: ["scene", "excalidraw"], inlineArtifacts: ["scene"] }, + }); + expectPatchOk(patched); + expect(patched.issues).toEqual([]); + const patchedScene = parseInlineScene( + patched.artifact.formats.find(({ format }) => format === "scene")?.inline, + ); + expect( + patchedScene.elements.flatMap((element) => + element.type === "node" && element.nodeId.includes(":activation:") + ? [[element.nodeId, element.rendererRole]] + : [], + ), + ).toEqual([ + ["b:lifeline:activation:q1", "sequence-activation"], + ["b:lifeline:activation:q2", "sequence-activation"], + ["b:lifeline:activation:q3", "sequence-activation"], + ]); + }); + it("styles a crossing sequence through an inline-scene patch", async () => { const runtime = createTestRuntime(); const built = await runtime.buildSequenceDiagram({ diff --git a/packages/diagram/agent/src/lib/code-mode/runtime.ts b/packages/diagram/agent/src/lib/code-mode/runtime.ts index 95690788..e90edd6c 100644 --- a/packages/diagram/agent/src/lib/code-mode/runtime.ts +++ b/packages/diagram/agent/src/lib/code-mode/runtime.ts @@ -25,6 +25,7 @@ import { import { renderIntermediateDiagram, renderSequenceDiagram, + isStructurallyValidSequenceActivation, isStructurallyValidSequenceLifeline, type RenderedDiagramScene, type ScenePoint, @@ -1087,8 +1088,10 @@ function normalizePatchableScene(scene: PatchableScene): RenderedDiagramScene | ...(element.roughness !== undefined ? { roughness: element.roughness } : {}), ...(element.strokeStyle ? { strokeStyle: element.strokeStyle } : {}), ...(element.strokeWidth !== undefined ? { strokeWidth: element.strokeWidth } : {}), - ...(element.rendererRole === "sequence-lifeline" && - isStructurallyValidSequenceLifeline(scene, element) + ...((element.rendererRole === "sequence-lifeline" && + isStructurallyValidSequenceLifeline(scene, element)) || + (element.rendererRole === "sequence-activation" && + isStructurallyValidSequenceActivation(scene, element)) ? { rendererRole: element.rendererRole } : {}), ...(element.textColor ? { textColor: element.textColor } : {}), diff --git a/packages/diagram/core/src/canvas.ts b/packages/diagram/core/src/canvas.ts index c2c9f909..906f9bcf 100644 --- a/packages/diagram/core/src/canvas.ts +++ b/packages/diagram/core/src/canvas.ts @@ -78,13 +78,25 @@ export interface CanvasIconAsset { readonly svg: string; } +/** + * Structural shapes the sequence renderer derives: lifelines and activation + * bars. They carry no label or icon and are not obstacles for messages. + */ +export const CANVAS_RENDERER_ROLES = ["sequence-lifeline", "sequence-activation"] as const; +export type CanvasRendererRole = (typeof CANVAS_RENDERER_ROLES)[number]; + +const RENDERER_ROLE_NAMES: Readonly> = { + "sequence-activation": "Sequence activation", + "sequence-lifeline": "Sequence lifeline", +}; + export interface CanvasShapeElement extends CanvasElementComposition, CanvasStrokeStyleFields { readonly type: "node"; readonly id: string; readonly nodeId: string; readonly kind?: string; readonly icon?: CanvasNodeIcon; - readonly rendererRole?: "sequence-lifeline"; + readonly rendererRole?: CanvasRendererRole; readonly shape: CanvasShapeKind; readonly points?: [CanvasPoint, CanvasPoint, CanvasPoint, ...CanvasPoint[]]; readonly textColor?: string; @@ -396,7 +408,8 @@ export interface DroppedCanvasIcon { readonly elementId: string; readonly index: number; /** - * unavailable: no catalog asset; lifeline: sequence lifelines carry no logo; + * unavailable: no catalog asset; lifeline: sequence lifelines and activation + * bars carry no logo; * no_room: the label would no longer fit; budget: per-scene bytes spent. */ readonly reason: "budget" | "lifeline" | "no_room" | "unavailable"; @@ -466,7 +479,8 @@ function nodeLabelOverflows( * Embed one asset per referenced icon slug and remove icons that cannot be * drawn, so an icon never fails a build and every exported scene renders * without a catalog or network. Any authored `icons` map is replaced. An icon - * is dropped when its asset is unavailable, it sits on a sequence lifeline, or + * is dropped when its asset is unavailable, it sits on a sequence lifeline or + * activation bar, or * its label would no longer fit; icons past the per-scene byte budget are * dropped in element order, which keeps the result deterministic. */ @@ -494,7 +508,7 @@ export function embedCanvasIcons( ) { return drop("unavailable"); } - if (element.rendererRole === "sequence-lifeline") return drop("lifeline"); + if (element.rendererRole) return drop("lifeline"); if (element.shape !== "polygon" && isVisible(element)) { const label = boundLabels.get(element.id)?.element; if ( @@ -533,11 +547,11 @@ function validateNodeIcon( if (!icon) return []; const path = `elements[${index}].icon`; const issues: CanvasValidationIssue[] = []; - if (element.rendererRole === "sequence-lifeline") { + if (element.rendererRole) { issues.push({ code: "invalid_icon", elementId: element.id, - message: `Sequence lifeline "${element.id}" cannot carry an icon.`, + message: `${RENDERER_ROLE_NAMES[element.rendererRole]} "${element.id}" cannot carry an icon.`, path, }); } @@ -715,7 +729,7 @@ export function getCanvasValidationIssues(canvas: CanvasSpec): CanvasValidationI } if (element.type === "node" && element.shape !== "polygon" && isLayerVisible(element)) { const boundLabel = nodeLabelsByContainerId.get(element.id); - if (boundLabel || element.rendererRole !== "sequence-lifeline") { + if (boundLabel || element.rendererRole === undefined) { if (nodeLabelOverflows(element, boundLabel?.element, element.icon)) { issues.push({ code: "label_overflow", diff --git a/packages/diagram/core/src/types/sequence.test.ts b/packages/diagram/core/src/types/sequence.test.ts index 03dfe69b..883d217f 100644 --- a/packages/diagram/core/src/types/sequence.test.ts +++ b/packages/diagram/core/src/types/sequence.test.ts @@ -8,6 +8,7 @@ import { apiRequestSequence, getSequenceValidationIssues, parseSequenceDiagram, + sequenceActivations, sequenceDiagramType, sequenceFixture, sequenceLifelineId, @@ -100,4 +101,197 @@ describe("Sequence diagram type", () => { }), ).toThrow(/participants/u); }); + + it("derives activations from answered calls and nests re-entrant ones", () => { + expect( + sequenceActivations(sequenceFixture).map(({ participantId, startIndex, endIndex, depth }) => [ + participantId, + startIndex, + endIndex, + depth, + ]), + ).toEqual([ + ["store", 0, 3, 0], + ["payments", 1, 2, 0], + ]); + // The analytics call is fire-and-forget, so it opens no span. + expect(sequenceActivations(apiRequestSequence).map((span) => span.participantId)).toEqual([ + "api", + "cache", + "database", + ]); + + const reentrant = parseSequenceDiagram({ + id: "oauth", + title: "OAuth callback", + type: "sequence", + participants: [ + { id: "client", label: "Client" }, + { id: "api", label: "API" }, + { id: "auth", label: "Auth" }, + ], + messages: [ + { id: "login", source: "client", target: "api", label: "Log in" }, + { id: "authorize", source: "api", target: "auth", label: "Authorize" }, + { id: "callback", source: "auth", target: "api", label: "Callback" }, + { id: "ack", source: "api", target: "auth", label: "Ack", type: "return" }, + { id: "token", source: "auth", target: "api", label: "Token", type: "return" }, + { id: "notify", source: "api", target: "client", label: "Notify" }, + { id: "stray", source: "auth", target: "client", label: "Stray", type: "return" }, + { id: "session", source: "api", target: "client", label: "Session", type: "return" }, + ], + }); + expect( + sequenceActivations(reentrant).map( + ({ participantId, callMessageId, returnMessageId, depth }) => [ + participantId, + callMessageId, + returnMessageId, + depth, + ], + ), + ).toEqual([ + ["api", "login", "session", 0], + ["auth", "authorize", "token", 0], + ["api", "callback", "ack", 1], + ]); + }); + + it("keeps interleaved calls open and stacks partially overlapping spans", () => { + const interleaved = parseSequenceDiagram({ + id: "interleaved", + title: "Interleaved calls", + type: "sequence", + participants: [ + { id: "a", label: "A" }, + { id: "b", label: "B" }, + { id: "c", label: "C" }, + ], + messages: [ + { id: "q1", source: "a", target: "b", label: "Query 1" }, + { id: "q2", source: "c", target: "b", label: "Query 2" }, + { id: "p1", source: "b", target: "a", label: "Reply 1", type: "return" }, + { id: "p2", source: "b", target: "c", label: "Reply 2", type: "return" }, + ], + }); + expect( + sequenceActivations(interleaved).map(({ callMessageId, returnMessageId, depth }) => [ + callMessageId, + returnMessageId, + depth, + ]), + ).toEqual([ + ["q1", "p1", 0], + ["q2", "p2", 1], + ]); + }); + + it("gives a span the lowest free lane once an earlier overlap closes", () => { + const diagram = parseSequenceDiagram({ + id: "lanes", + title: "Lanes", + type: "sequence", + participants: [ + { id: "a", label: "A" }, + { id: "b", label: "B" }, + { id: "c", label: "C" }, + ], + messages: [ + { id: "q1", source: "a", target: "b", label: "Query 1" }, + { id: "q2", source: "c", target: "b", label: "Query 2" }, + { id: "p1", source: "b", target: "a", label: "Reply 1", type: "return" }, + { id: "q3", source: "a", target: "b", label: "Query 3" }, + { id: "p2", source: "b", target: "c", label: "Reply 2", type: "return" }, + { id: "p3", source: "b", target: "a", label: "Reply 3", type: "return" }, + ], + }); + expect( + sequenceActivations(diagram).map(({ callMessageId, startIndex, endIndex, depth }) => [ + callMessageId, + startIndex, + endIndex, + depth, + ]), + ).toEqual([ + ["q1", 0, 2, 0], + ["q2", 1, 4, 1], + ["q3", 3, 5, 0], + ]); + }); + + it("never stacks overlapping spans on one lane and keeps nested spans inside", () => { + // Deterministic pseudo-random diagrams: calls, returns to open calls, and strays. + let seed = 0x5eed; + const random = (limit: number) => { + seed = (seed * 1_103_515_245 + 12_345) % 2_147_483_648; + return seed % limit; + }; + const ids = ["a", "b", "c"]; + let overlaps = 0; + for (let run = 0; run < 500; run += 1) { + const open: { caller: string; callee: string }[] = []; + const messages = Array.from({ length: 6 + random(24) }, (_, index) => { + const roll = random(10); + const returned = + open.length > 0 && roll < 5 ? open.splice(random(open.length), 1)[0] : undefined; + if (returned) { + return { + id: `m${index}`, + source: returned.callee, + target: returned.caller, + label: "Reply", + type: "return" as const, + }; + } + const source = ids[random(ids.length)] ?? "a"; + const others = ids.filter((id) => id !== source); + const target = others[random(others.length)] ?? "b"; + if (roll < 9) open.push({ caller: source, callee: target }); + return { + id: `m${index}`, + source, + target, + label: roll < 9 ? "Call" : "Stray reply", + ...(roll < 9 ? {} : { type: "return" as const }), + }; + }); + const spans = sequenceActivations( + parseSequenceDiagram({ + id: `random-${run}`, + title: "Random", + type: "sequence", + participants: ids.map((id) => ({ id, label: id.toUpperCase() })), + messages, + }), + ); + for (const left of spans) { + for (const right of spans) { + if (left === right || left.participantId !== right.participantId) continue; + const overlap = left.startIndex <= right.endIndex && right.startIndex <= left.endIndex; + if (overlap) { + overlaps += 1; + expect(left.depth, `run ${run}`).not.toBe(right.depth); + } + if (left.startIndex < right.startIndex && right.endIndex < left.endIndex) { + expect(right.depth, `run ${run}`).toBeGreaterThan(left.depth); + } + } + } + } + // The generator must actually produce overlapping spans to check. + expect(overlaps).toBeGreaterThan(100); + }); + + it("reserves every id under a participant's lifeline, including activation bars", () => { + expect( + issueCodes({ + ...sequenceFixture, + participants: [ + { id: "api", label: "API" }, + { id: "api:lifeline:activation:call", label: "Shadow" }, + ], + messages: [], + }), + ).toEqual([["lifeline_id_collision", "participants.[1].id"]]); + }); }); diff --git a/packages/diagram/core/src/types/sequence.ts b/packages/diagram/core/src/types/sequence.ts index 072dbf29..143b1268 100644 --- a/packages/diagram/core/src/types/sequence.ts +++ b/packages/diagram/core/src/types/sequence.ts @@ -15,6 +15,11 @@ export function sequenceLifelineId(participantId: string): string { return `${participantId}${SEQUENCE_LIFELINE_SUFFIX}`; } +/** Activation bars sit on their participant's lifeline, keyed by the call that opens them. */ +export function sequenceActivationId(participantId: string, callMessageId: string): string { + return `${sequenceLifelineId(participantId)}:activation:${callMessageId}`; +} + export const SequenceMessageTypeSchema = Schema.Literals(SEQUENCE_MESSAGE_TYPES); export const SequenceMessageStyleSchema = Schema.Literals(SEQUENCE_MESSAGE_STYLES); export type SequenceMessageType = typeof SequenceMessageTypeSchema.Type; @@ -97,17 +102,18 @@ export function getSequenceValidationIssues(diagram: SequenceDiagram): SequenceV participantIndexById.set(participant.id, index); }); + // Lifelines and their activation bars are addressed under `:lifeline`. for (const participant of diagram.participants) { const lifelineId = sequenceLifelineId(participant.id); - const collisionIndex = participantIndexById.get(lifelineId); - if (collisionIndex !== undefined) { + diagram.participants.forEach((candidate, collisionIndex) => { + if (candidate.id !== lifelineId && !candidate.id.startsWith(`${lifelineId}:`)) return; issues.push({ code: "lifeline_id_collision", path: `participants.[${collisionIndex}].id`, - message: `Participant id "${lifelineId}" collides with the generated lifeline for "${participant.id}".`, - hint: `Rename the participant so its id does not equal another participant id followed by ${SEQUENCE_LIFELINE_SUFFIX}.`, + message: `Participant id "${candidate.id}" collides with the generated lifeline for "${participant.id}".`, + hint: `Rename the participant so its id does not start with another participant id followed by ${SEQUENCE_LIFELINE_SUFFIX}.`, }); - } + }); } const messageIds = new Set(); @@ -149,6 +155,77 @@ export function getSequenceValidationIssues(diagram: SequenceDiagram): SequenceV return issues; } +/** + * A participant's active period: from a call it receives to its return to the + * caller. Spans are derived from message order, never authored, and stack when + * the participant is called again before it returns: `depth` is the lowest + * lane no overlapping span on the same lifeline uses, so overlapping spans + * never share a depth and nested spans keep their nesting order. + */ +export interface SequenceActivation { + readonly participantId: string; + readonly callMessageId: string; + readonly returnMessageId: string; + /** Index of the call in `messages`. */ + readonly startIndex: number; + /** Index of the matching return in `messages`. */ + readonly endIndex: number; + readonly depth: number; +} + +/** + * Pair each return with the latest unanswered call from its target to its + * source. Calls nobody answers (fire-and-forget) and returns without an open + * call open no span; answering one call leaves every other open call open. + */ +export function sequenceActivations(diagram: SequenceDiagram): SequenceActivation[] { + const open = new Map(); + const spans: Omit[] = []; + diagram.messages.forEach((message, index) => { + if (message.type !== "return") { + open.set(message.target, [ + ...(open.get(message.target) ?? []), + { caller: message.source, index, messageId: message.id }, + ]); + return; + } + const calls = open.get(message.source) ?? []; + const matched = calls.findLastIndex((call) => call.caller === message.target); + const call = calls[matched]; + if (!call) return; + // Other calls stay open, even when they interleave with this one. + open.set( + message.source, + calls.filter((_, callIndex) => callIndex !== matched), + ); + spans.push({ + participantId: message.source, + callMessageId: call.messageId, + returnMessageId: message.id, + startIndex: call.index, + endIndex: index, + }); + }); + // Interval lane assignment. In start order, each span takes the lowest depth + // that no still-open span on its lifeline holds and that sits above every + // span enclosing it, so overlapping spans never share a depth and nested + // spans always draw inside their enclosing span. + const laid: SequenceActivation[] = []; + for (const span of spans.toSorted((left, right) => left.startIndex - right.startIndex)) { + const open = laid.filter( + (other) => other.participantId === span.participantId && other.endIndex > span.startIndex, + ); + const taken = new Set(open.map((other) => other.depth)); + let depth = Math.max( + 0, + ...open.filter((other) => other.endIndex > span.endIndex).map((other) => other.depth + 1), + ); + while (taken.has(depth)) depth += 1; + laid.push({ ...span, depth }); + } + return laid; +} + export class SequenceValidationError extends DiagramValidationError { constructor(readonly issues: readonly SequenceValidationIssue[]) { super(issues[0]?.message ?? "Sequence diagram failed semantic validation."); diff --git a/packages/diagram/excalidraw/src/lib/convert.test.ts b/packages/diagram/excalidraw/src/lib/convert.test.ts index 7e682f7e..9ec4c1c8 100644 --- a/packages/diagram/excalidraw/src/lib/convert.test.ts +++ b/packages/diagram/excalidraw/src/lib/convert.test.ts @@ -3,10 +3,13 @@ import { describe, expect, it } from "vitest"; import { generateKeyBetween } from "fractional-indexing"; import { + apiRequestSequence, flowchartFixture, mindmapFixture, pharmaBatchDispositionFlowchart, parseFlowchartDiagram, + parseSequenceDiagram, + sequenceFixture, type CanvasSpec, getCanvasValidationIssues, } from "@sketchi/diagram-core"; @@ -1766,6 +1769,52 @@ describe("convertSceneToExcalidraw", () => { "customData.sketchiRendererRole", ); }); + + it("exports activation bars as unlabeled bound shapes that other messages may cross", () => { + const nested = parseSequenceDiagram({ + id: "oauth", + title: "OAuth callback", + type: "sequence", + participants: [ + { id: "client", label: "Client" }, + { id: "api", label: "API" }, + { id: "auth", label: "Auth" }, + ], + messages: [ + { id: "login", source: "client", target: "api", label: "Log in" }, + { id: "authorize", source: "api", target: "auth", label: "Authorize" }, + { id: "callback", source: "auth", target: "api", label: "Callback" }, + { id: "ack", source: "api", target: "auth", label: "Ack", type: "return" }, + { id: "token", source: "auth", target: "api", label: "Token", type: "return" }, + { id: "direct", source: "auth", target: "client", label: "Direct notice" }, + { id: "session", source: "api", target: "client", label: "Session", type: "return" }, + ], + }); + for (const diagram of [sequenceFixture, apiRequestSequence, nested]) { + const scene = convertSceneToExcalidraw(renderSequenceDiagram(diagram)); + expect(validateExcalidrawScene(scene), diagram.id).toEqual({ ok: true, issues: [] }); + } + + const scene = convertSceneToExcalidraw(renderSequenceDiagram(sequenceFixture)); + const bar = scene.elements.find( + (element) => element.id === "node:store:lifeline:activation:checkout", + ); + expect(bar).toMatchObject({ + type: "rectangle", + customData: { sketchiRendererRole: "sequence-activation" }, + boundElements: expect.arrayContaining([ + { id: "arrow:checkout", type: "arrow" }, + { id: "arrow:receipt", type: "arrow" }, + ]), + }); + expect( + scene.elements.some( + (element) => + element.type === "text" && + element.containerId === "node:store:lifeline:activation:checkout", + ), + ).toBe(false); + }); }); describe("bound label first paint", () => { diff --git a/packages/diagram/excalidraw/src/lib/convert.ts b/packages/diagram/excalidraw/src/lib/convert.ts index c7305b74..1af24bb7 100644 --- a/packages/diagram/excalidraw/src/lib/convert.ts +++ b/packages/diagram/excalidraw/src/lib/convert.ts @@ -1,5 +1,4 @@ import { - SEQUENCE_LIFELINE_ROLE, type ArrowSceneElement, type FrameSceneElement, type LineSceneElement, @@ -11,6 +10,7 @@ import { import { AXIS_ALIGNED_EPSILON, boundLabelWidth, + CANVAS_RENDERER_ROLES, canvasBoundTextBox, canvasBoundTextInset, canvasNodeIconBox, @@ -363,8 +363,8 @@ function shapeElement(input: { boundElements: boundElements.length > 0 ? boundElements : null, roundness: shapeType === "rectangle" ? { type: 3 } : null, strokeColor: input.shape.strokeColor ?? input.scene.accentColor, - ...(input.shape.rendererRole === SEQUENCE_LIFELINE_ROLE - ? { customData: { sketchiRendererRole: SEQUENCE_LIFELINE_ROLE } } + ...(input.shape.rendererRole + ? { customData: { sketchiRendererRole: input.shape.rendererRole } } : {}), }; } @@ -606,7 +606,7 @@ function synthesizeNodeLabels(sourceElements: readonly SceneElement[]): { textElements.flatMap((element) => (element.containerId ? [element.containerId] : [])), ); for (const node of nodes) { - if (explicitlyLabeledNodeIds.has(node.id) || node.rendererRole === SEQUENCE_LIFELINE_ROLE) { + if (explicitlyLabeledNodeIds.has(node.id) || node.rendererRole) { continue; } const baseId = `__sketchi_node_label__${node.id}`; @@ -978,7 +978,7 @@ function arrowSegmentsThroughShapes( for (const segment of segments) { for (const shape of shapes) { if ( - isSequenceLifelineShape(shape) || + isSequenceStructureShape(shape) || shape.id === segment.startBindingId || shape.id === segment.endBindingId ) { @@ -1007,13 +1007,12 @@ function arrowSegmentsThroughShapes( return issues; } -function isSequenceLifelineShape(element: ExcalidrawElement): boolean { +/** Messages may cross other participants' lifelines and activation bars. */ +function isSequenceStructureShape(element: ExcalidrawElement): boolean { const customData = element.customData; - return ( - customData !== null && - typeof customData === "object" && - (customData as Record)["sketchiRendererRole"] === SEQUENCE_LIFELINE_ROLE - ); + if (customData === null || typeof customData !== "object") return false; + const role = (customData as Record)["sketchiRendererRole"]; + return CANVAS_RENDERER_ROLES.some((candidate) => candidate === role); } function arrowEndpoint(element: ExcalidrawElement, key: BindingKey) { diff --git a/packages/diagram/generation/src/lib/messages.ts b/packages/diagram/generation/src/lib/messages.ts index c0ce492f..018a0d4e 100644 --- a/packages/diagram/generation/src/lib/messages.ts +++ b/packages/diagram/generation/src/lib/messages.ts @@ -67,6 +67,7 @@ const SEQUENCE_IR_INSTRUCTIONS = [ "Return ordered participants with stable ids and human labels.", "Return chronological messages with stable ids, participant source and target ids, and concise labels.", 'Use message type "message" for calls and "return" for responses; dashed style is appropriate for responses.', + "Answer a call with a later return from the callee back to the caller; Sketchi draws the callee's activation bar from the call to its return. Leave fire-and-forget events unanswered.", "Every message source and target must reference a participant, and self-messages are not supported.", ]; diff --git a/packages/diagram/renderer/src/diagram-types/sequence.test.ts b/packages/diagram/renderer/src/diagram-types/sequence.test.ts index 5edf63ff..83e10658 100644 --- a/packages/diagram/renderer/src/diagram-types/sequence.test.ts +++ b/packages/diagram/renderer/src/diagram-types/sequence.test.ts @@ -2,13 +2,34 @@ import { describe, expect, it } from "vitest"; import { type SequenceDiagram, SequenceValidationError, + apiRequestSequence, getCanvasValidationIssues, parseSequenceDiagram, sequenceFixture, } from "@sketchi/diagram-core"; import { renderDiagram } from "../diagram"; -import { renderSequenceDiagram } from "../sequence"; +import type { NodeSceneElement, RenderedDiagramScene } from "../scene"; +import { + SEQUENCE_ACTIVATION_ROLE, + isStructurallyValidSequenceActivation, + renderSequenceDiagram, +} from "../sequence"; + +function activationBars(scene: RenderedDiagramScene): NodeSceneElement[] { + return scene.elements.filter( + (element): element is NodeSceneElement => + element.type === "node" && element.rendererRole === SEQUENCE_ACTIVATION_ROLE, + ); +} + +function arrow(scene: RenderedDiagramScene, edgeId: string) { + const found = scene.elements.find( + (element) => element.type === "arrow" && element.edgeId === edgeId, + ); + if (found?.type !== "arrow") throw new Error(`Missing arrow ${edgeId}.`); + return found; +} const sequence = parseSequenceDiagram({ id: "checkout-sequence", @@ -111,4 +132,169 @@ describe("sequence diagram renderer", () => { ), ).toThrow(/collides with the generated lifeline/); }); + + it("draws an activation bar from each answered call to its return", () => { + const scene = renderSequenceDiagram(sequenceFixture); + const bars = activationBars(scene); + expect(bars.map((bar) => bar.nodeId)).toEqual([ + "store:lifeline:activation:checkout", + "payments:lifeline:activation:charge", + ]); + const [store, payments] = bars; + if (!store || !payments) throw new Error("Missing activation bars."); + + // The call lands on the bar's near edge and the return leaves from it. + const checkout = arrow(scene, "checkout"); + expect(checkout.targetNodeId).toBe(store.nodeId); + expect(checkout.points.at(-1)?.x).toBe(store.x); + const receipt = arrow(scene, "receipt"); + expect(receipt.sourceNodeId).toBe(store.nodeId); + expect(receipt.points[0].x).toBe(store.x); + const charge = arrow(scene, "charge"); + expect([charge.sourceNodeId, charge.targetNodeId]).toEqual([store.nodeId, payments.nodeId]); + expect(charge.points[0].x).toBe(store.x + store.width); + for (const [bar, call, ret] of [ + [store, checkout, receipt], + [payments, charge, arrow(scene, "charged")], + ] as const) { + expect(bar.y).toBeLessThan(call.points[0].y); + expect(bar.y + bar.height).toBeGreaterThan(ret.points[0].y); + expect(isStructurallyValidSequenceActivation(scene, bar)).toBe(true); + } + expect(scene.zOrder.indexOf(store.id)).toBeLessThan(scene.zOrder.indexOf(checkout.id)); + expect(getCanvasValidationIssues(scene)).toEqual([]); + }); + + it("keeps unanswered calls on the lifeline", () => { + const scene = renderSequenceDiagram(apiRequestSequence); + expect(arrow(scene, "track").targetNodeId).toBe("analytics:lifeline"); + expect(activationBars(scene)).toHaveLength(3); + expect(getCanvasValidationIssues(scene)).toEqual([]); + }); + + it("offsets a re-entrant activation and attaches messages to the innermost bar", () => { + const scene = renderSequenceDiagram( + parseSequenceDiagram({ + id: "oauth", + title: "OAuth callback", + type: "sequence", + participants: [ + { id: "client", label: "Client" }, + { id: "api", label: "API" }, + { id: "auth", label: "Auth" }, + ], + messages: [ + { id: "login", source: "client", target: "api", label: "Log in" }, + { id: "authorize", source: "api", target: "auth", label: "Authorize" }, + { id: "callback", source: "auth", target: "api", label: "Callback" }, + { id: "ack", source: "api", target: "auth", label: "Ack", type: "return" }, + { id: "token", source: "auth", target: "api", label: "Token", type: "return" }, + { id: "session", source: "api", target: "client", label: "Session", type: "return" }, + ], + }), + ); + const outer = activationBars(scene).find( + (bar) => bar.nodeId === "api:lifeline:activation:login", + ); + const inner = activationBars(scene).find( + (bar) => bar.nodeId === "api:lifeline:activation:callback", + ); + if (!outer || !inner) throw new Error("Missing api activation bars."); + expect(inner.x - outer.x).toBe(outer.width / 2); + expect(inner.y).toBeGreaterThan(outer.y); + expect(inner.y + inner.height).toBeLessThan(outer.y + outer.height); + expect(arrow(scene, "callback").targetNodeId).toBe(inner.nodeId); + expect(arrow(scene, "ack").sourceNodeId).toBe(inner.nodeId); + expect(arrow(scene, "token").targetNodeId).toBe(outer.nodeId); + expect(getCanvasValidationIssues(scene)).toEqual([]); + }); + + it("draws partially overlapping spans in separate lanes and attaches each message to its span", () => { + const scene = renderSequenceDiagram( + parseSequenceDiagram({ + id: "lanes", + title: "Lanes", + type: "sequence", + participants: [ + { id: "a", label: "A" }, + { id: "b", label: "B" }, + { id: "c", label: "C" }, + ], + messages: [ + { id: "q1", source: "a", target: "b", label: "Query 1" }, + { id: "q2", source: "c", target: "b", label: "Query 2" }, + { id: "p1", source: "b", target: "a", label: "Reply 1", type: "return" }, + { id: "q3", source: "a", target: "b", label: "Query 3" }, + { id: "p2", source: "b", target: "c", label: "Reply 2", type: "return" }, + { id: "p3", source: "b", target: "a", label: "Reply 3", type: "return" }, + ], + }), + ); + const bar = (call: string) => { + const found = activationBars(scene).find( + (candidate) => candidate.nodeId === `b:lifeline:activation:${call}`, + ); + if (!found) throw new Error(`Missing bar for ${call}.`); + return found; + }; + expect(bar("q2").x).not.toBe(bar("q3").x); + expect(bar("q3").x).toBe(bar("q1").x); + expect(arrow(scene, "q3").targetNodeId).toBe(bar("q3").nodeId); + expect(arrow(scene, "p2").sourceNodeId).toBe(bar("q2").nodeId); + expect(arrow(scene, "p3").sourceNodeId).toBe(bar("q3").nodeId); + for (const candidate of activationBars(scene)) { + expect(isStructurallyValidSequenceActivation(scene, candidate)).toBe(true); + } + expect(getCanvasValidationIssues(scene)).toEqual([]); + }); + + it("does not keep the activation role for a bar moved off its lifeline", () => { + const scene = renderSequenceDiagram(sequenceFixture); + const [bar] = activationBars(scene); + if (!bar) throw new Error("Missing activation bar."); + expect(isStructurallyValidSequenceActivation(scene, { ...bar, x: bar.x + 200 })).toBe(false); + expect(isStructurallyValidSequenceActivation(scene, { ...bar, width: bar.width + 1 })).toBe( + false, + ); + }); + + it("keeps the activation role for bars nested two levels deep", () => { + const scene = renderSequenceDiagram( + parseSequenceDiagram({ + id: "deep", + title: "Deep nesting", + type: "sequence", + participants: [ + { id: "a", label: "A" }, + { id: "b", label: "B" }, + { id: "c", label: "C" }, + ], + messages: [ + { id: "q1", source: "a", target: "b", label: "First" }, + { id: "q2", source: "c", target: "b", label: "Second" }, + { id: "q3", source: "a", target: "b", label: "Third" }, + { id: "r3", source: "b", target: "a", label: "Third done", type: "return" }, + { id: "r2", source: "b", target: "c", label: "Second done", type: "return" }, + { id: "r1", source: "b", target: "a", label: "First done", type: "return" }, + ], + }), + ); + const bars = activationBars(scene); + expect(bars.map((bar) => bar.nodeId)).toEqual([ + "b:lifeline:activation:q1", + "b:lifeline:activation:q2", + "b:lifeline:activation:q3", + ]); + expect(bars.map((bar) => bar.x - (bars[0]?.x ?? 0))).toEqual([0, 6, 12]); + for (const bar of bars) { + expect(isStructurallyValidSequenceActivation(scene, bar), bar.nodeId).toBe(true); + } + const [, , deepest] = bars; + if (!deepest) throw new Error("Missing the deepest bar."); + // A whole nesting step past the bars this lifeline has is not a bar. + expect(isStructurallyValidSequenceActivation(scene, { ...deepest, x: deepest.x + 6 })).toBe( + false, + ); + expect(getCanvasValidationIssues(scene)).toEqual([]); + }); }); diff --git a/packages/diagram/renderer/src/sequence.ts b/packages/diagram/renderer/src/sequence.ts index cec7f8ce..deb61722 100644 --- a/packages/diagram/renderer/src/sequence.ts +++ b/packages/diagram/renderer/src/sequence.ts @@ -1,7 +1,10 @@ import { CANVAS_SPEC_VERSION, SEQUENCE_LIFELINE_SUFFIX, + type SequenceActivation, type SequenceDiagram, + sequenceActivationId, + sequenceActivations, sequenceLifelineId, validateSequenceDiagram, wrapTextToWidth, @@ -26,8 +29,14 @@ const LABEL_FONT_SIZE = 14; const LABEL_LINE_HEIGHT = 1.35; const LABEL_VERTICAL_PADDING = 18; const LAYOUT_ALIGNMENT_EPSILON = 0.01; +const ACTIVATION_WIDTH = 12; +/** Each nested activation shifts right by half a bar, as in UML. */ +const ACTIVATION_NEST_OFFSET = ACTIVATION_WIDTH / 2; +/** Bars reach past their call and return rows so both arrows land inside them. */ +const ACTIVATION_OVERHANG = 12; export const SEQUENCE_LIFELINE_ROLE = "sequence-lifeline"; +export const SEQUENCE_ACTIVATION_ROLE = "sequence-activation"; interface SequenceLifelineStructureNode { readonly type: "node"; @@ -85,9 +94,56 @@ export function isStructurallyValidSequenceLifeline( ); } +/** + * An activation bar keeps its renderer role only while it still reads as one: + * a bar of the renderer's width on its own lifeline, offset by whole nesting + * steps and by no more steps than that lifeline has bars. + */ +export function isStructurallyValidSequenceActivation( + scene: SequenceLifelineStructureScene, + element: SequenceLifelineStructureNode, +): boolean { + const marker = `${SEQUENCE_LIFELINE_SUFFIX}:activation:`; + const markerIndex = element.nodeId.indexOf(marker); + if ( + markerIndex <= 0 || + markerIndex + marker.length === element.nodeId.length || + element.id !== `node:${element.nodeId}` || + element.shape !== "rectangle" || + element.width !== ACTIVATION_WIDTH + ) { + return false; + } + const lifelineNodeId = element.nodeId.slice(0, markerIndex + SEQUENCE_LIFELINE_SUFFIX.length); + const lifeline = scene.elements.find( + (candidate): candidate is SequenceLifelineStructureNode => + candidate.type === "node" && + candidate.id === `node:${lifelineNodeId}` && + candidate.nodeId === lifelineNodeId, + ); + if (!lifeline || !isStructurallyValidSequenceLifeline(scene, lifeline)) { + return false; + } + const lifelineCenter = lifeline.x + lifeline.width / 2; + const depth = (element.x - (lifelineCenter - ACTIVATION_WIDTH / 2)) / ACTIVATION_NEST_OFFSET; + const barsOnLifeline = scene.elements.filter( + (candidate) => + candidate.type === "node" && candidate.nodeId.startsWith(`${lifelineNodeId}:activation:`), + ).length; + return ( + depth >= -LAYOUT_ALIGNMENT_EPSILON && + Math.abs(depth - Math.round(depth)) <= LAYOUT_ALIGNMENT_EPSILON && + Math.round(depth) < barsOnLifeline && + element.y >= lifeline.y - LAYOUT_ALIGNMENT_EPSILON && + element.y + element.height <= lifeline.y + lifeline.height + LAYOUT_ALIGNMENT_EPSILON + ); +} + /** * Render a canonical sequence diagram: participants become ordered header - * columns with lifelines, and messages become rows in chronological order. + * columns with lifelines, messages become rows in chronological order, and + * each answered call draws an activation bar on the called lifeline. Messages + * attach to the innermost bar active on their row, otherwise to the lifeline. */ export function renderSequenceDiagram(diagram: SequenceDiagram): RenderedDiagramScene { const input = validateSequenceDiagram(diagram); @@ -163,33 +219,102 @@ export function renderSequenceDiagram(diagram: SequenceDiagram): RenderedDiagram }); }); + const rowY = (index: number) => firstMessageY + index * MESSAGE_GAP; + const labelById = new Map( + input.participants.map((participant) => [participant.id, participant.label]), + ); + const bars = sequenceActivations(input).map( + (activation): { readonly activation: SequenceActivation; readonly node: NodeSceneElement } => { + const nodeId = sequenceActivationId(activation.participantId, activation.callMessageId); + const centerX = centerXByParticipant.get(activation.participantId) ?? 0; + const top = rowY(activation.startIndex) - ACTIVATION_OVERHANG; + return { + activation, + node: { + type: "node", + id: `node:${nodeId}`, + nodeId, + rendererRole: SEQUENCE_ACTIVATION_ROLE, + shape: "rectangle", + fillColor: input.style.backgroundColor, + strokeColor: input.style.accentColor, + x: centerX - ACTIVATION_WIDTH / 2 + activation.depth * ACTIVATION_NEST_OFFSET, + y: top, + width: ACTIVATION_WIDTH, + height: rowY(activation.endIndex) + ACTIVATION_OVERHANG - top, + label: `${labelById.get(activation.participantId) ?? activation.participantId} active`, + }, + }; + }, + ); + /** + * Where a message meets a participant: the bar of the span it opens or + * closes, otherwise the innermost bar active on its row (overlapping bars + * never share a depth), otherwise the lifeline. + */ + const attachment = (participantId: string, index: number, messageId: string, towardX: number) => { + const centerX = centerXByParticipant.get(participantId) ?? 0; + const rightward = towardX > centerX; + const active = bars.filter( + ({ activation }) => + activation.participantId === participantId && + activation.startIndex <= index && + index <= activation.endIndex, + ); + const bar = + active.find( + ({ activation }) => + activation.callMessageId === messageId || activation.returnMessageId === messageId, + ) ?? + active.reduce<(typeof bars)[number] | undefined>( + (innermost, candidate) => + !innermost || candidate.activation.depth > innermost.activation.depth + ? candidate + : innermost, + undefined, + ); + if (!bar) { + return { + nodeId: sequenceLifelineId(participantId), + x: centerX + ((rightward ? 1 : -1) * LIFELINE_WIDTH) / 2, + }; + } + return { + nodeId: bar.node.nodeId, + x: rightward ? bar.node.x + bar.node.width : bar.node.x, + }; + }; + const messageArrows: ArrowSceneElement[] = input.messages.map((message, index) => { // Validation guarantees both participants exist and differ. - const sourceX = centerXByParticipant.get(message.source) ?? 0; - const targetX = centerXByParticipant.get(message.target) ?? 0; - const y = firstMessageY + index * MESSAGE_GAP; - const direction = targetX > sourceX ? 1 : -1; + const sourceCenter = centerXByParticipant.get(message.source) ?? 0; + const targetCenter = centerXByParticipant.get(message.target) ?? 0; + const source = attachment(message.source, index, message.id, targetCenter); + const target = attachment(message.target, index, message.id, sourceCenter); + const y = rowY(index); return { type: "arrow", id: `arrow:${message.id}`, edgeId: message.id, - sourceNodeId: sequenceLifelineId(message.source), - targetNodeId: sequenceLifelineId(message.target), + sourceNodeId: source.nodeId, + targetNodeId: target.nodeId, ...(message.type === "return" ? { strokeColor: "#6b7280" } : {}), ...(message.style === "dashed" || message.type === "return" ? { strokeStyle: "dashed" as const } : {}), points: [ - { x: sourceX + (direction * LIFELINE_WIDTH) / 2, y }, - { x: targetX - (direction * LIFELINE_WIDTH) / 2, y }, + { x: source.x, y }, + { x: target.x, y }, ], label: message.label, }; }); - const elements = [...messageArrows, ...lifelines, ...headers, ...headerLabels]; + const activationBars = bars.map(({ node }) => node); + const elements = [...messageArrows, ...lifelines, ...activationBars, ...headers, ...headerLabels]; const zOrder = [ ...lifelines.map((element) => element.id), + ...activationBars.map((element) => element.id), ...headers.map((element) => element.id), ...headerLabels.map((element) => element.id), ...messageArrows.map((element) => element.id),