@bovinphang/craft-common 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -2
  3. package/dist/content/catalog.d.ts +4 -0
  4. package/dist/content/resources.d.ts +2 -0
  5. package/dist/content/types.d.ts +21 -0
  6. package/dist/index.d.ts +7 -0
  7. package/dist/index.js +668 -0
  8. package/dist/install/identity.d.ts +5 -0
  9. package/dist/install/planner.d.ts +6 -0
  10. package/dist/install/transaction.d.ts +7 -0
  11. package/dist/install/types.d.ts +44 -0
  12. package/docs/consuming-common.md +33 -0
  13. package/localized/zh-CN/methods/debug.md +13 -0
  14. package/localized/zh-CN/methods/doc-sync.md +12 -0
  15. package/localized/zh-CN/methods/refactoring.md +16 -0
  16. package/localized/zh-CN/methods/source-driven-development.md +19 -0
  17. package/localized/zh-CN/methods/tdd.md +13 -0
  18. package/localized/zh-CN/skills/cc-debug-methodology/SKILL.md +8 -0
  19. package/localized/zh-CN/skills/cc-debug-methodology/references/method.md +13 -0
  20. package/localized/zh-CN/skills/cc-doc-sync-core/SKILL.md +8 -0
  21. package/localized/zh-CN/skills/cc-doc-sync-core/references/method.md +12 -0
  22. package/localized/zh-CN/skills/cc-drawio-studio/SKILL.md +73 -0
  23. package/localized/zh-CN/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
  24. package/localized/zh-CN/skills/cc-drawio-studio/data/brand-icons.json +878 -0
  25. package/localized/zh-CN/skills/cc-drawio-studio/data/shape-index.json +83570 -0
  26. package/localized/zh-CN/skills/cc-drawio-studio/references/data-residency.md +27 -0
  27. package/localized/zh-CN/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
  28. package/localized/zh-CN/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
  29. package/localized/zh-CN/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
  30. package/localized/zh-CN/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
  31. package/localized/zh-CN/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
  32. package/localized/zh-CN/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
  33. package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
  34. package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
  35. package/localized/zh-CN/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
  36. package/localized/zh-CN/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
  37. package/localized/zh-CN/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
  38. package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
  39. package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
  40. package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
  41. package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
  42. package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
  43. package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
  44. package/localized/zh-CN/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
  45. package/localized/zh-CN/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
  46. package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
  47. package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
  48. package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
  49. package/localized/zh-CN/skills/cc-drawio-studio/styles/schema.json +112 -0
  50. package/localized/zh-CN/skills/cc-refactoring-core/SKILL.md +8 -0
  51. package/localized/zh-CN/skills/cc-refactoring-core/references/method.md +16 -0
  52. package/localized/zh-CN/skills/cc-source-driven-development/SKILL.md +8 -0
  53. package/localized/zh-CN/skills/cc-source-driven-development/references/method.md +19 -0
  54. package/localized/zh-CN/skills/cc-tdd-methodology/SKILL.md +8 -0
  55. package/localized/zh-CN/skills/cc-tdd-methodology/references/method.md +13 -0
  56. package/localized/zh-CN/skills/cc-technical-diagrams/SKILL.md +34 -0
  57. package/localized/zh-CN/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
  58. package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
  59. package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
  60. package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
  61. package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
  62. package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
  63. package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
  64. package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
  65. package/localized/zh-CN/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
  66. package/localized/zh-CN/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
  67. package/localized/zh-CN/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
  68. package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
  69. package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
  70. package/localized/zh-CN/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
  71. package/localized/zh-CN/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
  72. package/localized/zh-CN/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
  73. package/localized/zh-CN/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
  74. package/localized/zh-CN/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
  75. package/localized/zh-CN/skills/eval_queries.json +160 -0
  76. package/localized/zh-CN/skills/metadata.json +223 -0
  77. package/localized/zh-CN/skills/relations.json +53 -0
  78. package/methods/debug.md +13 -0
  79. package/methods/doc-sync.md +12 -0
  80. package/methods/refactoring.md +16 -0
  81. package/methods/source-driven-development.md +19 -0
  82. package/methods/tdd.md +13 -0
  83. package/package.json +51 -4
  84. package/skills/cc-debug-methodology/SKILL.md +8 -0
  85. package/skills/cc-debug-methodology/references/method.md +13 -0
  86. package/skills/cc-doc-sync-core/SKILL.md +8 -0
  87. package/skills/cc-doc-sync-core/references/method.md +12 -0
  88. package/skills/cc-drawio-studio/SKILL.md +73 -0
  89. package/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
  90. package/skills/cc-drawio-studio/data/brand-icons.json +878 -0
  91. package/skills/cc-drawio-studio/data/shape-index.json +83570 -0
  92. package/skills/cc-drawio-studio/references/data-residency.md +27 -0
  93. package/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
  94. package/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
  95. package/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
  96. package/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
  97. package/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
  98. package/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
  99. package/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
  100. package/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
  101. package/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
  102. package/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
  103. package/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
  104. package/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
  105. package/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
  106. package/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
  107. package/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
  108. package/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
  109. package/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
  110. package/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
  111. package/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
  112. package/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
  113. package/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
  114. package/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
  115. package/skills/cc-drawio-studio/styles/schema.json +112 -0
  116. package/skills/cc-refactoring-core/SKILL.md +8 -0
  117. package/skills/cc-refactoring-core/references/method.md +16 -0
  118. package/skills/cc-source-driven-development/SKILL.md +8 -0
  119. package/skills/cc-source-driven-development/references/method.md +19 -0
  120. package/skills/cc-tdd-methodology/SKILL.md +8 -0
  121. package/skills/cc-tdd-methodology/references/method.md +13 -0
  122. package/skills/cc-technical-diagrams/SKILL.md +34 -0
  123. package/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
  124. package/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
  125. package/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
  126. package/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
  127. package/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
  128. package/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
  129. package/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
  130. package/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
  131. package/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
  132. package/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
  133. package/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
  134. package/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
  135. package/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
  136. package/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
  137. package/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
  138. package/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
  139. package/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
  140. package/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
  141. package/skills/eval_queries.json +160 -0
  142. package/skills/metadata.json +223 -0
  143. package/skills/relations.json +53 -0
@@ -0,0 +1,1537 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { layoutText, fitNodeText, routeConnector, placeEdgeLabel, overlaps } from "./diagram-layout.mjs";
7
+
8
+ /** @type {import("./diagram-layout.mjs").QualityIssue[]} */
9
+ const qualityIssues = [];
10
+ /** @type {Array<{id:string,owner:string,kind:string,text:string,lines:string[],x:number,y:number,width:number,height:number}>} */
11
+ const textBounds = [];
12
+ /** @type {Box[]} */
13
+ let routeObstacles = [];
14
+ /** @type {Array<Array<[number,number]>>} */
15
+ const occupiedRoutes = [];
16
+ /** @type {Array<{id:string,label:string,points:Array<[number,number]>}>} */
17
+ const deferredLabels=[];
18
+ let finalCanvas = { width: 0, height: 0 };
19
+
20
+ const ALLOWED_TYPES = new Set(["workflow", "sequence", "dataflow", "lifecycle", "architecture"]);
21
+ const NODE_TYPES = new Set([
22
+ "frontend", "backend", "database", "cloud", "security", "messagebus", "external", "active", "waiting", "decision", "success", "failure", "neutral", "start",
23
+ "agent", "model", "memory", "vectorstore", "graphdb", "tool", "document", "queue", "browser", "user", "gateway",
24
+ ]);
25
+ const EDGE_VARIANTS = new Set(["default", "emphasis", "security", "dashed", "return"]);
26
+ const VISUAL_STYLES = new Set(["default", "terminal", "blueprint", "minimal", "glass", "warm", "openai", "editorial-dark"]);
27
+ const FLOW_TYPES = new Set(["control", "data", "read", "write", "async", "feedback"]);
28
+
29
+ const args = parseArgs(process.argv.slice(2));
30
+
31
+ try {
32
+ const inputPath = requireArg(args, "input");
33
+ const outputPath = requireArg(args, "output");
34
+ const type = requireArg(args, "type");
35
+ const format = args.format ?? "markdown";
36
+
37
+ if (!ALLOWED_TYPES.has(type)) {
38
+ throw usageError("type", `Unsupported diagram type "${type}". Expected workflow, sequence, dataflow, lifecycle, or architecture.`);
39
+ }
40
+ if (format !== "json" && format !== "markdown") {
41
+ throw usageError("format", `Unsupported report format "${format}". Expected json or markdown.`);
42
+ }
43
+
44
+ const diagram = readJson(inputPath);
45
+ validateCommon(diagram, type);
46
+ const renderResult = renderDiagram(diagram, type);
47
+ /** @type {SummaryCard[]} */
48
+ const summary = "summary" in renderResult ? /** @type {SummaryCard[]} */ (renderResult.summary) : [];
49
+ const style = normalizeVisualStyle(diagram.visual, "/visual");
50
+ const rawHtml = renderHtml(diagram.meta.title, diagram.meta.subtitle ?? "", renderResult.svg, type, summary, style);
51
+
52
+ const pageStyles = rawHtml.match(/<style>([\s\S]*?)<\/style>/)?.[1] ?? "";
53
+ const diagramStyles=(pageStyles.match(/[^{}]+\{[^{}]*\}/g) ?? []).filter(rule=>/^\s*(?:[:][r]oot|\[data-|\.node|\.edge|\.group|\.lane|\.legend|\.flow|\.label|\.sublabel|\.muted|\.actor|\.step|\.lifeline|\.tech-diagram|\.surface|\.stage|marker|#arrow)/.test(rule)).join("\n");
54
+ const svgStyles = diagramStyles.replaceAll(":root", "svg").replaceAll(/\[data-theme=/g, "svg[data-theme=").replaceAll(/\[data-visual-style=/g, "svg[data-visual-style=");
55
+ const html = rawHtml.replace('data-visual-style="default" data-export-root', `data-visual-style="${style}" data-export-root`).replace(/(<svg[^>]*>)/, `$1<style>${svgStyles}\nsvg{font-family:ui-sans-serif,system-ui,"Segoe UI",sans-serif;}svg[data-theme="dark"]{--surface:#172033;--text:#e2e8f0;--muted:#94a3b8;--line:#94a3b8;}svg[data-theme="light"]{--surface:#f8fafc;--text:#0f172a;--muted:#475569;--line:#475569;--frontend:#0369a1;--backend:#047857;--database:#0e7490;--external:#475569;--agent:#0e7490;--model:#7e22ce;--memory:#047857;--gateway:#a16207;--browser:#0369a1;--queue:#c2410c;--flow-control:#b45309;--flow-data:#0369a1;--flow-read:#047857;--flow-write:#15803d;--flow-async:#64748b;--flow-feedback:#7e22ce;}</style>`);
56
+ fs.mkdirSync(path.dirname(path.resolve(outputPath)), { recursive: true });
57
+ fs.writeFileSync(outputPath, html, "utf8");
58
+
59
+ if (args.manifest) {
60
+ fs.mkdirSync(path.dirname(path.resolve(args.manifest)), { recursive: true });
61
+ fs.writeFileSync(args.manifest, `${JSON.stringify(renderResult.manifest, null, 2)}\n`, "utf8");
62
+ }
63
+
64
+ const report = {
65
+ ok: true,
66
+ type,
67
+ input: inputPath,
68
+ output: outputPath,
69
+ manifest: args.manifest ?? null,
70
+ quality: { status: qualityIssues.length ? "partial" : "estimated", issues: qualityIssues },
71
+ nodes: renderResult.manifest.boxes.length,
72
+ connectors: renderResult.manifest.connectors.length,
73
+ };
74
+ printReport(report, format);
75
+ } catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ if (args.format === "json") {
78
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
79
+ } else {
80
+ console.error(message);
81
+ }
82
+ process.exitCode = 1;
83
+ }
84
+
85
+ /**
86
+ * @param {string[]} argv
87
+ * @returns {Record<string, string | undefined>}
88
+ */
89
+ function parseArgs(argv) {
90
+ /** @type {Record<string, string | undefined>} */
91
+ const parsed = {};
92
+ for (let index = 0; index < argv.length; index += 1) {
93
+ const token = argv[index];
94
+ if (!token.startsWith("--")) {
95
+ throw usageError("argv", `Unexpected positional argument "${token}".`);
96
+ }
97
+ const key = token.slice(2);
98
+ const value = argv[index + 1];
99
+ if (!value || value.startsWith("--")) {
100
+ throw usageError(key, `Missing value for --${key}.`);
101
+ }
102
+ parsed[key] = value;
103
+ index += 1;
104
+ }
105
+ return parsed;
106
+ }
107
+
108
+ /**
109
+ * @param {Record<string, string | undefined>} parsed
110
+ * @param {string} key
111
+ */
112
+ function requireArg(parsed, key) {
113
+ const value = parsed[key];
114
+ if (!value) throw usageError(key, `Missing required --${key}.`);
115
+ return value;
116
+ }
117
+
118
+ /**
119
+ * @param {string} filePath
120
+ */
121
+ function readJson(filePath) {
122
+ try {
123
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
124
+ } catch (error) {
125
+ throw usageError("input", `Invalid JSON at /: ${error instanceof Error ? error.message : String(error)}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * @param {unknown} diagram
131
+ * @param {string} type
132
+ */
133
+ function validateCommon(diagram, type) {
134
+ assertObject(diagram, "/");
135
+ const model = /** @type {Record<string, unknown>} */ (diagram);
136
+ if (model.schema_version !== 1) {
137
+ throw usageError("/schema_version", "Expected schema_version to equal 1.");
138
+ }
139
+ if (model.diagram_type !== type) {
140
+ throw usageError("/diagram_type", `Expected diagram_type to equal "${type}".`);
141
+ }
142
+ assertObject(model.meta, "/meta");
143
+ requireString(/** @type {Record<string, unknown>} */ (model.meta), "title", "/meta/title");
144
+ normalizeVisualStyle(model.visual, "/visual");
145
+ }
146
+
147
+ /**
148
+ * @param {unknown} diagram
149
+ * @param {string} type
150
+ */
151
+ function renderDiagram(diagram, type) {
152
+ if (type === "workflow") return renderLaneDiagram(/** @type {LaneDiagram} */ (diagram), "nodes", "edges");
153
+ if (type === "lifecycle") return renderLaneDiagram(/** @type {LaneDiagram} */ (diagram), "states", "transitions");
154
+ if (type === "sequence") return renderSequence(/** @type {SequenceDiagram} */ (diagram));
155
+ if (type === "architecture") return renderArchitecture(/** @type {ArchitectureDiagram} */ (diagram));
156
+ return renderDataflow(/** @type {DataflowDiagram} */ (diagram));
157
+ }
158
+
159
+ /**
160
+ * @param {ArchitectureDiagram} diagram
161
+ */
162
+ function renderArchitecture(diagram) {
163
+ const nodes = requireObjectArray(diagram.nodes, "/nodes");
164
+ const connections = requireObjectArray(diagram.connections, "/connections");
165
+ const groups = diagram.groups === undefined ? [] : requireObjectArray(diagram.groups, "/groups");
166
+ const groupIds = new Set();
167
+ /** @type {Map<string, Box>} */
168
+ const boxes = new Map();
169
+ /** @type {Box[]} */
170
+ const groupBoxes = [];
171
+
172
+ for (const [index, group] of groups.entries()) {
173
+ const id = requireId(group, "id", `/groups/${index}/id`);
174
+ ensureUnique(groupIds, id, `/groups/${index}/id`);
175
+ const label = requireString(group, "label", `/groups/${index}/label`);
176
+ const type = normalizeType(group.type, `/groups/${index}/type`);
177
+ const box = {
178
+ id,
179
+ x: requireNumber(group, "x", `/groups/${index}/x`),
180
+ y: requireNumber(group, "y", `/groups/${index}/y`),
181
+ width: requireNumber(group, "width", `/groups/${index}/width`),
182
+ height: requireNumber(group, "height", `/groups/${index}/height`),
183
+ label,
184
+ sublabel: "",
185
+ type,
186
+ kind: "group",
187
+ };
188
+ groupBoxes.push(box);
189
+ }
190
+
191
+ for (const [index, node] of nodes.entries()) {
192
+ const id = requireId(node, "id", `/nodes/${index}/id`);
193
+ ensureUnique(boxes, id, `/nodes/${index}/id`);
194
+ if (typeof node.group === "string" && !groupIds.has(node.group)) {
195
+ throw usageError(`/nodes/${index}/group`, `Unknown group "${node.group}".`);
196
+ }
197
+ boxes.set(id, {
198
+ id,
199
+ x: requireNumber(node, "x", `/nodes/${index}/x`),
200
+ y: requireNumber(node, "y", `/nodes/${index}/y`),
201
+ width: typeof node.width === "number" ? node.width : 132,
202
+ height: typeof node.height === "number" ? node.height : 62,
203
+ label: requireString(node, "label", `/nodes/${index}/label`),
204
+ sublabel: typeof node.sublabel === "string" ? node.sublabel : "",
205
+ type: normalizeType(node.type, `/nodes/${index}/type`),
206
+ });
207
+ }
208
+
209
+ prepareBoxes(boxes, nodes);
210
+ for (const node of nodes) {
211
+ const box = boxes.get(String(node.id));
212
+ const group = groupBoxes.find((candidate) => candidate.id === node.group);
213
+ if (box && group && (box.x < group.x + 12 || box.y < group.y + 32 || box.x + box.width > group.x + group.width - 12 || box.y + box.height > group.y + group.height - 12)) qualityIssues.push({code:"group-containment",id:box.id,message:`Node "${box.id}" exceeds group "${group.id}" content area.`});
214
+ }
215
+
216
+
217
+ /** @type {Connector[]} */
218
+ const connectors = [];
219
+ const flowSet = new Set();
220
+ const connectionSvg = connections.map((connection, index) => {
221
+ const from = requireId(connection, "from", `/connections/${index}/from`);
222
+ const to = requireId(connection, "to", `/connections/${index}/to`);
223
+ const fromBox = boxes.get(from);
224
+ const toBox = boxes.get(to);
225
+ if (!fromBox) throw usageError(`/connections/${index}/from`, `Unknown endpoint "${from}".`);
226
+ if (!toBox) throw usageError(`/connections/${index}/to`, `Unknown endpoint "${to}".`);
227
+ const variant = normalizeVariant(connection.variant, `/connections/${index}/variant`);
228
+ const flow = normalizeFlow(connection.flow, `/connections/${index}/flow`);
229
+ if (flow) flowSet.add(flow);
230
+ const points = architectureRoute(fromBox, toBox, connection.waypoints, `/connections/${index}/waypoints`);
231
+ connectors.push({ id: `${from}-${to}-${index}`, from, to, points });
232
+ return renderConnector(points, variant, typeof connection.label === "string" ? connection.label : "", flow);
233
+ }).join("\n");
234
+
235
+ const allBoxes = [...groupBoxes, ...boxes.values()];
236
+ const contentBottom = Math.max(360, maxBoxExtent(allBoxes, "y") + 64);
237
+ const legend = normalizeLegend(diagram.legend, [...boxes.values()]);
238
+ const flowLegend = normalizeFlowLegend(diagram.flowLegend, [...flowSet]);
239
+ const width = Math.max(860, maxBoxExtent(allBoxes, "x") + 64);
240
+ const columns=Math.max(1,Math.floor((width-88)/200));
241
+ const legendHeight=legend.length ? 30+Math.ceil(legend.length/columns)*72 : 0;
242
+ const flowHeight=flowLegend.length ? 30+Math.ceil(flowLegend.length/columns)*72 : 0;
243
+ const legendSvg = renderArchitectureLegend(legend,44,contentBottom-4,columns);
244
+ const flowLegendSvg = renderFlowLegend(flowLegend,44,contentBottom+legendHeight,columns);
245
+ const height=contentBottom+legendHeight+flowHeight+32;
246
+ const groupSvg = groupBoxes.map(renderGroup).join("\n");
247
+ const boxSvg = [...boxes.values()].map(renderBox).join("\n");
248
+ const svg = wrapSvg(width, height, `${groupSvg}\n${connectionSvg}\n${boxSvg}\n${legendSvg}\n${flowLegendSvg}`);
249
+
250
+ return {
251
+ svg,
252
+ manifest: manifestFrom(width, height, allBoxes, connectors),
253
+ summary: normalizeSummary(diagram.summary),
254
+ };
255
+ }
256
+
257
+ /**
258
+ * @param {LaneDiagram} diagram
259
+ * @param {"nodes" | "states"} itemKey
260
+ * @param {"edges" | "transitions"} edgeKey
261
+ */
262
+ function renderLaneDiagram(diagram, itemKey, edgeKey) {
263
+ const lanes = requireObjectArray(diagram.lanes, "/lanes");
264
+ const items = requireObjectArray(diagram[itemKey], `/${itemKey}`);
265
+ const edges = requireObjectArray(diagram[edgeKey], `/${edgeKey}`);
266
+ const isWorkflow = itemKey === "nodes";
267
+ const laneIds = new Set();
268
+ for (const [index, lane] of lanes.entries()) {
269
+ assertObject(lane, `/lanes/${index}`);
270
+ const id = requireId(lane, "id", `/lanes/${index}/id`);
271
+ requireString(lane, "label", `/lanes/${index}/label`);
272
+ ensureUnique(laneIds, id, `/lanes/${index}/id`);
273
+ }
274
+
275
+ /** @type {Map<string, Box>} */
276
+ const boxes = new Map();
277
+ const maxCol = Math.max(0, ...items.map((item, index) => {
278
+ assertObject(item, `/${itemKey}/${index}`);
279
+ return requireNumber(item, "col", `/${itemKey}/${index}/col`);
280
+ }));
281
+ let width = Math.max(780, 190 + (maxCol + 1) * 180);
282
+ let height = Math.max(420, 140 + lanes.length * 145);
283
+ const laneHeight = 118;
284
+ const top = 72;
285
+ const left = 142;
286
+ const colGap = 172;
287
+
288
+ const laneIndex = new Map(lanes.map((lane, index) => [String(lane.id), index]));
289
+ for (const [index, item] of items.entries()) {
290
+ const id = requireId(item, "id", `/${itemKey}/${index}/id`);
291
+ ensureUnique(boxes, id, `/${itemKey}/${index}/id`);
292
+ const lane = requireId(item, "lane", `/${itemKey}/${index}/lane`);
293
+ if (!laneIds.has(lane)) throw usageError(`/${itemKey}/${index}/lane`, `Unknown lane "${lane}".`);
294
+ const type = normalizeType(item.type, `/${itemKey}/${index}/type`);
295
+ const label = requireString(item, "label", `/${itemKey}/${index}/label`);
296
+ const sublabel = typeof item.sublabel === "string" ? item.sublabel : "";
297
+ const col = requireNumber(item, "col", `/${itemKey}/${index}/col`);
298
+ const yOffset = typeof item.yOffset === "number" ? item.yOffset : 0;
299
+ const width = typeof item.width === "number" ? item.width : defaultWorkflowWidth(type, isWorkflow);
300
+ const height = typeof item.height === "number" ? item.height : defaultWorkflowHeight(type, isWorkflow);
301
+ const box = {
302
+ id,
303
+ x: left + col * colGap,
304
+ y: top + Number(laneIndex.get(lane)) * laneHeight + 38 + yOffset,
305
+ width,
306
+ height,
307
+ label,
308
+ sublabel,
309
+ type,
310
+ actor: isWorkflow && typeof item.actor === "string" ? item.actor : "",
311
+ step: isWorkflow ? normalizeStep(item.step, index + 1, `/${itemKey}/${index}/step`) : "",
312
+ shape: isWorkflow ? shapeForWorkflowType(type) : "box",
313
+ };
314
+ boxes.set(id, box);
315
+ }
316
+
317
+ prepareBoxes(boxes, items);
318
+ let laneTop = 72;
319
+ /** @type {Array<{y:number,height:number}>} */
320
+ const laneGeometry = [];
321
+ const columns = [...new Set(items.map((item) => Number(item.col)))].sort((a, b) => a - b);
322
+ const columnX = new Map();
323
+ let nextX = 160;
324
+ for (const col of columns) {
325
+ columnX.set(col, nextX);
326
+ nextX += Math.max(0, ...items.filter((item) => item.col === col).map((item) => boxes.get(String(item.id))?.width ?? 0)) + 64;
327
+ }
328
+ for (const lane of lanes) {
329
+ const members = items.filter((item) => item.lane === lane.id);
330
+ const columnBottom = new Map();
331
+ for (const item of members) {
332
+ const box = boxes.get(String(item.id));
333
+ if (!box) continue;
334
+ box.x = Number(columnX.get(Number(item.col)));
335
+ const baseline = columnBottom.get(item.col) ?? laneTop + 48;
336
+ box.y = baseline + (typeof item.yOffset === "number" ? item.yOffset : 0);
337
+ columnBottom.set(item.col, box.y + box.height + 32);
338
+ }
339
+ const bottom = Math.max(laneTop + 112, ...members.map((item) => { const b = boxes.get(String(item.id)); return b ? b.y + b.height + 24 : laneTop; }));
340
+ laneGeometry.push({ y: laneTop, height: bottom - laneTop });
341
+ laneTop = bottom + 32;
342
+ }
343
+ width = Math.max(width, nextX + 40);
344
+ height = Math.max(height, laneTop + 40);
345
+
346
+
347
+ /** @type {Connector[]} */
348
+ const connectors = [];
349
+ const edgeSvg = edges.map((edge, index) => {
350
+ assertObject(edge, `/${edgeKey}/${index}`);
351
+ const from = requireId(edge, "from", `/${edgeKey}/${index}/from`);
352
+ const to = requireId(edge, "to", `/${edgeKey}/${index}/to`);
353
+ const fromBox = boxes.get(from);
354
+ const toBox = boxes.get(to);
355
+ if (!fromBox) throw usageError(`/${edgeKey}/${index}/from`, `Unknown endpoint "${from}".`);
356
+ if (!toBox) throw usageError(`/${edgeKey}/${index}/to`, `Unknown endpoint "${to}".`);
357
+ const variant = normalizeVariant(edge.variant, `/${edgeKey}/${index}/variant`);
358
+ const points = edge.waypoints === undefined ? routeBoxes(fromBox, toBox) : routeWithWaypoints(fromBox, toBox, edge.waypoints, `/${edgeKey}/${index}/waypoints`);
359
+ const connectorId = `${from}-${to}-${index}`;
360
+ connectors.push({ id: connectorId, from, to, points });
361
+ return renderConnector(points, variant, typeof edge.label === "string" ? edge.label : "");
362
+ }).join("\n");
363
+
364
+ const laneSvg = lanes.map((lane, index) => {
365
+ const y = laneGeometry[index].y;
366
+ return `<g class="lane"><rect x="34" y="${y}" width="${width - 68}" height="${laneGeometry[index].height}" rx="10"/><text x="52" y="${y + 28}" class="label muted">${escapeHtml(String(lane.label))}</text></g>`;
367
+ }).join("\n");
368
+ const boxSvg = [...boxes.values()].map(renderBox).join("\n");
369
+ const svg = wrapSvg(width, height, `${laneSvg}\n${edgeSvg}\n${boxSvg}`);
370
+
371
+ return {
372
+ svg,
373
+ manifest: manifestFrom(width, height, [...boxes.values()], connectors),
374
+ summary: isWorkflow ? normalizeSummary(diagram.summary) : [],
375
+ };
376
+ }
377
+
378
+ /**
379
+ * @param {SequenceDiagram} diagram
380
+ */
381
+ function renderSequence(diagram) {
382
+ const participants = requireObjectArray(diagram.participants, "/participants");
383
+ const messages = requireObjectArray(diagram.messages, "/messages");
384
+ const ids = new Set();
385
+ let width = Math.max(760, 160 + participants.length * 170);
386
+ let height = Math.max(420, 170 + messages.length * 82);
387
+ const top = 74;
388
+ const laneGap = (width - 160) / Math.max(1, participants.length - 1);
389
+ /** @type {Map<string, Box>} */
390
+ const boxes = new Map();
391
+
392
+ for (const [index, participant] of participants.entries()) {
393
+ assertObject(participant, `/participants/${index}`);
394
+ const id = requireId(participant, "id", `/participants/${index}/id`);
395
+ ensureUnique(ids, id, `/participants/${index}/id`);
396
+ const type = normalizeType(participant.type, `/participants/${index}/type`);
397
+ const label = requireString(participant, "label", `/participants/${index}/label`);
398
+ const x = participants.length === 1 ? width / 2 - 64 : 80 + index * laneGap - 64;
399
+ boxes.set(id, {
400
+ id,
401
+ x,
402
+ y: top,
403
+ width: 128,
404
+ height: 56,
405
+ label,
406
+ sublabel: typeof participant.sublabel === "string" ? participant.sublabel : "",
407
+ type,
408
+ });
409
+ }
410
+
411
+ prepareBoxes(boxes, participants);
412
+ const gap = Math.max(280, ...[...boxes.values()].map(box=>box.width+80), ...messages.map((m) => layoutText(typeof m.label === "string" ? m.label : "", 260, 12, 16).width + 60));
413
+ width = Math.max(width, 160 + participants.length * gap);
414
+ [...boxes.values()].forEach((box, index) => { box.x = 80 + index * gap + gap / 2 - box.width / 2; });
415
+ let messageY = Math.max(...[...boxes.values()].map((box) => box.y + box.height)) + 72;
416
+ height = Math.max(height, messageY + messages.length * 108 + 80);
417
+
418
+
419
+ /** @type {Connector[]} */
420
+ const connectors = [];
421
+
422
+ const messageSvg = messages.map((message, index) => {
423
+ assertObject(message, `/messages/${index}`);
424
+ if (["activation","activate","deactivate","fragment","alt","loop","par","async"].some((key)=>key in message)) throw usageError(`/messages/${index}`,"Complex sequence semantics require the Mermaid route; do not simplify activation/fragments/async fields.");
425
+ const from = requireId(message, "from", `/messages/${index}/from`);
426
+ const to = requireId(message, "to", `/messages/${index}/to`);
427
+ const fromBox = boxes.get(from);
428
+ const toBox = boxes.get(to);
429
+ if (!fromBox) throw usageError(`/messages/${index}/from`, `Unknown endpoint "${from}".`);
430
+ if (!toBox) throw usageError(`/messages/${index}/to`, `Unknown endpoint "${to}".`);
431
+ const messageText = typeof message.label === "string" ? message.label : "";
432
+ const textLayout = layoutText(messageText, 190, 12, 16);
433
+ const y = messageY;
434
+ messageY += Math.max(72, textLayout.height + 56 + (from === to ? 32 : 0));
435
+ /** @type {Array<[number, number]>} */
436
+ const points = from === to ? [[centerX(fromBox), y], [centerX(fromBox) + 64, y], [centerX(fromBox) + 64, y + 32], [centerX(fromBox), y + 32]] : [[centerX(fromBox), y], [centerX(toBox), y]];
437
+ occupiedRoutes.push(points);
438
+ const variant = normalizeVariant(message.variant, `/messages/${index}/variant`);
439
+ connectors.push({ id: `${from}-${to}-${index}`, from, to, points, kind: "sequence", self: from === to });
440
+ return renderConnector(points, variant, typeof message.label === "string" ? message.label : "");
441
+ }).join("\n");
442
+ const boxSvg = [...boxes.values()].map(renderBox).join("\n");
443
+ height=Math.max(height,messageY+80);
444
+ const lifelines = [...boxes.values()].map((box) => `<line x1="${centerX(box)}" y1="${box.y + box.height}" x2="${centerX(box)}" y2="${height - 56}" class="lifeline"/>`).join("\n");
445
+ const svg = wrapSvg(width, height, `${lifelines}\n${messageSvg}\n${boxSvg}`);
446
+
447
+ return { svg, manifest: manifestFrom(width, height, [...boxes.values()], connectors) };
448
+ }
449
+
450
+ /**
451
+ * @param {DataflowDiagram} diagram
452
+ */
453
+ function renderDataflow(diagram) {
454
+ const stages = requireObjectArray(diagram.stages, "/stages");
455
+ const nodes = requireObjectArray(diagram.nodes, "/nodes");
456
+ const flows = requireObjectArray(diagram.flows, "/flows");
457
+ const width = Math.max(860, 150 + stages.length * 180);
458
+ const height = Math.max(460, 180 + maxRowsByStage(nodes) * 110);
459
+ const stageWidth = (width - 84) / Math.max(1, stages.length);
460
+ /** @type {Map<string, Box>} */
461
+ const boxes = new Map();
462
+
463
+ for (const [index, stage] of stages.entries()) {
464
+ assertObject(stage, `/stages/${index}`);
465
+ requireString(stage, "label", `/stages/${index}/label`);
466
+ }
467
+ for (const [index, node] of nodes.entries()) {
468
+ assertObject(node, `/nodes/${index}`);
469
+ const id = requireId(node, "id", `/nodes/${index}/id`);
470
+ ensureUnique(boxes, id, `/nodes/${index}/id`);
471
+ const stage = requireNumber(node, "stage", `/nodes/${index}/stage`);
472
+ if (stage < 0 || stage >= stages.length) throw usageError(`/nodes/${index}/stage`, `Stage index ${stage} is outside /stages.`);
473
+ const row = typeof node.row === "number" ? node.row : countStageBefore(nodes, index, stage);
474
+ const type = normalizeType(node.type, `/nodes/${index}/type`);
475
+ boxes.set(id, {
476
+ id,
477
+ x: 54 + stage * stageWidth + Math.max(8, stageWidth / 2 - 64),
478
+ y: 118 + row * 96,
479
+ width: 128,
480
+ height: 58,
481
+ label: requireString(node, "label", `/nodes/${index}/label`),
482
+ sublabel: typeof node.sublabel === "string" ? node.sublabel : "",
483
+ type,
484
+ });
485
+ }
486
+
487
+ prepareBoxes(boxes, nodes);
488
+
489
+ /** @type {Connector[]} */
490
+ const connectors = [];
491
+ const stageSvg = stages.map((stage, index) => {
492
+ const x = 42 + index * stageWidth;
493
+ return `<g class="stage"><rect x="${x}" y="64" width="${stageWidth - 10}" height="${height - 118}" rx="12"/><text x="${x + 18}" y="92" class="label muted">${escapeHtml(String(stage.label))}</text></g>`;
494
+ }).join("\n");
495
+ const flowSvg = flows.map((flow, index) => {
496
+ assertObject(flow, `/flows/${index}`);
497
+ const from = requireId(flow, "from", `/flows/${index}/from`);
498
+ const to = requireId(flow, "to", `/flows/${index}/to`);
499
+ const fromBox = boxes.get(from);
500
+ const toBox = boxes.get(to);
501
+ if (!fromBox) throw usageError(`/flows/${index}/from`, `Unknown endpoint "${from}".`);
502
+ if (!toBox) throw usageError(`/flows/${index}/to`, `Unknown endpoint "${to}".`);
503
+ const variant = normalizeVariant(flow.variant, `/flows/${index}/variant`);
504
+ const points = routeBoxes(fromBox, toBox);
505
+ connectors.push({ id: `${from}-${to}-${index}`, from, to, points });
506
+ return renderConnector(points, variant, typeof flow.label === "string" ? flow.label : "");
507
+ }).join("\n");
508
+ const boxSvg = [...boxes.values()].map(renderBox).join("\n");
509
+ const svg = wrapSvg(width, height, `${stageSvg}\n${flowSvg}\n${boxSvg}`);
510
+
511
+ return { svg, manifest: manifestFrom(width, height, [...boxes.values()], connectors) };
512
+ }
513
+
514
+ /**
515
+ * @param {Array<Record<string, unknown>>} nodes
516
+ */
517
+ function maxRowsByStage(nodes) {
518
+ /** @type {Map<number, number>} */
519
+ const rows = new Map();
520
+ for (const node of nodes) {
521
+ if (typeof node.stage === "number") {
522
+ rows.set(node.stage, (rows.get(node.stage) ?? 0) + 1);
523
+ }
524
+ }
525
+ return Math.max(1, ...rows.values());
526
+ }
527
+
528
+ /**
529
+ * @param {Array<Record<string, unknown>>} nodes
530
+ * @param {number} endIndex
531
+ * @param {number} stage
532
+ */
533
+ function countStageBefore(nodes, endIndex, stage) {
534
+ let count = 0;
535
+ for (let index = 0; index < endIndex; index += 1) {
536
+ const node = nodes[index];
537
+ if (node.stage === stage) count += 1;
538
+ }
539
+ return count;
540
+ }
541
+
542
+
543
+ /** @param {Map<string, Box>} boxes @param {Array<Record<string, unknown>>} sources */
544
+ function prepareBoxes(boxes, sources) {
545
+ for (const [id, box] of boxes) {
546
+ const source = sources.find((item) => item.id === id) ?? {};
547
+ const fitted = fitNodeText(box, { width: typeof source.width === "number", height: typeof source.height === "number" });
548
+ boxes.set(id, fitted.box);
549
+ qualityIssues.push(...fitted.issues);
550
+ }
551
+ routeObstacles = [...boxes.values()];
552
+ }
553
+
554
+ /** @param {string} id @param {string} owner @param {string} kind @param {string} text @param {import("./diagram-layout.mjs").TextLayout} layout @param {number} cx @param {number} top @param {string} cls */
555
+ function renderText(id, owner, kind, text, layout, cx, top, cls) {
556
+ textBounds.push({ id, owner, kind, text, lines: layout.lines, x: cx - layout.width / 2, y: top, width: layout.width, height: layout.height });
557
+ return `<text data-label-id="${escapeHtml(id)}" x="${cx}" y="${top + layout.fontSize}" class="${cls}" font-size="${layout.fontSize}" text-anchor="middle">${layout.lines.map((line, i) => `<tspan x="${cx}" dy="${i ? layout.lineHeight : 0}">${escapeHtml(line)}</tspan>`).join("")}</text>`;
558
+ }
559
+
560
+ /**
561
+ * @param {Box} box
562
+ */
563
+ function renderBox(box) {
564
+ const shape = box.shape ?? "box";
565
+ const isSemantic = isSemanticNodeType(box.type);
566
+ const available = shape === "diamond" ? box.width * 0.5 - 12 : (box.type === "queue" ? box.width - 64 : box.width - 32);
567
+ const main = layoutText(box.label, available);
568
+ const sub = layoutText(box.sublabel, available, 12, 16);
569
+ const total = main.height + (box.sublabel ? sub.height + 6 : 0);
570
+ const textTop = shape === "diamond" ? centerY(box) - total / 2 : box.y + (["database", "vectorstore"].includes(box.type) ? 30 : box.type === "queue" ? 20 : isSemantic ? 46 : box.actor ? 40 : 20);
571
+ const labelSvg = renderText(box.id + "-label", box.id, "node", box.label, main, centerX(box), textTop, "label");
572
+ const sublabel = box.sublabel ? renderText(box.id + "-sub", box.id, "node", box.sublabel, sub, centerX(box), textTop + main.height + 6, "sublabel") : "";
573
+ const actorLayout=layoutText(box.actor??"",box.width-32,12,16);
574
+ const actor = box.actor ? renderText(box.id+"-actor",box.id,"group",box.actor,actorLayout,centerX(box),box.y-actorLayout.height-12,"actor") : "";
575
+ const stepLayout=layoutText(box.step??"",Math.max(14,box.width-16),9,12);
576
+ const step = box.step ? `<g class="step-badge"><circle cx="${box.x + 13}" cy="${box.y + 13}" r="${Math.max(10,stepLayout.width/2+4)}"/>${renderText(box.id+"-step",box.id,"group",box.step,stepLayout,box.x+13,box.y+7,"step-label")}</g>` : "";
577
+ const shapeSvg = renderSemanticShape(box) ?? (shape === "diamond"
578
+ ? `<polygon points="${centerX(box)},${box.y} ${box.x + box.width},${centerY(box)} ${centerX(box)},${box.y + box.height} ${box.x},${centerY(box)}"/>`
579
+ : `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="${shape === "terminal" ? Math.round(box.height / 2) : 10}"/>`);
580
+ return `<g data-node-id="${escapeHtml(box.id)}" class="node node-${box.type}">${actor}\n${shapeSvg}\n${step}\n${labelSvg}\n${sublabel}</g>`;
581
+ }
582
+
583
+ /**
584
+ * @param {Box} box
585
+ * @returns {string | null}
586
+ */
587
+ function renderSemanticShape(box) {
588
+ const cx = centerX(box);
589
+ const cy = centerY(box);
590
+ if (box.type === "agent") {
591
+ const inset = Math.min(28, box.width * 0.18);
592
+ return `<polygon points="${box.x + inset},${box.y} ${box.x + box.width - inset},${box.y} ${box.x + box.width},${cy} ${box.x + box.width - inset},${box.y + box.height} ${box.x + inset},${box.y + box.height} ${box.x},${cy}"/><rect x="${box.x + 5}" y="${box.y + 5}" width="${box.width - 10}" height="${box.height - 10}" rx="10" fill="none" opacity="0.45"/>`;
593
+ }
594
+ if (box.type === "model") {
595
+ return `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="14"/><rect x="${box.x + 5}" y="${box.y + 5}" width="${box.width - 10}" height="${box.height - 10}" rx="10" fill="none" opacity="0.5"/><path d="M ${cx - 12} ${box.y + 20} L ${cx - 4} ${box.y + 20} L ${cx} ${box.y + 10} L ${cx + 4} ${box.y + 20} L ${cx + 12} ${box.y + 20} L ${cx + 6} ${box.y + 27} L ${cx + 9} ${box.y + 38} L ${cx} ${box.y + 31} L ${cx - 9} ${box.y + 38} L ${cx - 6} ${box.y + 27} Z" class="node-glyph"/>`;
596
+ }
597
+ if (box.type === "memory") {
598
+ return `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="12" stroke-dasharray="6 4"/>`;
599
+ }
600
+ if (box.type === "database" || box.type === "vectorstore") {
601
+ const ry = Math.min(14, box.height / 5);
602
+ const rings = box.type === "vectorstore" ? `<ellipse cx="${cx}" cy="${box.y + box.height * 0.45}" rx="${box.width / 2}" ry="${ry}" fill="none" opacity="0.45"/><ellipse cx="${cx}" cy="${box.y + box.height * 0.66}" rx="${box.width / 2}" ry="${ry}" fill="none" opacity="0.32"/>` : "";
603
+ return `<ellipse cx="${cx}" cy="${box.y + ry}" rx="${box.width / 2}" ry="${ry}"/><rect x="${box.x}" y="${box.y + ry}" width="${box.width}" height="${box.height - ry * 2}" stroke="none"/><line x1="${box.x}" y1="${box.y + ry}" x2="${box.x}" y2="${box.y + box.height - ry}"/><line x1="${box.x + box.width}" y1="${box.y + ry}" x2="${box.x + box.width}" y2="${box.y + box.height - ry}"/>${rings}<ellipse cx="${cx}" cy="${box.y + box.height - ry}" rx="${box.width / 2}" ry="${ry}"/>`;
604
+ }
605
+ if (box.type === "graphdb") {
606
+ return `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="14"/><circle cx="${cx - 18}" cy="${cy - 2}" r="12" class="node-glyph-outline"/><circle cx="${cx + 8}" cy="${cy - 14}" r="10" class="node-glyph-outline"/><circle cx="${cx + 22}" cy="${cy + 10}" r="11" class="node-glyph-outline"/><line x1="${cx - 7}" y1="${cy - 7}" x2="${cx}" y2="${cy - 12}" class="node-glyph-line"/><line x1="${cx + 16}" y1="${cy - 5}" x2="${cx + 20}" y2="${cy}" class="node-glyph-line"/>`;
607
+ }
608
+ if (box.type === "tool") {
609
+ return `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="10"/><circle cx="${cx}" cy="${box.y + 24}" r="9" class="node-glyph-outline"/><line x1="${cx}" y1="${box.y + 11}" x2="${cx}" y2="${box.y + 16}" class="node-glyph-line"/><line x1="${cx}" y1="${box.y + 32}" x2="${cx}" y2="${box.y + 37}" class="node-glyph-line"/><line x1="${cx - 13}" y1="${box.y + 24}" x2="${cx - 8}" y2="${box.y + 24}" class="node-glyph-line"/><line x1="${cx + 8}" y1="${box.y + 24}" x2="${cx + 13}" y2="${box.y + 24}" class="node-glyph-line"/>`;
610
+ }
611
+ if (box.type === "document") {
612
+ const fold = Math.min(16, box.width * 0.16);
613
+ return `<path d="M ${box.x} ${box.y} L ${box.x + box.width - fold} ${box.y} L ${box.x + box.width} ${box.y + fold} L ${box.x + box.width} ${box.y + box.height} L ${box.x} ${box.y + box.height} Z"/><path d="M ${box.x + box.width - fold} ${box.y} L ${box.x + box.width - fold} ${box.y + fold} L ${box.x + box.width} ${box.y + fold}" fill="none"/>`;
614
+ }
615
+ if (box.type === "queue") {
616
+ const ry = box.height / 2;
617
+ const end = Math.min(18, box.width / 6);
618
+ return `<ellipse cx="${box.x + end}" cy="${cy}" rx="${end}" ry="${ry}"/><rect x="${box.x + end}" y="${cy - ry}" width="${box.width - end * 2}" height="${ry * 2}" stroke="none"/><line x1="${box.x + end}" y1="${cy - ry}" x2="${box.x + box.width - end}" y2="${cy - ry}"/><line x1="${box.x + end}" y1="${cy + ry}" x2="${box.x + box.width - end}" y2="${cy + ry}"/><ellipse cx="${box.x + box.width - end}" cy="${cy}" rx="${end}" ry="${ry}"/>`;
619
+ }
620
+ if (box.type === "browser") {
621
+ return `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="10"/><rect x="${box.x}" y="${box.y}" width="${box.width}" height="20" rx="10" class="node-chrome"/><circle cx="${box.x + 14}" cy="${box.y + 10}" r="3" class="node-dot"/><circle cx="${box.x + 26}" cy="${box.y + 10}" r="3" class="node-dot"/><circle cx="${box.x + 38}" cy="${box.y + 10}" r="3" class="node-dot"/>`;
622
+ }
623
+ if (box.type === "user") {
624
+ return `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="12"/><circle cx="${cx}" cy="${box.y + 22}" r="9" class="node-glyph-outline"/><path d="M ${cx - 16} ${box.y + 43} Q ${cx} ${box.y + 28} ${cx + 16} ${box.y + 43}" class="node-glyph-line"/>`;
625
+ }
626
+ if (box.type === "gateway") {
627
+ const inset = Math.min(22, box.width * 0.18);
628
+ return `<polygon points="${box.x + inset},${box.y} ${box.x + box.width - inset},${box.y} ${box.x + box.width},${cy} ${box.x + box.width - inset},${box.y + box.height} ${box.x + inset},${box.y + box.height} ${box.x},${cy}"/>`;
629
+ }
630
+ return null;
631
+ }
632
+
633
+ /**
634
+ * @param {Box} box
635
+ */
636
+ function renderGroup(box) {
637
+ return `<g class="group group-${box.type}"><rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="14"/>${renderText(box.id+"-group",box.id,"group",box.label,layoutText(box.label,box.width-32,12,16),box.x+box.width/2,box.y+10,"sublabel")}</g>`;
638
+ }
639
+
640
+ /**
641
+ * @param {Array<[number, number]>} points
642
+ * @param {string} variant
643
+ * @param {string} label
644
+ */
645
+ function renderConnector(points, variant, label, flow = "") {
646
+ const pathData = points.map(([x, y], index) => `${index === 0 ? "M" : "L"} ${x} ${y}`).join(" ");
647
+ const textLayout = layoutText(label, 190, 12, 16);
648
+ const placed = placeEdgeLabel(points, textLayout, [...routeObstacles, ...textBounds]);
649
+ const id = `edge-label-${textBounds.length}`;
650
+ if (label) qualityIssues.push(...placed.issues.map((issue) => ({ ...issue, id })));
651
+ if(label) deferredLabels.push({id,label,points});
652
+ const labelSvg = label ? renderText(id, id, "edge", label, textLayout, placed.rect.x + placed.rect.width / 2, placed.rect.y, "edge-label") : "";
653
+ const marker = flow ? `flow-${flow}` : (variant === "return" ? "default" : variant);
654
+ return `<g class="edge edge-${variant}${flow ? ` flow-${flow}` : ""}"><path d="${pathData}" marker-end="url(#arrow-${marker})"/>\n${labelSvg}</g>`;
655
+ }
656
+
657
+ /**
658
+ * @param {number} width
659
+ * @param {number} height
660
+ * @param {string} body
661
+ */
662
+ function wrapSvg(width, height, body) {
663
+ for(let i=textBounds.length-1;i>=0;i--) if(textBounds[i].kind === "edge") textBounds.splice(i,1);
664
+ for(const {id,label,points} of deferredLabels) {
665
+ const layout=layoutText(label,190,12,16);
666
+ const placed=placeEdgeLabel(points,layout,[...routeObstacles,...textBounds],occupiedRoutes);
667
+ qualityIssues.push(...placed.issues);
668
+ const replacement=renderText(id,id,"edge",label,layout,placed.rect.x+placed.rect.width/2,placed.rect.y,"edge-label");
669
+ body=body.replace(new RegExp('<text data-label-id="'+id+'"[^>]*>[\\s\\S]*?</text>'),replacement);
670
+ }
671
+
672
+ width = Math.ceil(Math.max(width, ...routeObstacles.map((b) => b.x + b.width + 40), ...textBounds.map((b) => b.x + b.width + 40), ...occupiedRoutes.flat().map((p) => p[0] + 40)));
673
+ height = Math.ceil(Math.max(height, ...routeObstacles.map((b) => b.y + b.height + 40), ...textBounds.map((b) => b.y + b.height + 40), ...occupiedRoutes.flat().map((p) => p[1] + 40)));
674
+ finalCanvas = { width, height };
675
+ return `<svg class="tech-diagram" data-theme="light" data-visual-style="default" data-export-root="1" role="img" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
676
+ <defs>
677
+ <marker id="arrow-default" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
678
+ <marker id="arrow-emphasis" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
679
+ <marker id="arrow-security" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
680
+ <marker id="arrow-dashed" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
681
+ <marker id="arrow-flow-control" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
682
+ <marker id="arrow-flow-data" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
683
+ <marker id="arrow-flow-read" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
684
+ <marker id="arrow-flow-write" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
685
+ <marker id="arrow-flow-async" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
686
+ <marker id="arrow-flow-feedback" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
687
+ </defs>
688
+ <rect x="16" y="18" width="${width - 32}" height="${height - 36}" rx="18" class="surface"/>
689
+ ${body}
690
+ </svg>`;
691
+ }
692
+
693
+ /**
694
+ * @param {string} title
695
+ * @param {string} subtitle
696
+ * @param {string} svg
697
+ * @param {string} type
698
+ * @param {SummaryCard[]} [summary]
699
+ */
700
+ function renderHtml(title, subtitle, svg, type, summary = [], style = "default") {
701
+ const summaryHtml = renderSummaryCards(summary);
702
+ return `<!doctype html>
703
+ <html lang="en" data-theme="auto" data-visual-style="${escapeHtml(style)}">
704
+ <head>
705
+ <meta charset="utf-8">
706
+ <meta name="viewport" content="width=device-width, initial-scale=1">
707
+ <title>${escapeHtml(title)}</title>
708
+ <style>
709
+ :root {
710
+ color-scheme: dark light;
711
+ --bg: #0f172a;
712
+ --panel: #111827;
713
+ --surface: #172033;
714
+ --text: #f8fafc;
715
+ --muted: #94a3b8;
716
+ --line: #64748b;
717
+ --frontend: #38bdf8;
718
+ --backend: #34d399;
719
+ --database: #a78bfa;
720
+ --cloud: #fbbf24;
721
+ --security: #fb7185;
722
+ --messagebus: #fb923c;
723
+ --external: #cbd5e1;
724
+ --agent: #22d3ee;
725
+ --model: #c084fc;
726
+ --memory: #34d399;
727
+ --vectorstore: #60a5fa;
728
+ --graphdb: #f472b6;
729
+ --tool: #f59e0b;
730
+ --document: #cbd5e1;
731
+ --queue: #fb923c;
732
+ --browser: #38bdf8;
733
+ --user: #fda4af;
734
+ --gateway: #fbbf24;
735
+ --flow-control: #f59e0b;
736
+ --flow-data: #38bdf8;
737
+ --flow-read: #34d399;
738
+ --flow-write: #22c55e;
739
+ --flow-async: #94a3b8;
740
+ --flow-feedback: #c084fc;
741
+ }
742
+ [data-theme="light"] {
743
+ --bg: #f8fafc;
744
+ --panel: #ffffff;
745
+ --surface: #eef2f7;
746
+ --text: #0f172a;
747
+ --muted: #64748b;
748
+ --line: #475569;
749
+ }
750
+ [data-visual-style="terminal"] {
751
+ --bg: #0b1020;
752
+ --panel: #0f172a;
753
+ --surface: #111827;
754
+ --text: #e2e8f0;
755
+ --muted: #94a3b8;
756
+ --frontend: #38bdf8;
757
+ --backend: #22c55e;
758
+ --security: #f472b6;
759
+ }
760
+ [data-visual-style="blueprint"] {
761
+ --bg: #082f49;
762
+ --panel: #0b3b5e;
763
+ --surface: #0c4a6e;
764
+ --text: #e0f2fe;
765
+ --muted: #7dd3fc;
766
+ --line: #67e8f9;
767
+ --frontend: #38bdf8;
768
+ --backend: #67e8f9;
769
+ --database: #fde047;
770
+ --cloud: #bae6fd;
771
+ }
772
+ [data-visual-style="minimal"] {
773
+ --bg: #f8fafc;
774
+ --panel: #ffffff;
775
+ --surface: #f1f5f9;
776
+ --text: #111827;
777
+ --muted: #64748b;
778
+ --line: #94a3b8;
779
+ }
780
+ [data-visual-style="glass"] {
781
+ --bg: #111827;
782
+ --panel: color-mix(in srgb, #1f2937, transparent 18%);
783
+ --surface: color-mix(in srgb, #334155, transparent 18%);
784
+ --text: #f8fafc;
785
+ --muted: #cbd5e1;
786
+ }
787
+ [data-visual-style="warm"] {
788
+ --bg: #f8f6f3;
789
+ --panel: #fffcf7;
790
+ --surface: #f1ede6;
791
+ --text: #141413;
792
+ --muted: #7c756c;
793
+ --line: #8b7355;
794
+ --frontend: #d97757;
795
+ --backend: #7b8b5c;
796
+ --database: #8c6f5a;
797
+ --cloud: #b45309;
798
+ }
799
+ [data-visual-style="openai"] {
800
+ --bg: #ffffff;
801
+ --panel: #ffffff;
802
+ --surface: #f8fafc;
803
+ --text: #0f172a;
804
+ --muted: #64748b;
805
+ --line: #10a37f;
806
+ --frontend: #10a37f;
807
+ --backend: #0f766e;
808
+ --database: #0891b2;
809
+ }
810
+ [data-visual-style="editorial-dark"] {
811
+ --bg: #0a0a0a;
812
+ --panel: #111111;
813
+ --surface: #181818;
814
+ --text: #f5f0e8;
815
+ --muted: #b8a88f;
816
+ --line: #d4a574;
817
+ --frontend: #38bdf8;
818
+ --backend: #34d399;
819
+ --database: #a78bfa;
820
+ --cloud: #d4a574;
821
+ --security: #fb7185;
822
+ }
823
+ * { box-sizing: border-box; }
824
+ body {
825
+ margin: 0;
826
+ min-height: 100vh;
827
+ background: var(--bg);
828
+ color: var(--text);
829
+ font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
830
+ }
831
+ main { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 28px 0; }
832
+ header { display: flex; align-items: end; justify-content: space-between; gap: 20px; margin-bottom: 18px; }
833
+ h1 { font-size: 24px; margin: 0; letter-spacing: 0; }
834
+ p { margin: 6px 0 0; color: var(--muted); }
835
+ .toolbar { display: flex; gap: 8px; flex-wrap: wrap; }
836
+ button {
837
+ border: 1px solid color-mix(in srgb, var(--muted), transparent 55%);
838
+ background: var(--panel);
839
+ color: var(--text);
840
+ border-radius: 8px;
841
+ padding: 8px 11px;
842
+ cursor: pointer;
843
+ }
844
+ .diagram-frame { background: var(--panel); border: 1px solid color-mix(in srgb, var(--muted), transparent 70%); border-radius: 12px; overflow: auto; }
845
+ .tech-diagram { display: block; min-width: 720px; width: 100%; height: auto; }
846
+ .group rect { fill: color-mix(in srgb, currentColor, transparent 94%); stroke: currentColor; stroke-width: 1.2; stroke-dasharray: 8 5; }
847
+ .group-frontend { color: var(--frontend); }
848
+ .group-backend, .group-active, .group-start { color: var(--backend); }
849
+ .group-database { color: var(--database); }
850
+ .group-cloud, .group-decision { color: var(--cloud); }
851
+ .group-security, .group-failure, .group-waiting { color: var(--security); }
852
+ .group-messagebus { color: var(--messagebus); }
853
+ .group-external, .group-neutral, .group-success { color: var(--external); }
854
+ .surface { fill: var(--surface); stroke: color-mix(in srgb, var(--muted), transparent 70%); }
855
+ .lane rect, .stage rect { fill: color-mix(in srgb, var(--surface), transparent 25%); stroke: color-mix(in srgb, var(--muted), transparent 72%); }
856
+ .node rect, .node polygon, .node path, .node ellipse, .node line, .node circle { fill: color-mix(in srgb, currentColor, transparent 86%); stroke: currentColor; stroke-width: 1.6; }
857
+ .node-frontend { color: var(--frontend); }
858
+ .node-backend, .node-active, .node-start, .node-success { color: var(--backend); }
859
+ .node-database { color: var(--database); }
860
+ .node-cloud, .node-decision { color: var(--cloud); }
861
+ .node-security, .node-failure, .node-waiting { color: var(--security); }
862
+ .node-messagebus { color: var(--messagebus); }
863
+ .node-external, .node-neutral { color: var(--external); }
864
+ .node-agent { color: var(--agent); }
865
+ .node-model { color: var(--model); }
866
+ .node-memory { color: var(--memory); }
867
+ .node-vectorstore { color: var(--vectorstore); }
868
+ .node-graphdb { color: var(--graphdb); }
869
+ .node-tool { color: var(--tool); }
870
+ .node-document { color: var(--document); }
871
+ .node-queue { color: var(--queue); }
872
+ .node-browser { color: var(--browser); }
873
+ .node-user { color: var(--user); }
874
+ .node-gateway { color: var(--gateway); }
875
+ .node-glyph { fill: currentColor; stroke: none; }
876
+ .node-glyph-outline, .node-glyph-line { fill: none; stroke: currentColor; }
877
+ .node-chrome { fill: currentColor; opacity: 0.25; stroke: none; }
878
+ .node-dot { fill: currentColor; stroke: none; opacity: 0.65; }
879
+ .label { fill: var(--text); font-weight: 650; font-size: 14px; }
880
+ .sublabel, .muted, .actor { fill: var(--muted); font-size: 12px; }
881
+ .actor { font-weight: 650; }
882
+ .step-badge circle { fill: var(--panel); stroke: currentColor; stroke-width: 1.3; }
883
+ .step-badge text { fill: var(--text); font-size: 9px; font-weight: 750; }
884
+ .edge path { fill: none; stroke: var(--line); stroke-width: 1.6; }
885
+ .edge-emphasis path { stroke: var(--frontend); stroke-width: 2; }
886
+ .edge-security path { stroke: var(--security); stroke-dasharray: 5 4; }
887
+ .edge-dashed path, .edge-return path { stroke-dasharray: 5 4; }
888
+ .flow-control path { stroke: var(--flow-control); }
889
+ .flow-data path { stroke: var(--flow-data); stroke-width: 2; }
890
+ .flow-read path { stroke: var(--flow-read); }
891
+ .flow-write path { stroke: var(--flow-write); stroke-dasharray: 5 4; }
892
+ .flow-async path { stroke: var(--flow-async); stroke-dasharray: 4 4; }
893
+ .flow-feedback path { stroke: var(--flow-feedback); stroke-width: 2; }
894
+ .edge-label { fill: var(--muted); font-size: 12px; paint-order: stroke; stroke: var(--surface); stroke-width: 5px; stroke-linejoin: round; }
895
+ .lifeline { stroke: color-mix(in srgb, var(--muted), transparent 48%); stroke-dasharray: 4 5; }
896
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin-top: 12px; }
897
+ .summary-card { background: var(--panel); border: 1px solid color-mix(in srgb, var(--muted), transparent 74%); border-radius: 8px; padding: 12px; }
898
+ .summary-card h2 { margin: 0 0 7px; font-size: 13px; letter-spacing: 0; }
899
+ .summary-card ul { margin: 0; padding-left: 18px; color: var(--muted); font-size: 12px; }
900
+ .summary-card li + li { margin-top: 4px; }
901
+ marker path { fill: var(--line); }
902
+ #arrow-emphasis path { fill: var(--frontend); }
903
+ #arrow-security path { fill: var(--security); }
904
+ #arrow-dashed path { fill: var(--line); }
905
+ #arrow-flow-control path { fill: var(--flow-control); }
906
+ #arrow-flow-data path { fill: var(--flow-data); }
907
+ #arrow-flow-read path { fill: var(--flow-read); }
908
+ #arrow-flow-write path { fill: var(--flow-write); }
909
+ #arrow-flow-async path { fill: var(--flow-async); }
910
+ #arrow-flow-feedback path { fill: var(--flow-feedback); }
911
+ footer { margin-top: 12px; color: var(--muted); font-size: 12px; }
912
+ @media print {
913
+ body { background: #fff; }
914
+ main { width: 100%; padding: 0; }
915
+ .toolbar, footer { display: none; }
916
+ .diagram-frame { border: 0; }
917
+ }
918
+ </style>
919
+ </head>
920
+ <body>
921
+ <main>
922
+ <header>
923
+ <div>
924
+ <h1>${escapeHtml(title)}</h1>
925
+ <p>${escapeHtml(subtitle || `${type} technical diagram`)}</p>
926
+ </div>
927
+ <div class="toolbar" aria-label="Diagram actions">
928
+ <button type="button" data-action="theme">Toggle theme</button>
929
+ <button type="button" data-action="svg">Download SVG</button>
930
+ <button type="button" data-action="print">Print / Save PDF</button>
931
+ </div>
932
+ </header>
933
+ <section class="diagram-frame" aria-label="${escapeHtml(title)}">
934
+ ${svg}
935
+ </section>
936
+ ${summaryHtml}
937
+ <footer>Generated as a self-contained Frontend Craft HTML technical diagram. Export raster images from the browser after visual QA.</footer>
938
+ </main>
939
+ <script>
940
+ const root = document.documentElement;
941
+ const stored = localStorage.getItem("fec-tech-diagram-theme");
942
+ const diagramSvg = document.querySelector("[data-export-root]");
943
+ root.dataset.theme = stored === "dark" ? "dark" : "light";
944
+ diagramSvg.dataset.theme = root.dataset.theme;
945
+ diagramSvg.dataset.visualStyle = root.dataset.visualStyle;
946
+ document.querySelector('[data-action="theme"]').addEventListener("click", () => {
947
+ const next = root.dataset.theme === "light" ? "dark" : "light";
948
+ root.dataset.theme = next;
949
+ diagramSvg.dataset.theme = next;
950
+ localStorage.setItem("fec-tech-diagram-theme", next);
951
+ });
952
+ document.querySelector('[data-action="print"]').addEventListener("click", () => window.print());
953
+ document.querySelector('[data-action="svg"]').addEventListener("click", () => {
954
+ const svg = document.querySelector("[data-export-root]");
955
+ const clone = svg.cloneNode(true);
956
+ const originals = [svg, ...svg.querySelectorAll("*")];
957
+ const copies = [clone, ...clone.querySelectorAll("*")];
958
+ const properties = ["fill","stroke","stroke-width","stroke-dasharray","font-family","font-size","font-weight","color","opacity","paint-order","text-anchor"];
959
+ originals.forEach((element,index) => { const computed=getComputedStyle(element); properties.forEach(property=>copies[index].style?.setProperty(property,computed.getPropertyValue(property))); });
960
+ const source = new XMLSerializer().serializeToString(clone);
961
+ const blob = new Blob([source], { type: "image/svg+xml" });
962
+ const link = document.createElement("a");
963
+ link.href = URL.createObjectURL(blob);
964
+ link.download = document.title.replace(/[^a-z0-9_-]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() + ".svg";
965
+ link.click();
966
+ URL.revokeObjectURL(link.href);
967
+ });
968
+ </script>
969
+ </body>
970
+ </html>
971
+ `;
972
+ }
973
+
974
+ /**
975
+ * @param {SummaryCard[]} summary
976
+ */
977
+ function renderSummaryCards(summary) {
978
+ if (summary.length === 0) return "";
979
+ return ` <section class="summary-grid" aria-label="Diagram summary">
980
+ ${summary.map((card) => ` <article class="summary-card summary-${card.type}">
981
+ <h2>${escapeHtml(card.title)}</h2>
982
+ <ul>${card.items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
983
+ </article>`).join("\n")}
984
+ </section>`;
985
+ }
986
+
987
+ /**
988
+ * @param {number} width
989
+ * @param {number} height
990
+ * @param {Box[]} boxes
991
+ * @param {Connector[]} connectors
992
+ */
993
+ function manifestFrom(width, height, boxes, connectors) {
994
+ for(const connector of connectors) if(connector.points.some(([x,y])=>x<0||y<0)) qualityIssues.push({code:"connector-out-of-bounds",id:connector.id,message:`Connector "${connector.id}" has negative explicit coordinates.`});
995
+ const nodes = boxes.filter((box) => box.kind !== "group");
996
+ for (const box of boxes) if (box.x < 0 || box.y < 0) qualityIssues.push({code:"box-out-of-bounds",id:box.id,message:`Box "${box.id}" has negative coordinates.`});
997
+ for (let i=0;i<nodes.length;i++) for (let j=i+1;j<nodes.length;j++) if (overlaps(nodes[i],nodes[j])) qualityIssues.push({code:"box-overlap",id:nodes[i].id,message:`Nodes "${nodes[i].id}" and "${nodes[j].id}" overlap.`});
998
+ return {
999
+ canvas: finalCanvas.width ? finalCanvas : { width, height },
1000
+ boxes: boxes.map(({ id, x, y, width: boxWidth, height: boxHeight, label, kind }) => ({ id, x, y, width: boxWidth, height: boxHeight, label, kind })),
1001
+ groups: boxes.filter((box) => box.kind === "group").map((box) => ({ ...box })),
1002
+ connectors,
1003
+ labels: textBounds,
1004
+ coordinateSpace: "svg",
1005
+ scale: 1,
1006
+ measurement: "estimated",
1007
+ issues: qualityIssues,
1008
+ };
1009
+ }
1010
+
1011
+ /**
1012
+ * @param {Box[]} boxes
1013
+ * @param {"x" | "y"} axis
1014
+ */
1015
+ function maxBoxExtent(boxes, axis) {
1016
+ const sizeKey = axis === "x" ? "width" : "height";
1017
+ return Math.max(0, ...boxes.map((box) => box[axis] + box[sizeKey]));
1018
+ }
1019
+
1020
+ /**
1021
+ * @param {Box} from
1022
+ * @param {Box} to
1023
+ * @param {unknown} waypoints
1024
+ * @param {string} pointer
1025
+ * @returns {Array<[number, number]>}
1026
+ */
1027
+ function architectureRoute(from, to, waypoints, pointer) {
1028
+ return routeWithWaypoints(from, to, waypoints, pointer);
1029
+ }
1030
+
1031
+ /**
1032
+ * @param {Box} from
1033
+ * @param {Box} to
1034
+ * @param {unknown} waypoints
1035
+ * @param {string} pointer
1036
+ * @returns {Array<[number, number]>}
1037
+ */
1038
+ function routeWithWaypoints(from, to, waypoints, pointer) {
1039
+ if (waypoints === undefined) return routeBoxes(from, to);
1040
+ if (!Array.isArray(waypoints)) throw usageError(pointer, "Expected array of [x, y] waypoints.");
1041
+ /** @type {Array<[number, number]>} */
1042
+ const parsed = waypoints.map((point, index) => {
1043
+ if (!Array.isArray(point) || point.length !== 2 || typeof point[0] !== "number" || typeof point[1] !== "number" || !Number.isFinite(point[0]) || !Number.isFinite(point[1])) {
1044
+ throw usageError(`${pointer}/${index}`, "Expected [x, y] number tuple.");
1045
+ }
1046
+ return [point[0], point[1]];
1047
+ });
1048
+ const result = routeConnector(from, to, routeObstacles.filter((box) => box.id !== from.id && box.id !== to.id), occupiedRoutes, parsed);
1049
+ qualityIssues.push(...result.issues);
1050
+ occupiedRoutes.push(result.points);
1051
+ return result.points;
1052
+ }
1053
+
1054
+ /**
1055
+ * @param {string} type
1056
+ * @param {boolean} isWorkflow
1057
+ */
1058
+ function defaultWorkflowWidth(type, isWorkflow) {
1059
+ if (!isWorkflow) return 132;
1060
+ if (type === "decision") return 92;
1061
+ if (type === "start" || type === "success" || type === "failure") return 128;
1062
+ return 132;
1063
+ }
1064
+
1065
+ /**
1066
+ * @param {string} type
1067
+ * @param {boolean} isWorkflow
1068
+ */
1069
+ function defaultWorkflowHeight(type, isWorkflow) {
1070
+ if (!isWorkflow) return 58;
1071
+ if (type === "decision") return 92;
1072
+ if (type === "start" || type === "success" || type === "failure") return 46;
1073
+ return 58;
1074
+ }
1075
+
1076
+ /**
1077
+ * @param {string} type
1078
+ * @returns {"box" | "diamond" | "terminal"}
1079
+ */
1080
+ function shapeForWorkflowType(type) {
1081
+ if (type === "decision") return "diamond";
1082
+ if (type === "start" || type === "success" || type === "failure") return "terminal";
1083
+ return "box";
1084
+ }
1085
+
1086
+ /**
1087
+ * @param {unknown} value
1088
+ * @param {number} fallback
1089
+ * @param {string} pointer
1090
+ */
1091
+ function normalizeStep(value, fallback, pointer) {
1092
+ if (value === undefined) return String(fallback);
1093
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1094
+ if (typeof value === "string" && value.length > 0) return value;
1095
+ throw usageError(pointer, "Expected non-empty string or finite number.");
1096
+ }
1097
+
1098
+ /**
1099
+ * @param {Box} box
1100
+ * @param {[number, number]} target
1101
+ * @returns {[number, number]}
1102
+ */
1103
+ function edgePointToward(box, target) {
1104
+ const cx = centerX(box);
1105
+ const cy = centerY(box);
1106
+ const dx = target[0] - cx;
1107
+ const dy = target[1] - cy;
1108
+ if (Math.abs(dx) / Math.max(1, box.width) > Math.abs(dy) / Math.max(1, box.height)) {
1109
+ return [dx >= 0 ? box.x + box.width : box.x, cy];
1110
+ }
1111
+ return [cx, dy >= 0 ? box.y + box.height : box.y];
1112
+ }
1113
+
1114
+ /**
1115
+ * @param {unknown} legend
1116
+ * @param {Box[]} boxes
1117
+ * @returns {LegendItem[]}
1118
+ */
1119
+ function normalizeLegend(legend, boxes) {
1120
+ if (legend === false) return [];
1121
+ if (legend !== undefined) {
1122
+ const items = requireObjectArray(legend, "/legend");
1123
+ return items.map((item, index) => ({
1124
+ label: requireString(item, "label", `/legend/${index}/label`),
1125
+ type: normalizeType(item.type, `/legend/${index}/type`),
1126
+ }));
1127
+ }
1128
+ const seen = new Set();
1129
+ /** @type {LegendItem[]} */
1130
+ const items = [];
1131
+ for (const box of boxes) {
1132
+ if (!seen.has(box.type)) {
1133
+ seen.add(box.type);
1134
+ items.push({ label: labelForType(box.type), type: box.type });
1135
+ }
1136
+ }
1137
+ return items;
1138
+ }
1139
+
1140
+ /**
1141
+ * @param {LegendItem[]} items
1142
+ * @param {number} x
1143
+ * @param {number} y
1144
+ */
1145
+ function renderArchitectureLegend(items, x, y, columns = 4) {
1146
+ if (items.length === 0) return "";
1147
+ const itemSvg = items.map((item, index) => {
1148
+ const itemX = x + (index % columns) * 200;
1149
+ const itemY = y + Math.floor(index / columns) * 72;
1150
+ const text = layoutText(item.label,160,12,16);
1151
+ return `<g class="node node-${item.type}"><rect x="${itemX}" y="${itemY + 16}" width="16" height="10" rx="2"/>${renderText("legend-"+index,"legend","group",item.label,text,itemX+24+text.width/2,itemY+14,"sublabel")}</g>`;
1152
+ }).join("\n");
1153
+ return `<g class="legend"><text x="${x}" y="${y}" class="label">Legend</text>\n${itemSvg}</g>`;
1154
+ }
1155
+
1156
+ /**
1157
+ * @param {LegendItem[]} items
1158
+ * @param {number} x
1159
+ * @param {number} y
1160
+ */
1161
+ function renderFlowLegend(items, x, y, columns = 4) {
1162
+ if (items.length === 0) return "";
1163
+ const itemSvg = items.map((item, index) => {
1164
+ const itemX = x + (index % columns) * 200;
1165
+ const itemY = y + Math.floor(index / columns) * 72;
1166
+ const text = layoutText(item.label,152,12,16);
1167
+ return `<g class="edge flow-${item.type}"><path d="M ${itemX} ${itemY + 22} L ${itemX + 28} ${itemY + 22}" marker-end="url(#arrow-flow-${item.type})"/>${renderText("flow-legend-"+index,"flow-legend","group",item.label,text,itemX+38+text.width/2,itemY+14,"sublabel")}</g>`;
1168
+ }).join("\n");
1169
+ return `<g class="flow-legend"><text x="${x}" y="${y}" class="label">Flow Legend</text>\n${itemSvg}</g>`;
1170
+ }
1171
+
1172
+ /**
1173
+ * @param {string} type
1174
+ */
1175
+ function labelForType(type) {
1176
+ return ({
1177
+ frontend: "Frontend",
1178
+ backend: "Backend",
1179
+ database: "Database",
1180
+ cloud: "Cloud",
1181
+ security: "Security",
1182
+ messagebus: "Message bus",
1183
+ external: "External",
1184
+ agent: "Agent",
1185
+ model: "Model",
1186
+ memory: "Memory",
1187
+ vectorstore: "Vector store",
1188
+ graphdb: "Graph DB",
1189
+ tool: "Tool",
1190
+ document: "Document",
1191
+ queue: "Queue",
1192
+ browser: "Browser",
1193
+ user: "User",
1194
+ gateway: "Gateway",
1195
+ })[type] ?? type;
1196
+ }
1197
+
1198
+ /**
1199
+ * @param {string} flow
1200
+ */
1201
+ function labelForFlow(flow) {
1202
+ return ({
1203
+ control: "Control",
1204
+ data: "Data",
1205
+ read: "Read",
1206
+ write: "Write",
1207
+ async: "Async",
1208
+ feedback: "Feedback",
1209
+ })[flow] ?? flow;
1210
+ }
1211
+
1212
+ /**
1213
+ * @param {unknown} legend
1214
+ * @param {string[]} flows
1215
+ * @returns {LegendItem[]}
1216
+ */
1217
+ function normalizeFlowLegend(legend, flows) {
1218
+ if (legend === false) return [];
1219
+ if (legend !== undefined) {
1220
+ const items = requireObjectArray(legend, "/flowLegend");
1221
+ return items.map((item, index) => ({
1222
+ label: requireString(item, "label", `/flowLegend/${index}/label`),
1223
+ type: normalizeFlow(item.type, `/flowLegend/${index}/type`) ?? "control",
1224
+ }));
1225
+ }
1226
+ if (flows.length < 2) return [];
1227
+ return flows.map((flow) => ({ label: labelForFlow(flow), type: flow }));
1228
+ }
1229
+
1230
+ /**
1231
+ * @param {unknown} summary
1232
+ * @returns {SummaryCard[]}
1233
+ */
1234
+ function normalizeSummary(summary) {
1235
+ if (summary === undefined) return [];
1236
+ const cards = requireObjectArray(summary, "/summary").slice(0, 3);
1237
+ return cards.map((card, index) => {
1238
+ const items = requireArray(card.items, `/summary/${index}/items`);
1239
+ return {
1240
+ title: requireString(card, "title", `/summary/${index}/title`),
1241
+ type: normalizeType(card.type, `/summary/${index}/type`),
1242
+ items: items.map((item, itemIndex) => {
1243
+ if (typeof item !== "string" || item.length === 0) {
1244
+ throw usageError(`/summary/${index}/items/${itemIndex}`, "Expected non-empty string.");
1245
+ }
1246
+ return item;
1247
+ }),
1248
+ };
1249
+ });
1250
+ }
1251
+
1252
+ /**
1253
+ * @param {Box} from
1254
+ * @param {Box} to
1255
+ * @returns {Array<[number, number]>}
1256
+ */
1257
+ function routeBoxes(from, to) {
1258
+ const result = routeConnector(from, to, routeObstacles.filter((box) => box.id !== from.id && box.id !== to.id), occupiedRoutes);
1259
+ qualityIssues.push(...result.issues);
1260
+ occupiedRoutes.push(result.points);
1261
+ return result.points;
1262
+ }
1263
+
1264
+ /**
1265
+ * @param {Box} box
1266
+ */
1267
+ function centerX(box) {
1268
+ return box.x + box.width / 2;
1269
+ }
1270
+
1271
+ /**
1272
+ * @param {Box} box
1273
+ */
1274
+ function centerY(box) {
1275
+ return box.y + box.height / 2;
1276
+ }
1277
+
1278
+ /**
1279
+ * @param {string} value
1280
+ * @param {number} size
1281
+ */
1282
+ function wrapLabel(value, size) {
1283
+ const words = value.split(/\s+/).filter(Boolean);
1284
+ /** @type {string[]} */
1285
+ const lines = [];
1286
+ let line = "";
1287
+ for (const word of words) {
1288
+ if ((line ? `${line} ${word}` : word).length > size) {
1289
+ if (line) lines.push(line);
1290
+ line = word;
1291
+ } else {
1292
+ line = line ? `${line} ${word}` : word;
1293
+ }
1294
+ }
1295
+ if (line) lines.push(line);
1296
+ return lines.length > 0 ? lines : [value];
1297
+ }
1298
+
1299
+ /**
1300
+ * @param {string} type
1301
+ */
1302
+ function isSemanticNodeType(type) {
1303
+ return ["agent", "model", "memory", "vectorstore", "graphdb", "tool", "document", "queue", "browser", "user", "gateway"].includes(type);
1304
+ }
1305
+
1306
+ /**
1307
+ * @param {unknown} visual
1308
+ * @param {string} pointer
1309
+ */
1310
+ function normalizeVisualStyle(visual, pointer) {
1311
+ if (visual === undefined) return "default";
1312
+ assertObject(visual, pointer);
1313
+ const style = /** @type {Record<string, unknown>} */ (visual).style;
1314
+ if (style === undefined) return "default";
1315
+ if (typeof style !== "string" || !VISUAL_STYLES.has(style)) {
1316
+ throw usageError(`${pointer}/style`, `Unsupported visual style "${String(style)}".`);
1317
+ }
1318
+ return style;
1319
+ }
1320
+
1321
+ /**
1322
+ * @param {unknown} value
1323
+ * @param {string} pointer
1324
+ */
1325
+ function normalizeFlow(value, pointer) {
1326
+ if (value === undefined) return "";
1327
+ if (typeof value !== "string" || !FLOW_TYPES.has(value)) {
1328
+ throw usageError(pointer, `Unsupported flow "${String(value)}".`);
1329
+ }
1330
+ return value;
1331
+ }
1332
+
1333
+ /**
1334
+ * @param {unknown} value
1335
+ * @param {string} pointer
1336
+ */
1337
+ function normalizeType(value, pointer) {
1338
+ if (typeof value !== "string") return "external";
1339
+ if (!NODE_TYPES.has(value)) throw usageError(pointer, `Unsupported node type "${value}".`);
1340
+ return value;
1341
+ }
1342
+
1343
+ /**
1344
+ * @param {unknown} value
1345
+ * @param {string} pointer
1346
+ */
1347
+ function normalizeVariant(value, pointer) {
1348
+ if (value === undefined) return "default";
1349
+ if (typeof value !== "string" || !EDGE_VARIANTS.has(value)) {
1350
+ throw usageError(pointer, `Unsupported edge variant "${String(value)}".`);
1351
+ }
1352
+ return value;
1353
+ }
1354
+
1355
+ /**
1356
+ * @param {unknown} value
1357
+ * @param {string} pointer
1358
+ */
1359
+ function assertObject(value, pointer) {
1360
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1361
+ throw usageError(pointer, "Expected object.");
1362
+ }
1363
+ }
1364
+
1365
+ /**
1366
+ * @param {unknown} value
1367
+ * @param {string} pointer
1368
+ * @returns {unknown[]}
1369
+ */
1370
+ function requireArray(value, pointer) {
1371
+ if (!Array.isArray(value)) throw usageError(pointer, "Expected array.");
1372
+ return value;
1373
+ }
1374
+
1375
+ /**
1376
+ * @param {unknown} value
1377
+ * @param {string} pointer
1378
+ * @returns {Array<Record<string, unknown>>}
1379
+ */
1380
+ function requireObjectArray(value, pointer) {
1381
+ const items = requireArray(value, pointer);
1382
+ for (const [index, item] of items.entries()) {
1383
+ assertObject(item, `${pointer}/${index}`);
1384
+ }
1385
+ return /** @type {Array<Record<string, unknown>>} */ (items);
1386
+ }
1387
+
1388
+ /**
1389
+ * @param {Record<string, unknown>} object
1390
+ * @param {string} key
1391
+ * @param {string} pointer
1392
+ */
1393
+ function requireString(object, key, pointer) {
1394
+ const value = object[key];
1395
+ if (typeof value !== "string" || value.length === 0) {
1396
+ throw usageError(pointer, "Expected non-empty string.");
1397
+ }
1398
+ return value;
1399
+ }
1400
+
1401
+ /**
1402
+ * @param {Record<string, unknown>} object
1403
+ * @param {string} key
1404
+ * @param {string} pointer
1405
+ */
1406
+ function requireId(object, key, pointer) {
1407
+ const value = requireString(object, key, pointer);
1408
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
1409
+ throw usageError(pointer, "Expected id to match ^[a-zA-Z][a-zA-Z0-9_-]*$.");
1410
+ }
1411
+ return value;
1412
+ }
1413
+
1414
+ /**
1415
+ * @param {Record<string, unknown>} object
1416
+ * @param {string} key
1417
+ * @param {string} pointer
1418
+ */
1419
+ function requireNumber(object, key, pointer) {
1420
+ const value = object[key];
1421
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1422
+ throw usageError(pointer, "Expected finite number.");
1423
+ }
1424
+ return value;
1425
+ }
1426
+
1427
+ /**
1428
+ * @param {Set<string> | Map<string, unknown>} seen
1429
+ * @param {string} id
1430
+ * @param {string} pointer
1431
+ */
1432
+ function ensureUnique(seen, id, pointer) {
1433
+ if (seen.has(id)) throw usageError(pointer, `Duplicate id "${id}".`);
1434
+ if (seen instanceof Set) seen.add(id);
1435
+ else seen.set(id, undefined);
1436
+ }
1437
+
1438
+ /**
1439
+ * @param {string} pointer
1440
+ * @param {string} message
1441
+ */
1442
+ function usageError(pointer, message) {
1443
+ return new Error(`${pointer}: ${message}`);
1444
+ }
1445
+
1446
+ /**
1447
+ * @param {string} value
1448
+ */
1449
+ function escapeHtml(value) {
1450
+ return value.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[char] ?? char));
1451
+ }
1452
+
1453
+ /**
1454
+ * @param {{ ok: boolean; type: string; input: string; output: string; manifest: string | null; nodes: number; connectors: number }} report
1455
+ * @param {string} format
1456
+ */
1457
+ function printReport(report, format) {
1458
+ if (format === "json") {
1459
+ console.log(JSON.stringify(report, null, 2));
1460
+ return;
1461
+ }
1462
+ console.log(`Rendered ${report.type} technical diagram`);
1463
+ console.log(`- Output: ${report.output}`);
1464
+ if (report.manifest) console.log(`- Manifest: ${report.manifest}`);
1465
+ console.log(`- Nodes: ${report.nodes}`);
1466
+ console.log(`- Connectors: ${report.connectors}`);
1467
+ }
1468
+
1469
+ /**
1470
+ * @typedef {{
1471
+ * id: string;
1472
+ * x: number;
1473
+ * y: number;
1474
+ * width: number;
1475
+ * height: number;
1476
+ * label: string;
1477
+ * sublabel: string;
1478
+ * type: string;
1479
+ * actor?: string;
1480
+ * step?: string;
1481
+ * kind?: string;
1482
+ * shape?: "box" | "diamond" | "terminal";
1483
+ * }} Box
1484
+ *
1485
+ * @typedef {{
1486
+ * id: string;
1487
+ * from: string;
1488
+ * to: string;
1489
+ * kind?: string;
1490
+ * self?: boolean;
1491
+ * points: Array<[number, number]>;
1492
+ * }} Connector
1493
+ *
1494
+ * @typedef {{
1495
+ * meta: { title: string; subtitle?: string };
1496
+ * lanes: Array<Record<string, unknown>>;
1497
+ * nodes?: Array<Record<string, unknown>>;
1498
+ * edges?: Array<Record<string, unknown>>;
1499
+ * states?: Array<Record<string, unknown>>;
1500
+ * transitions?: Array<Record<string, unknown>>;
1501
+ * summary?: Array<Record<string, unknown>>;
1502
+ * }} LaneDiagram
1503
+ *
1504
+ * @typedef {{
1505
+ * meta: { title: string; subtitle?: string };
1506
+ * participants: Array<Record<string, unknown>>;
1507
+ * messages: Array<Record<string, unknown>>;
1508
+ * }} SequenceDiagram
1509
+ *
1510
+ * @typedef {{
1511
+ * meta: { title: string; subtitle?: string };
1512
+ * stages: Array<Record<string, unknown>>;
1513
+ * nodes: Array<Record<string, unknown>>;
1514
+ * flows: Array<Record<string, unknown>>;
1515
+ * }} DataflowDiagram
1516
+ *
1517
+ * @typedef {{
1518
+ * meta: { title: string; subtitle?: string };
1519
+ * groups?: Array<Record<string, unknown>>;
1520
+ * nodes: Array<Record<string, unknown>>;
1521
+ * connections: Array<Record<string, unknown>>;
1522
+ * legend?: Array<Record<string, unknown>> | false;
1523
+ * flowLegend?: Array<Record<string, unknown>> | false;
1524
+ * summary?: Array<Record<string, unknown>>;
1525
+ * }} ArchitectureDiagram
1526
+ *
1527
+ * @typedef {{
1528
+ * label: string;
1529
+ * type: string;
1530
+ * }} LegendItem
1531
+ *
1532
+ * @typedef {{
1533
+ * title: string;
1534
+ * type: string;
1535
+ * items: string[];
1536
+ * }} SummaryCard
1537
+ */