@bovinphang/frontend-craft 2.8.0 → 2.9.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 (195) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +378 -339
  4. package/CHANGELOG.zh-CN.md +377 -338
  5. package/CONTRIBUTING.md +371 -361
  6. package/CONTRIBUTING.zh-CN.md +371 -361
  7. package/README.md +545 -493
  8. package/README.openclaw.md +359 -320
  9. package/README.openclaw.zh-CN.md +359 -320
  10. package/README.zh-CN.md +545 -493
  11. package/agents/fec-code-reviewer.md +24 -3
  12. package/agents/fec-debugger.md +1 -1
  13. package/agents/fec-doc-updater.md +1 -1
  14. package/agents/fec-e2e-runner.md +1 -1
  15. package/agents/fec-figma-implementer.md +1 -1
  16. package/agents/fec-performance-optimizer.md +1 -1
  17. package/agents/fec-refactor-cleaner.md +4 -1
  18. package/agents/fec-refactoring-expert.md +81 -0
  19. package/agents/fec-security-reviewer.md +17 -1
  20. package/agents/fec-test-planner.md +1 -1
  21. package/agents/fec-typescript-reviewer.md +24 -8
  22. package/agents/fec-ui-checker.md +11 -11
  23. package/commands/fec-debug.md +1 -1
  24. package/commands/fec-refactor-plan.md +17 -0
  25. package/commands/fec-refactor.md +18 -0
  26. package/commands/fec-review.md +40 -10
  27. package/commands/fec-scaffold.md +1 -1
  28. package/commands/fec-smell.md +17 -0
  29. package/commands/fec-tdd.md +6 -2
  30. package/dist/bin/frontend-craft.js +190 -38
  31. package/dist/hooks/fec-run-tests.js +5 -1
  32. package/dist/hooks/fec-session-start.js +1 -1
  33. package/docs/example-prompts.md +157 -140
  34. package/docs/ja-JP/README.md +534 -493
  35. package/docs/ko-KR/README.md +534 -493
  36. package/docs/project-structure.md +151 -131
  37. package/docs/refactoring/README.md +50 -0
  38. package/docs/refactoring/catalog.md +105 -0
  39. package/docs/refactoring/smell-refactoring-matrix.md +41 -0
  40. package/docs/review-and-diagram-workflows.md +39 -0
  41. package/docs/runtimes/antigravity.md +18 -10
  42. package/docs/runtimes/augment.md +18 -10
  43. package/docs/runtimes/claude.md +191 -177
  44. package/docs/runtimes/claude.zh-CN.md +189 -175
  45. package/docs/runtimes/cline.md +18 -10
  46. package/docs/runtimes/codebuddy.md +18 -10
  47. package/docs/runtimes/codex.md +18 -10
  48. package/docs/runtimes/compatibility.md +27 -0
  49. package/docs/runtimes/copilot.md +18 -10
  50. package/docs/runtimes/cursor.md +18 -10
  51. package/docs/runtimes/gemini.md +18 -10
  52. package/docs/runtimes/kilo.md +18 -10
  53. package/docs/runtimes/openclaw.md +18 -10
  54. package/docs/runtimes/opencode.md +18 -10
  55. package/docs/runtimes/qoder.md +18 -19
  56. package/docs/runtimes/trae.md +18 -10
  57. package/docs/runtimes/windsurf.md +18 -10
  58. package/docs/superpowers/plans/2026-09-26-content-integrity.md +145 -0
  59. package/docs/superpowers/plans/2026-09-26-diagram-quality.md +170 -0
  60. package/docs/superpowers/specs/2026-09-26-diagram-quality-design.md +77 -0
  61. package/docs/superpowers/specs/2026-09-26-project-optimization-design.md +95 -0
  62. package/docs/zh-CN/example-prompts.md +157 -140
  63. package/docs/zh-CN/project-structure.md +148 -128
  64. package/docs/zh-CN/review-and-diagram-workflows.md +39 -0
  65. package/docs/zh-TW/README.md +534 -493
  66. package/localized/zh-CN/agents/fec-code-reviewer.md +22 -3
  67. package/localized/zh-CN/agents/fec-refactor-cleaner.md +3 -0
  68. package/localized/zh-CN/agents/fec-refactoring-expert.md +53 -0
  69. package/localized/zh-CN/agents/fec-security-reviewer.md +16 -0
  70. package/localized/zh-CN/agents/fec-typescript-reviewer.md +23 -7
  71. package/localized/zh-CN/commands/fec-refactor-plan.md +15 -0
  72. package/localized/zh-CN/commands/fec-refactor.md +14 -0
  73. package/localized/zh-CN/commands/fec-review.md +40 -9
  74. package/localized/zh-CN/commands/fec-smell.md +13 -0
  75. package/localized/zh-CN/commands/fec-tdd.md +4 -0
  76. package/localized/zh-CN/skills/eval_queries.json +244 -0
  77. package/localized/zh-CN/skills/fec-code-review/SKILL.md +20 -1
  78. package/localized/zh-CN/skills/fec-code-review/references/report-template.md +11 -1
  79. package/localized/zh-CN/skills/fec-code-smells/SKILL.md +33 -0
  80. package/localized/zh-CN/skills/fec-code-smells/references/detection-guide.md +25 -0
  81. package/localized/zh-CN/skills/fec-code-smells/references/smell-catalog.md +817 -0
  82. package/localized/zh-CN/skills/fec-code-smells/references/smell-to-refactoring-map.md +265 -0
  83. package/localized/zh-CN/skills/fec-image-generation/SKILL.md +31 -1
  84. package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/README.md +14 -0
  85. package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/architecture.json +121 -0
  86. package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/invalid-explicit.json +41 -0
  87. package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/sequence.json +53 -0
  88. package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/sequence.mmd +21 -0
  89. package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/workflow.json +98 -0
  90. package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/workflow.mmd +7 -0
  91. package/localized/zh-CN/skills/fec-image-generation/references/diagram-workflows.md +10 -3
  92. package/localized/zh-CN/skills/fec-image-generation/references/html-technical-diagrams.md +8 -1
  93. package/localized/zh-CN/skills/fec-image-generation/references/png-qa-autofix.md +9 -2
  94. package/localized/zh-CN/skills/fec-image-generation/scripts/diagram-browser.mjs +90 -0
  95. package/localized/zh-CN/skills/fec-image-generation/scripts/diagram-layout.mjs +176 -0
  96. package/localized/zh-CN/skills/fec-image-generation/scripts/export-diagram.mjs +48 -80
  97. package/localized/zh-CN/skills/fec-image-generation/scripts/mermaid-render.mjs +64 -0
  98. package/localized/zh-CN/skills/fec-image-generation/scripts/png-qa.mjs +35 -4
  99. package/localized/zh-CN/skills/fec-image-generation/scripts/tech-diagram-render.mjs +193 -53
  100. package/localized/zh-CN/skills/fec-refactor-clean/SKILL.md +3 -0
  101. package/localized/zh-CN/skills/fec-refactoring/SKILL.md +83 -0
  102. package/localized/zh-CN/skills/fec-refactoring/references/principles.md +43 -0
  103. package/localized/zh-CN/skills/fec-refactoring/references/refactoring-vs-feature-change.md +25 -0
  104. package/localized/zh-CN/skills/fec-refactoring/references/when-to-refactor.md +17 -0
  105. package/localized/zh-CN/skills/fec-refactoring/references/workflow.md +59 -0
  106. package/localized/zh-CN/skills/fec-refactoring-api/SKILL.md +25 -0
  107. package/localized/zh-CN/skills/fec-refactoring-api/references/api-refactorings.md +496 -0
  108. package/localized/zh-CN/skills/fec-refactoring-catalog/SKILL.md +21 -0
  109. package/localized/zh-CN/skills/fec-refactoring-catalog/references/catalog-index.md +99 -0
  110. package/localized/zh-CN/skills/fec-refactoring-catalog/references/composition-guide.md +49 -0
  111. package/localized/zh-CN/skills/fec-refactoring-catalog/references/selection-guide.md +41 -0
  112. package/localized/zh-CN/skills/fec-refactoring-control/SKILL.md +25 -0
  113. package/localized/zh-CN/skills/fec-refactoring-control/references/conditional-refactorings.md +299 -0
  114. package/localized/zh-CN/skills/fec-refactoring-data/SKILL.md +25 -0
  115. package/localized/zh-CN/skills/fec-refactoring-data/references/data-refactorings.md +248 -0
  116. package/localized/zh-CN/skills/fec-refactoring-encapsulation/SKILL.md +25 -0
  117. package/localized/zh-CN/skills/fec-refactoring-encapsulation/references/encapsulation-refactorings.md +452 -0
  118. package/localized/zh-CN/skills/fec-refactoring-functions/SKILL.md +25 -0
  119. package/localized/zh-CN/skills/fec-refactoring-functions/references/function-refactorings.md +567 -0
  120. package/localized/zh-CN/skills/fec-refactoring-inheritance/SKILL.md +25 -0
  121. package/localized/zh-CN/skills/fec-refactoring-inheritance/references/inheritance-refactorings.md +550 -0
  122. package/localized/zh-CN/skills/fec-refactoring-move-features/SKILL.md +25 -0
  123. package/localized/zh-CN/skills/fec-refactoring-move-features/references/moving-refactorings.md +453 -0
  124. package/localized/zh-CN/skills/fec-refactoring-validation/SKILL.md +52 -0
  125. package/localized/zh-CN/skills/fec-refactoring-validation/references/behavior-preservation.md +15 -0
  126. package/localized/zh-CN/skills/fec-refactoring-validation/references/refactoring-test-checklist.md +25 -0
  127. package/localized/zh-CN/skills/fec-security-review/SKILL.md +16 -0
  128. package/localized/zh-CN/skills/fec-security-review/references/report-template.md +10 -0
  129. package/localized/zh-CN/skills/fec-tdd-workflow/SKILL.md +3 -0
  130. package/localized/zh-CN/skills/metadata.json +408 -50
  131. package/localized/zh-CN/skills/relations.json +271 -3
  132. package/localized/zh-CN/templates/shared/rules/fec-agent-workflow.md +1 -0
  133. package/openclaw.plugin.json +2 -3
  134. package/package.json +27 -17
  135. package/skills/eval_queries.json +244 -0
  136. package/skills/fec-code-review/SKILL.md +20 -1
  137. package/skills/fec-code-review/references/report-template.md +11 -1
  138. package/skills/fec-code-smells/SKILL.md +33 -0
  139. package/skills/fec-code-smells/references/detection-guide.md +25 -0
  140. package/skills/fec-code-smells/references/smell-catalog.md +861 -0
  141. package/skills/fec-code-smells/references/smell-to-refactoring-map.md +265 -0
  142. package/skills/fec-data-fetching/SKILL.md +1 -1
  143. package/skills/fec-image-generation/SKILL.md +31 -1
  144. package/skills/fec-image-generation/assets/quality-examples/README.md +14 -0
  145. package/skills/fec-image-generation/assets/quality-examples/architecture.json +121 -0
  146. package/skills/fec-image-generation/assets/quality-examples/invalid-explicit.json +41 -0
  147. package/skills/fec-image-generation/assets/quality-examples/sequence.json +53 -0
  148. package/skills/fec-image-generation/assets/quality-examples/sequence.mmd +21 -0
  149. package/skills/fec-image-generation/assets/quality-examples/workflow.json +98 -0
  150. package/skills/fec-image-generation/assets/quality-examples/workflow.mmd +7 -0
  151. package/skills/fec-image-generation/references/diagram-workflows.md +10 -3
  152. package/skills/fec-image-generation/references/html-technical-diagrams.md +8 -1
  153. package/skills/fec-image-generation/references/png-qa-autofix.md +9 -2
  154. package/skills/fec-image-generation/scripts/diagram-browser.mjs +90 -0
  155. package/skills/fec-image-generation/scripts/diagram-layout.mjs +176 -0
  156. package/skills/fec-image-generation/scripts/export-diagram.mjs +48 -80
  157. package/skills/fec-image-generation/scripts/mermaid-render.mjs +64 -0
  158. package/skills/fec-image-generation/scripts/png-qa.mjs +35 -4
  159. package/skills/fec-image-generation/scripts/tech-diagram-render.mjs +193 -53
  160. package/skills/fec-legacy-web-standard/SKILL.md +1 -1
  161. package/skills/fec-refactor-clean/SKILL.md +3 -0
  162. package/skills/fec-refactoring/SKILL.md +83 -0
  163. package/skills/fec-refactoring/references/principles.md +60 -0
  164. package/skills/fec-refactoring/references/refactoring-vs-feature-change.md +27 -0
  165. package/skills/fec-refactoring/references/when-to-refactor.md +31 -0
  166. package/skills/fec-refactoring/references/workflow.md +90 -0
  167. package/skills/fec-refactoring-api/SKILL.md +25 -0
  168. package/skills/fec-refactoring-api/references/api-refactorings.md +496 -0
  169. package/skills/fec-refactoring-catalog/SKILL.md +28 -0
  170. package/skills/fec-refactoring-catalog/references/catalog-index.md +99 -0
  171. package/skills/fec-refactoring-catalog/references/composition-guide.md +49 -0
  172. package/skills/fec-refactoring-catalog/references/selection-guide.md +47 -0
  173. package/skills/fec-refactoring-control/SKILL.md +25 -0
  174. package/skills/fec-refactoring-control/references/conditional-refactorings.md +299 -0
  175. package/skills/fec-refactoring-data/SKILL.md +25 -0
  176. package/skills/fec-refactoring-data/references/data-refactorings.md +248 -0
  177. package/skills/fec-refactoring-encapsulation/SKILL.md +25 -0
  178. package/skills/fec-refactoring-encapsulation/references/encapsulation-refactorings.md +452 -0
  179. package/skills/fec-refactoring-functions/SKILL.md +25 -0
  180. package/skills/fec-refactoring-functions/references/function-refactorings.md +567 -0
  181. package/skills/fec-refactoring-inheritance/SKILL.md +25 -0
  182. package/skills/fec-refactoring-inheritance/references/inheritance-refactorings.md +550 -0
  183. package/skills/fec-refactoring-move-features/SKILL.md +25 -0
  184. package/skills/fec-refactoring-move-features/references/moving-refactorings.md +453 -0
  185. package/skills/fec-refactoring-validation/SKILL.md +59 -0
  186. package/skills/fec-refactoring-validation/references/behavior-preservation.md +53 -0
  187. package/skills/fec-refactoring-validation/references/refactoring-test-checklist.md +42 -0
  188. package/skills/fec-security-review/SKILL.md +16 -0
  189. package/skills/fec-security-review/references/report-template.md +10 -0
  190. package/skills/fec-tdd-workflow/SKILL.md +5 -1
  191. package/skills/metadata.json +434 -71
  192. package/skills/relations.json +264 -0
  193. package/templates/shared/rules/fec-agent-workflow.md +1 -0
  194. package/docs/superpowers/plans/2026-05-29-fec-debug.md +0 -491
  195. package/docs/superpowers/specs/2026-05-29-fec-debug-design.md +0 -340
@@ -3,7 +3,10 @@
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import zlib from "node:zlib";
6
+ import { segmentHits } from "./diagram-layout.mjs";
7
+ import { pathToFileURL } from "node:url";
6
8
 
9
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
7
10
  const args = parseArgs(process.argv.slice(2));
8
11
 
9
12
  if (!args.png || args.help) {
@@ -26,6 +29,8 @@ try {
26
29
  process.exit(1);
27
30
  }
28
31
 
32
+ }
33
+
29
34
  function parseArgs(values) {
30
35
  const parsed = { format: "markdown", strict: false, help: false };
31
36
  for (let index = 0; index < values.length; index += 1) {
@@ -50,6 +55,7 @@ function printHelp() {
50
55
  function readManifest(filePath) {
51
56
  const manifest = JSON.parse(fs.readFileSync(filePath, "utf8"));
52
57
  return {
58
+ ...manifest,
53
59
  canvas: manifest.canvas,
54
60
  boxes: Array.isArray(manifest.boxes) ? manifest.boxes : [],
55
61
  connectors: Array.isArray(manifest.connectors) ? manifest.connectors : [],
@@ -188,6 +194,8 @@ function analyze(image, manifest, strict) {
188
194
  inkCoverage: Number(ink.coverage.toFixed(6)),
189
195
  inkBounds: ink.bounds,
190
196
  },
197
+ measurement: manifest?.measurement ?? "image-only",
198
+ verification: { image: "checked", geometry: manifest ? "checked" : "unavailable", text: manifest?.measurement === "browser" ? "browser-measured" : "estimated-or-unavailable", visual: "pending" },
191
199
  issues,
192
200
  nextActions: [...nextActions],
193
201
  };
@@ -243,7 +251,7 @@ function colorDistance(left, right) {
243
251
  return Math.abs(left.r - right.r) + Math.abs(left.g - right.g) + Math.abs(left.b - right.b) + Math.abs(left.a - right.a);
244
252
  }
245
253
 
246
- function analyzeManifest(image, manifest, issues, nextActions, strict) {
254
+ export function analyzeManifest(image, manifest, issues, nextActions, strict) {
247
255
  if (manifest.canvas && (manifest.canvas.width !== image.width || manifest.canvas.height !== image.height)) {
248
256
  addIssue(
249
257
  issues,
@@ -254,12 +262,26 @@ function analyzeManifest(image, manifest, issues, nextActions, strict) {
254
262
  );
255
263
  }
256
264
 
257
- const boxes = manifest.boxes.map(normalizeBox).filter(Boolean);
265
+ for (const box of manifest.boxes) if (![box.x,box.y,box.width,box.height].every(Number.isFinite) || box.width <= 0 || box.height <= 0) addIssue(issues,nextActions,"invalid-box-geometry",`Invalid geometry for "${box.id}".`,"Correct source dimensions.");
266
+ const allBoxes = manifest.boxes.map(normalizeBox).filter(Boolean);
267
+ const boxes = allBoxes.filter((box) => box.kind !== "group");
268
+ for (const issue of manifest.issues ?? []) addIssue(issues, nextActions, issue.code, issue.message, "Fix the source layout and render again.");
269
+ const labels = manifest.labels ?? [];
270
+ for (const label of labels) {
271
+ const owner = allBoxes.find((box) => box.id === label.owner);
272
+ if (label.kind === "node" && !owner) addIssue(issues,nextActions,"label-owner-missing",`Label "${label.id}" has an unknown node owner.`,"Correct label ownership in the manifest.");
273
+ if (![label.x,label.y,label.width,label.height].every(Number.isFinite) || label.width < 0 || label.height < 0) addIssue(issues,nextActions,"invalid-label-geometry",`Label "${label.id}" has invalid bounds.`,"Measure source labels again.");
274
+ if (label.x < 0 || label.y < 0 || label.x + label.width > image.width || label.y + label.height > image.height) addIssue(issues, nextActions, "label-out-of-bounds", `Label "${label.id}" exceeds the canvas.`, "Expand the canvas or adjust the source label position.");
275
+ if (label.kind === "node" && owner && (label.x < owner.x || label.y < owner.y || label.x + label.width > owner.x + owner.width || label.y + label.height > owner.y + owner.height)) addIssue(issues, nextActions, "node-text-overflow", `Label "${label.id}" exceeds node "${owner.id}".`, "Expand the source node or adjust its text layout.");
276
+ if (Array.isArray(label.lines) && label.lines.join("").replace(/\r?\n/g, "") !== String(label.text ?? "").replace(/\r?\n/g, "")) addIssue(issues, nextActions, "label-content-loss", `Label "${label.id}" does not preserve source text.`, "Restore the complete source label.");
277
+ if (label.kind === "edge") for (const box of boxes) if (overlapArea(label, box) > 0) addIssue(issues, nextActions, "edge-label-collision", `Label "${label.id}" overlaps node "${box.id}".`, "Move the edge label to a clear route segment.");
278
+ }
279
+ for (let i = 0; i < labels.length; i++) for (let j = i + 1; j < labels.length; j++) if (overlapArea(labels[i], labels[j]) > 0) addIssue(issues, nextActions, "text-overlap", `Labels "${labels[i].id}" and "${labels[j].id}" overlap.`, "Increase label spacing in the source.");
258
280
  for (const box of boxes) {
259
281
  if (box.x < 0 || box.y < 0 || box.x + box.width > image.width || box.y + box.height > image.height) {
260
282
  addIssue(issues, nextActions, "box-out-of-bounds", `Box "${box.id}" is outside the canvas.`, "Move out-of-bounds nodes inside the canvas or expand the canvas.");
261
283
  }
262
- if (box.label && estimateLabelOverflow(box, strict)) {
284
+ if (!labels.length && box.label && estimateLabelOverflow(box, strict)) {
263
285
  addIssue(issues, nextActions, "label-overflow", `Box "${box.id}" label is likely to overflow.`, "Wrap or shorten labels, widen boxes, or increase export scale.");
264
286
  }
265
287
  }
@@ -277,9 +299,12 @@ function analyzeManifest(image, manifest, issues, nextActions, strict) {
277
299
  const segments = [];
278
300
  for (const connector of manifest.connectors) {
279
301
  const points = normalizePoints(connector.points);
302
+ if(points.some(point=>point.x<0||point.y<0||point.x>image.width||point.y>image.height)) addIssue(issues,nextActions,"connector-out-of-bounds",`Connector "${connector.id}" exceeds the canvas.`,"Correct explicit waypoints or expand the source canvas.");
303
+ if (points.length < 2 || points.every((point) => point.x === points[0].x && point.y === points[0].y)) addIssue(issues, nextActions, "degenerate-connector", `Connector "${connector.id ?? "unnamed"}" has no visible path.`, "Give self calls a loop and other connectors distinct endpoints.");
280
304
  for (let index = 0; index < points.length - 1; index += 1) {
281
305
  const segment = { connector, a: points[index], b: points[index + 1] };
282
306
  segments.push(segment);
307
+ for (const label of labels) if (segmentHits([segment.a.x,segment.a.y],[segment.b.x,segment.b.y],label)) addIssue(issues,nextActions,"connector-through-text",`Connector "${connector.id}" crosses label "${label.id}".`,"Move the source label or route around its text.");
283
308
  for (const box of boxes) {
284
309
  if (box.id === connector.from || box.id === connector.to) continue;
285
310
  if (segmentIntersectsBox(segment, box)) {
@@ -306,6 +331,7 @@ function normalizeBox(box) {
306
331
  if (!box || typeof box.id !== "string") return null;
307
332
  const normalized = {
308
333
  id: box.id,
334
+ kind: box.kind,
309
335
  x: Number(box.x),
310
336
  y: Number(box.y),
311
337
  width: Number(box.width),
@@ -347,6 +373,10 @@ function overlapArea(left, right) {
347
373
  }
348
374
 
349
375
  function segmentIntersectsBox(segment, box) {
376
+ return segmentHits([segment.a.x,segment.a.y],[segment.b.x,segment.b.y],box);
377
+ }
378
+
379
+ function legacySegmentIntersectsBox(segment, box) {
350
380
  if (pointInsideBox(segment.a, box) || pointInsideBox(segment.b, box)) return true;
351
381
  const edges = [
352
382
  [{ x: box.x, y: box.y }, { x: box.x + box.width, y: box.y }],
@@ -399,7 +429,8 @@ function rangesOverlap(left, right) {
399
429
 
400
430
  function addIssue(issues, nextActions, code, message, action) {
401
431
  issues.push({ code, message, action });
402
- nextActions.add(action);
432
+ if (typeof nextActions.add === "function") nextActions.add(action);
433
+ else if (!nextActions.includes(action)) nextActions.push(action);
403
434
  }
404
435
 
405
436
  function renderMarkdown(report) {
@@ -3,6 +3,19 @@
3
3
 
4
4
  import fs from "node:fs";
5
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 };
6
19
 
7
20
  const ALLOWED_TYPES = new Set(["workflow", "sequence", "dataflow", "lifecycle", "architecture"]);
8
21
  const NODE_TYPES = new Set([
@@ -34,8 +47,12 @@ try {
34
47
  /** @type {SummaryCard[]} */
35
48
  const summary = "summary" in renderResult ? /** @type {SummaryCard[]} */ (renderResult.summary) : [];
36
49
  const style = normalizeVisualStyle(diagram.visual, "/visual");
37
- const html = renderHtml(diagram.meta.title, diagram.meta.subtitle ?? "", renderResult.svg, type, summary, style);
50
+ const rawHtml = renderHtml(diagram.meta.title, diagram.meta.subtitle ?? "", renderResult.svg, type, summary, style);
38
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>`);
39
56
  fs.mkdirSync(path.dirname(path.resolve(outputPath)), { recursive: true });
40
57
  fs.writeFileSync(outputPath, html, "utf8");
41
58
 
@@ -50,6 +67,7 @@ try {
50
67
  input: inputPath,
51
68
  output: outputPath,
52
69
  manifest: args.manifest ?? null,
70
+ quality: { status: qualityIssues.length ? "partial" : "estimated", issues: qualityIssues },
53
71
  nodes: renderResult.manifest.boxes.length,
54
72
  connectors: renderResult.manifest.connectors.length,
55
73
  };
@@ -165,6 +183,7 @@ function renderArchitecture(diagram) {
165
183
  label,
166
184
  sublabel: "",
167
185
  type,
186
+ kind: "group",
168
187
  };
169
188
  groupBoxes.push(box);
170
189
  }
@@ -187,6 +206,14 @@ function renderArchitecture(diagram) {
187
206
  });
188
207
  }
189
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
+
190
217
  /** @type {Connector[]} */
191
218
  const connectors = [];
192
219
  const flowSet = new Set();
@@ -209,11 +236,13 @@ function renderArchitecture(diagram) {
209
236
  const contentBottom = Math.max(360, maxBoxExtent(allBoxes, "y") + 64);
210
237
  const legend = normalizeLegend(diagram.legend, [...boxes.values()]);
211
238
  const flowLegend = normalizeFlowLegend(diagram.flowLegend, [...flowSet]);
212
- const legendWidth = Math.max(88 + legend.length * 118, 88 + flowLegend.length * 128);
213
- const width = Math.max(860, maxBoxExtent(allBoxes, "x") + 64, legendWidth);
214
- const legendSvg = renderArchitectureLegend(legend, 44, contentBottom - 4);
215
- const flowLegendSvg = renderFlowLegend(flowLegend, 44, contentBottom + (legend.length > 0 ? 42 : 0));
216
- const height = contentBottom + (legend.length > 0 ? 54 : 0) + (flowLegend.length > 0 ? 54 : 0);
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;
217
246
  const groupSvg = groupBoxes.map(renderGroup).join("\n");
218
247
  const boxSvg = [...boxes.values()].map(renderBox).join("\n");
219
248
  const svg = wrapSvg(width, height, `${groupSvg}\n${connectionSvg}\n${boxSvg}\n${legendSvg}\n${flowLegendSvg}`);
@@ -249,8 +278,8 @@ function renderLaneDiagram(diagram, itemKey, edgeKey) {
249
278
  assertObject(item, `/${itemKey}/${index}`);
250
279
  return requireNumber(item, "col", `/${itemKey}/${index}/col`);
251
280
  }));
252
- const width = Math.max(780, 190 + (maxCol + 1) * 180);
253
- const height = Math.max(420, 140 + lanes.length * 145);
281
+ let width = Math.max(780, 190 + (maxCol + 1) * 180);
282
+ let height = Math.max(420, 140 + lanes.length * 145);
254
283
  const laneHeight = 118;
255
284
  const top = 72;
256
285
  const left = 142;
@@ -285,6 +314,36 @@ function renderLaneDiagram(diagram, itemKey, edgeKey) {
285
314
  boxes.set(id, box);
286
315
  }
287
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
+
288
347
  /** @type {Connector[]} */
289
348
  const connectors = [];
290
349
  const edgeSvg = edges.map((edge, index) => {
@@ -303,8 +362,8 @@ function renderLaneDiagram(diagram, itemKey, edgeKey) {
303
362
  }).join("\n");
304
363
 
305
364
  const laneSvg = lanes.map((lane, index) => {
306
- const y = top + index * laneHeight;
307
- return `<g class="lane"><rect x="34" y="${y}" width="${width - 68}" height="${laneHeight - 16}" rx="10"/><text x="52" y="${y + 28}" class="label muted">${escapeHtml(String(lane.label))}</text></g>`;
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>`;
308
367
  }).join("\n");
309
368
  const boxSvg = [...boxes.values()].map(renderBox).join("\n");
310
369
  const svg = wrapSvg(width, height, `${laneSvg}\n${edgeSvg}\n${boxSvg}`);
@@ -323,8 +382,8 @@ function renderSequence(diagram) {
323
382
  const participants = requireObjectArray(diagram.participants, "/participants");
324
383
  const messages = requireObjectArray(diagram.messages, "/messages");
325
384
  const ids = new Set();
326
- const width = Math.max(760, 160 + participants.length * 170);
327
- const height = Math.max(420, 170 + messages.length * 82);
385
+ let width = Math.max(760, 160 + participants.length * 170);
386
+ let height = Math.max(420, 170 + messages.length * 82);
328
387
  const top = 74;
329
388
  const laneGap = (width - 160) / Math.max(1, participants.length - 1);
330
389
  /** @type {Map<string, Box>} */
@@ -349,25 +408,40 @@ function renderSequence(diagram) {
349
408
  });
350
409
  }
351
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
+
352
419
  /** @type {Connector[]} */
353
420
  const connectors = [];
354
- 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");
421
+
355
422
  const messageSvg = messages.map((message, index) => {
356
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.");
357
425
  const from = requireId(message, "from", `/messages/${index}/from`);
358
426
  const to = requireId(message, "to", `/messages/${index}/to`);
359
427
  const fromBox = boxes.get(from);
360
428
  const toBox = boxes.get(to);
361
429
  if (!fromBox) throw usageError(`/messages/${index}/from`, `Unknown endpoint "${from}".`);
362
430
  if (!toBox) throw usageError(`/messages/${index}/to`, `Unknown endpoint "${to}".`);
363
- const y = top + 104 + index * 72;
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));
364
435
  /** @type {Array<[number, number]>} */
365
- const points = [[centerX(fromBox), y], [centerX(toBox), y]];
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);
366
438
  const variant = normalizeVariant(message.variant, `/messages/${index}/variant`);
367
- connectors.push({ id: `${from}-${to}-${index}`, from, to, points });
439
+ connectors.push({ id: `${from}-${to}-${index}`, from, to, points, kind: "sequence", self: from === to });
368
440
  return renderConnector(points, variant, typeof message.label === "string" ? message.label : "");
369
441
  }).join("\n");
370
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");
371
445
  const svg = wrapSvg(width, height, `${lifelines}\n${messageSvg}\n${boxSvg}`);
372
446
 
373
447
  return { svg, manifest: manifestFrom(width, height, [...boxes.values()], connectors) };
@@ -410,6 +484,8 @@ function renderDataflow(diagram) {
410
484
  });
411
485
  }
412
486
 
487
+ prepareBoxes(boxes, nodes);
488
+
413
489
  /** @type {Connector[]} */
414
490
  const connectors = [];
415
491
  const stageSvg = stages.map((stage, index) => {
@@ -463,22 +539,45 @@ function countStageBefore(nodes, endIndex, stage) {
463
539
  return count;
464
540
  }
465
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
+
466
560
  /**
467
561
  * @param {Box} box
468
562
  */
469
563
  function renderBox(box) {
470
- const labelLines = wrapLabel(box.label, 18).slice(0, 2);
471
564
  const shape = box.shape ?? "box";
472
565
  const isSemantic = isSemanticNodeType(box.type);
473
- const labelStartY = shape === "diamond" ? centerY(box) - (labelLines.length > 1 ? 5 : -3) : box.y + (isSemantic ? Math.max(30, box.height - 24) : 23);
474
- const labelSvg = labelLines.map((line, index) => `<text x="${centerX(box)}" y="${labelStartY + index * 13}" class="label" text-anchor="middle">${escapeHtml(line)}</text>`).join("\n");
475
- const sublabel = box.sublabel ? `<text x="${centerX(box)}" y="${box.y + box.height - 13}" class="sublabel" text-anchor="middle">${escapeHtml(box.sublabel)}</text>` : "";
476
- const actor = box.actor ? `<text x="${centerX(box)}" y="${box.y - 8}" class="actor" text-anchor="middle">${escapeHtml(box.actor)}</text>` : "";
477
- const step = box.step ? `<g class="step-badge"><circle cx="${box.x + 13}" cy="${box.y + 13}" r="10"/><text x="${box.x + 13}" y="${box.y + 17}" text-anchor="middle">${escapeHtml(box.step)}</text></g>` : "";
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>` : "";
478
577
  const shapeSvg = renderSemanticShape(box) ?? (shape === "diamond"
479
578
  ? `<polygon points="${centerX(box)},${box.y} ${box.x + box.width},${centerY(box)} ${centerX(box)},${box.y + box.height} ${box.x},${centerY(box)}"/>`
480
579
  : `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="${shape === "terminal" ? Math.round(box.height / 2) : 10}"/>`);
481
- return `<g class="node node-${box.type}">${actor}\n${shapeSvg}\n${step}\n${labelSvg}\n${sublabel}</g>`;
580
+ return `<g data-node-id="${escapeHtml(box.id)}" class="node node-${box.type}">${actor}\n${shapeSvg}\n${step}\n${labelSvg}\n${sublabel}</g>`;
482
581
  }
483
582
 
484
583
  /**
@@ -514,8 +613,9 @@ function renderSemanticShape(box) {
514
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"/>`;
515
614
  }
516
615
  if (box.type === "queue") {
517
- const ry = Math.min(18, box.height / 3);
518
- return `<ellipse cx="${box.x + ry}" cy="${cy}" rx="${ry * 0.58}" ry="${ry}"/><rect x="${box.x + ry}" y="${cy - ry}" width="${box.width - ry * 2}" height="${ry * 2}" stroke="none"/><line x1="${box.x + ry}" y1="${cy - ry}" x2="${box.x + box.width - ry}" y2="${cy - ry}"/><line x1="${box.x + ry}" y1="${cy + ry}" x2="${box.x + box.width - ry}" y2="${cy + ry}"/><ellipse cx="${box.x + box.width - ry}" cy="${cy}" rx="${ry * 0.58}" ry="${ry}"/>`;
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}"/>`;
519
619
  }
520
620
  if (box.type === "browser") {
521
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"/>`;
@@ -534,7 +634,7 @@ function renderSemanticShape(box) {
534
634
  * @param {Box} box
535
635
  */
536
636
  function renderGroup(box) {
537
- return `<g class="group group-${box.type}"><rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="14"/><text x="${box.x + 16}" y="${box.y + 26}" class="label muted">${escapeHtml(box.label)}</text></g>`;
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>`;
538
638
  }
539
639
 
540
640
  /**
@@ -544,8 +644,12 @@ function renderGroup(box) {
544
644
  */
545
645
  function renderConnector(points, variant, label, flow = "") {
546
646
  const pathData = points.map(([x, y], index) => `${index === 0 ? "M" : "L"} ${x} ${y}`).join(" ");
547
- const midpoint = points[Math.floor((points.length - 1) / 2)];
548
- const labelSvg = label ? `<text x="${midpoint[0]}" y="${midpoint[1] - 8}" class="edge-label" text-anchor="middle">${escapeHtml(label)}</text>` : "";
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") : "";
549
653
  const marker = flow ? `flow-${flow}` : (variant === "return" ? "default" : variant);
550
654
  return `<g class="edge edge-${variant}${flow ? ` flow-${flow}` : ""}"><path d="${pathData}" marker-end="url(#arrow-${marker})"/>\n${labelSvg}</g>`;
551
655
  }
@@ -556,7 +660,19 @@ function renderConnector(points, variant, label, flow = "") {
556
660
  * @param {string} body
557
661
  */
558
662
  function wrapSvg(width, height, body) {
559
- return `<svg class="tech-diagram" data-export-root="1" role="img" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
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">
560
676
  <defs>
561
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>
562
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>
@@ -760,8 +876,8 @@ button {
760
876
  .node-glyph-outline, .node-glyph-line { fill: none; stroke: currentColor; }
761
877
  .node-chrome { fill: currentColor; opacity: 0.25; stroke: none; }
762
878
  .node-dot { fill: currentColor; stroke: none; opacity: 0.65; }
763
- .label { fill: var(--text); font-weight: 650; font-size: 12px; }
764
- .sublabel, .muted, .actor { fill: var(--muted); font-size: 10px; }
879
+ .label { fill: var(--text); font-weight: 650; font-size: 14px; }
880
+ .sublabel, .muted, .actor { fill: var(--muted); font-size: 12px; }
765
881
  .actor { font-weight: 650; }
766
882
  .step-badge circle { fill: var(--panel); stroke: currentColor; stroke-width: 1.3; }
767
883
  .step-badge text { fill: var(--text); font-size: 9px; font-weight: 750; }
@@ -775,7 +891,7 @@ button {
775
891
  .flow-write path { stroke: var(--flow-write); stroke-dasharray: 5 4; }
776
892
  .flow-async path { stroke: var(--flow-async); stroke-dasharray: 4 4; }
777
893
  .flow-feedback path { stroke: var(--flow-feedback); stroke-width: 2; }
778
- .edge-label { fill: var(--muted); font-size: 10px; paint-order: stroke; stroke: var(--surface); stroke-width: 5px; stroke-linejoin: round; }
894
+ .edge-label { fill: var(--muted); font-size: 12px; paint-order: stroke; stroke: var(--surface); stroke-width: 5px; stroke-linejoin: round; }
779
895
  .lifeline { stroke: color-mix(in srgb, var(--muted), transparent 48%); stroke-dasharray: 4 5; }
780
896
  .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin-top: 12px; }
781
897
  .summary-card { background: var(--panel); border: 1px solid color-mix(in srgb, var(--muted), transparent 74%); border-radius: 8px; padding: 12px; }
@@ -823,16 +939,25 @@ ${summaryHtml}
823
939
  <script>
824
940
  const root = document.documentElement;
825
941
  const stored = localStorage.getItem("fec-tech-diagram-theme");
826
- if (stored === "dark" || stored === "light") root.dataset.theme = stored;
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;
827
946
  document.querySelector('[data-action="theme"]').addEventListener("click", () => {
828
947
  const next = root.dataset.theme === "light" ? "dark" : "light";
829
948
  root.dataset.theme = next;
949
+ diagramSvg.dataset.theme = next;
830
950
  localStorage.setItem("fec-tech-diagram-theme", next);
831
951
  });
832
952
  document.querySelector('[data-action="print"]').addEventListener("click", () => window.print());
833
953
  document.querySelector('[data-action="svg"]').addEventListener("click", () => {
834
954
  const svg = document.querySelector("[data-export-root]");
835
- const source = new XMLSerializer().serializeToString(svg);
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);
836
961
  const blob = new Blob([source], { type: "image/svg+xml" });
837
962
  const link = document.createElement("a");
838
963
  link.href = URL.createObjectURL(blob);
@@ -866,10 +991,20 @@ ${summary.map((card) => ` <article class="summary-card summary-${card.type}">
866
991
  * @param {Connector[]} connectors
867
992
  */
868
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.`});
869
998
  return {
870
- canvas: { width, height },
871
- boxes: boxes.map(({ id, x, y, width: boxWidth, height: boxHeight, label }) => ({ id, x, y, width: boxWidth, height: boxHeight, label })),
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 })),
872
1002
  connectors,
1003
+ labels: textBounds,
1004
+ coordinateSpace: "svg",
1005
+ scale: 1,
1006
+ measurement: "estimated",
1007
+ issues: qualityIssues,
873
1008
  };
874
1009
  }
875
1010
 
@@ -905,14 +1040,15 @@ function routeWithWaypoints(from, to, waypoints, pointer) {
905
1040
  if (!Array.isArray(waypoints)) throw usageError(pointer, "Expected array of [x, y] waypoints.");
906
1041
  /** @type {Array<[number, number]>} */
907
1042
  const parsed = waypoints.map((point, index) => {
908
- if (!Array.isArray(point) || point.length !== 2 || typeof point[0] !== "number" || typeof point[1] !== "number") {
1043
+ if (!Array.isArray(point) || point.length !== 2 || typeof point[0] !== "number" || typeof point[1] !== "number" || !Number.isFinite(point[0]) || !Number.isFinite(point[1])) {
909
1044
  throw usageError(`${pointer}/${index}`, "Expected [x, y] number tuple.");
910
1045
  }
911
1046
  return [point[0], point[1]];
912
1047
  });
913
- const firstTarget = parsed[0] ?? [centerX(to), centerY(to)];
914
- const lastTarget = parsed[parsed.length - 1] ?? [centerX(from), centerY(from)];
915
- return [edgePointToward(from, firstTarget), ...parsed, edgePointToward(to, lastTarget)];
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;
916
1052
  }
917
1053
 
918
1054
  /**
@@ -1006,11 +1142,13 @@ function normalizeLegend(legend, boxes) {
1006
1142
  * @param {number} x
1007
1143
  * @param {number} y
1008
1144
  */
1009
- function renderArchitectureLegend(items, x, y) {
1145
+ function renderArchitectureLegend(items, x, y, columns = 4) {
1010
1146
  if (items.length === 0) return "";
1011
1147
  const itemSvg = items.map((item, index) => {
1012
- const itemX = x + index * 118;
1013
- return `<g class="node node-${item.type}"><rect x="${itemX}" y="${y + 16}" width="16" height="10" rx="2"/><text x="${itemX + 24}" y="${y + 25}" class="sublabel">${escapeHtml(item.label)}</text></g>`;
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>`;
1014
1152
  }).join("\n");
1015
1153
  return `<g class="legend"><text x="${x}" y="${y}" class="label">Legend</text>\n${itemSvg}</g>`;
1016
1154
  }
@@ -1020,11 +1158,13 @@ function renderArchitectureLegend(items, x, y) {
1020
1158
  * @param {number} x
1021
1159
  * @param {number} y
1022
1160
  */
1023
- function renderFlowLegend(items, x, y) {
1161
+ function renderFlowLegend(items, x, y, columns = 4) {
1024
1162
  if (items.length === 0) return "";
1025
1163
  const itemSvg = items.map((item, index) => {
1026
- const itemX = x + index * 128;
1027
- return `<g class="edge flow-${item.type}"><path d="M ${itemX} ${y + 22} L ${itemX + 28} ${y + 22}" marker-end="url(#arrow-flow-${item.type})"/><text x="${itemX + 38}" y="${y + 26}" class="sublabel">${escapeHtml(item.label)}</text></g>`;
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>`;
1028
1168
  }).join("\n");
1029
1169
  return `<g class="flow-legend"><text x="${x}" y="${y}" class="label">Flow Legend</text>\n${itemSvg}</g>`;
1030
1170
  }
@@ -1115,13 +1255,10 @@ function normalizeSummary(summary) {
1115
1255
  * @returns {Array<[number, number]>}
1116
1256
  */
1117
1257
  function routeBoxes(from, to) {
1118
- const start = [centerX(from), centerY(from)];
1119
- const end = [centerX(to), centerY(to)];
1120
- if (Math.abs(start[1] - end[1]) < 18) {
1121
- return [[from.x + from.width, start[1]], [to.x, end[1]]];
1122
- }
1123
- const midX = Math.round((start[0] + end[0]) / 2);
1124
- return [[start[0], start[1]], [midX, start[1]], [midX, end[1]], [end[0], end[1]]];
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;
1125
1262
  }
1126
1263
 
1127
1264
  /**
@@ -1341,6 +1478,7 @@ function printReport(report, format) {
1341
1478
  * type: string;
1342
1479
  * actor?: string;
1343
1480
  * step?: string;
1481
+ * kind?: string;
1344
1482
  * shape?: "box" | "diamond" | "terminal";
1345
1483
  * }} Box
1346
1484
  *
@@ -1348,6 +1486,8 @@ function printReport(report, format) {
1348
1486
  * id: string;
1349
1487
  * from: string;
1350
1488
  * to: string;
1489
+ * kind?: string;
1490
+ * self?: boolean;
1351
1491
  * points: Array<[number, number]>;
1352
1492
  * }} Connector
1353
1493
  *
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fec-legacy-web-standard
3
- description: Use when maintaining or safely modifying existing non-framework frontend code: vanilla JavaScript, jQuery, HTML/CSS, MPA pages, server-rendered templates, legacy plugins, or long-lived code that should stay in its current stack. Use migration skill when planning a move to React/Vue/TypeScript; Chinese triggers include traditional frontend, native JS, jQuery.
3
+ description: "Use when maintaining or safely modifying existing non-framework frontend code: vanilla JavaScript, jQuery, HTML/CSS, MPA pages, server-rendered templates, legacy plugins, or long-lived code that should stay in its current stack. Use migration skill when planning a move to React/Vue/TypeScript; Chinese triggers include traditional frontend, native JS, jQuery."
4
4
  ---
5
5
 
6
6
  # Traditional front-end project specifications (JS + jQuery + HTML)
@@ -49,3 +49,6 @@ When writing a cleanup report, load [references/report-template.md](references/r
49
49
 
50
50
  - Clean reports are saved as `reports/refactor-clean-YYYY-MM-DD-HHmmss.md`.
51
51
  - After the code is cleaned, the relevant verification commands pass, or the cause of the blocking is clearly stated.
52
+ ## Structural Refactoring Boundary
53
+
54
+ This workflow owns proven dead/unused code. When code is live but structurally difficult—long functions, ownership problems, mutable/derived data, conditionals, API shape, or hierarchy design—route it to structural refactoring instead of deleting or rewriting it under cleanup.