@bovinphang/frontend-craft 2.6.0 → 2.8.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 (414) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +72 -1
  4. package/CHANGELOG.zh-CN.md +71 -1
  5. package/README.md +10 -6
  6. package/README.openclaw.md +100 -42
  7. package/README.openclaw.zh-CN.md +100 -42
  8. package/README.zh-CN.md +10 -6
  9. package/agents/fec-architect.md +70 -70
  10. package/agents/fec-code-reviewer.md +82 -82
  11. package/agents/fec-debugger.md +20 -20
  12. package/agents/fec-design-token-mapper.md +36 -36
  13. package/agents/fec-doc-updater.md +13 -13
  14. package/agents/fec-e2e-runner.md +45 -45
  15. package/agents/fec-figma-implementer.md +59 -59
  16. package/agents/fec-performance-optimizer.md +128 -128
  17. package/agents/fec-refactor-cleaner.md +14 -14
  18. package/agents/fec-security-reviewer.md +66 -67
  19. package/agents/fec-test-planner.md +27 -27
  20. package/agents/fec-typescript-reviewer.md +88 -88
  21. package/agents/fec-ui-checker.md +74 -74
  22. package/commands/fec-debug.md +19 -19
  23. package/commands/fec-doc-sync.md +11 -11
  24. package/commands/fec-init.md +37 -38
  25. package/commands/fec-plan.md +58 -58
  26. package/commands/fec-refactor-clean.md +11 -11
  27. package/commands/fec-review.md +41 -41
  28. package/commands/fec-scaffold.md +41 -41
  29. package/commands/fec-tdd.md +13 -13
  30. package/dist/bin/frontend-craft.js +29 -27
  31. package/docs/example-prompts.md +84 -69
  32. package/docs/ja-JP/README.md +56 -52
  33. package/docs/ko-KR/README.md +45 -41
  34. package/docs/project-structure.md +19 -7
  35. package/docs/superpowers/plans/2026-05-29-fec-debug.md +491 -0
  36. package/docs/superpowers/specs/2026-05-29-fec-debug-design.md +340 -0
  37. package/docs/zh-CN/example-prompts.md +84 -69
  38. package/docs/zh-CN/project-structure.md +14 -5
  39. package/docs/zh-TW/README.md +33 -29
  40. package/localized/zh-CN/agents/fec-architect.md +125 -0
  41. package/localized/zh-CN/agents/fec-code-reviewer.md +152 -0
  42. package/localized/zh-CN/agents/fec-debugger.md +44 -0
  43. package/localized/zh-CN/agents/fec-design-token-mapper.md +75 -0
  44. package/localized/zh-CN/agents/fec-doc-updater.md +30 -0
  45. package/localized/zh-CN/agents/fec-e2e-runner.md +107 -0
  46. package/localized/zh-CN/agents/fec-figma-implementer.md +104 -0
  47. package/localized/zh-CN/agents/fec-performance-optimizer.md +293 -0
  48. package/localized/zh-CN/agents/fec-refactor-cleaner.md +32 -0
  49. package/localized/zh-CN/agents/fec-security-reviewer.md +116 -0
  50. package/localized/zh-CN/agents/fec-test-planner.md +50 -0
  51. package/localized/zh-CN/agents/fec-typescript-reviewer.md +142 -0
  52. package/localized/zh-CN/agents/fec-ui-checker.md +120 -0
  53. package/localized/zh-CN/commands/fec-debug.md +30 -0
  54. package/localized/zh-CN/commands/fec-doc-sync.md +19 -0
  55. package/localized/zh-CN/commands/fec-init.md +70 -0
  56. package/localized/zh-CN/commands/fec-plan.md +94 -0
  57. package/localized/zh-CN/commands/fec-refactor-clean.md +19 -0
  58. package/localized/zh-CN/commands/fec-review.md +66 -0
  59. package/localized/zh-CN/commands/fec-scaffold.md +205 -0
  60. package/localized/zh-CN/commands/fec-tdd.md +21 -0
  61. package/localized/zh-CN/skills/TEMPLATE.md +155 -0
  62. package/localized/zh-CN/skills/eval_queries.json +1013 -0
  63. package/localized/zh-CN/skills/fec-accessibility-check/SKILL.md +39 -0
  64. package/localized/zh-CN/skills/fec-accessibility-check/references/report-template.md +23 -0
  65. package/localized/zh-CN/skills/fec-accessibility-check/references/screen-reader-testing.md +33 -0
  66. package/localized/zh-CN/skills/fec-alchemy/SKILL.md +74 -0
  67. package/localized/zh-CN/skills/fec-alchemy/assets/absorption-plan-template.md +47 -0
  68. package/localized/zh-CN/skills/fec-alchemy/assets/intake-template.md +55 -0
  69. package/localized/zh-CN/skills/fec-alchemy/assets/review-checklist.md +31 -0
  70. package/localized/zh-CN/skills/fec-alchemy/references/methodology.md +94 -0
  71. package/localized/zh-CN/skills/fec-alchemy/references/originality-and-licensing.md +57 -0
  72. package/localized/zh-CN/skills/fec-api-integration/SKILL.md +78 -0
  73. package/localized/zh-CN/skills/fec-api-integration/references/integration-patterns.md +80 -0
  74. package/localized/zh-CN/skills/fec-backend-requirements-handoff/SKILL.md +45 -0
  75. package/localized/zh-CN/skills/fec-browser-storage/SKILL.md +34 -0
  76. package/localized/zh-CN/skills/fec-browser-storage/references/storage-patterns.md +65 -0
  77. package/localized/zh-CN/skills/fec-canvas-threejs/SKILL.md +66 -0
  78. package/localized/zh-CN/skills/fec-canvas-threejs/references/rendering-patterns.md +67 -0
  79. package/localized/zh-CN/skills/fec-canvas-threejs/references/shader-webgl-patterns.md +67 -0
  80. package/localized/zh-CN/skills/fec-code-review/SKILL.md +143 -0
  81. package/localized/zh-CN/skills/fec-code-review/references/report-template.md +29 -0
  82. package/localized/zh-CN/skills/fec-component-testing/SKILL.md +119 -0
  83. package/localized/zh-CN/skills/fec-data-fetching/SKILL.md +62 -0
  84. package/localized/zh-CN/skills/fec-data-fetching/references/query-patterns.md +103 -0
  85. package/localized/zh-CN/skills/fec-debug-framework/SKILL.md +157 -0
  86. package/localized/zh-CN/skills/fec-debug-framework/references/report-template.md +58 -0
  87. package/localized/zh-CN/skills/fec-dependency-upgrade/SKILL.md +34 -0
  88. package/localized/zh-CN/skills/fec-dependency-upgrade/references/dependency-upgrade-workflow.md +56 -0
  89. package/localized/zh-CN/skills/fec-doc-sync/SKILL.md +39 -0
  90. package/localized/zh-CN/skills/fec-drawio-studio/SKILL.md +73 -0
  91. package/localized/zh-CN/skills/fec-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
  92. package/localized/zh-CN/skills/fec-drawio-studio/data/brand-icons.json +878 -0
  93. package/localized/zh-CN/skills/fec-drawio-studio/data/shape-index.json +83570 -0
  94. package/localized/zh-CN/skills/fec-drawio-studio/references/data-residency.md +27 -0
  95. package/localized/zh-CN/skills/fec-drawio-studio/references/diagram-patterns.md +60 -0
  96. package/localized/zh-CN/skills/fec-drawio-studio/references/flowchart-quality.md +49 -0
  97. package/localized/zh-CN/skills/fec-drawio-studio/references/layout-and-export.md +72 -0
  98. package/localized/zh-CN/skills/fec-drawio-studio/references/troubleshooting.md +29 -0
  99. package/localized/zh-CN/skills/fec-drawio-studio/references/xml-and-mermaid.md +58 -0
  100. package/localized/zh-CN/skills/fec-drawio-studio/scripts/brand-symbols.mjs +99 -0
  101. package/localized/zh-CN/skills/fec-drawio-studio/scripts/diagram-lint.mjs +205 -0
  102. package/localized/zh-CN/skills/fec-drawio-studio/scripts/diagram-url.mjs +102 -0
  103. package/localized/zh-CN/skills/fec-drawio-studio/scripts/drawio-export.mjs +134 -0
  104. package/localized/zh-CN/skills/fec-drawio-studio/scripts/layout-graph.mjs +107 -0
  105. package/localized/zh-CN/skills/fec-drawio-studio/scripts/png-embed-fix.mjs +26 -0
  106. package/localized/zh-CN/skills/fec-drawio-studio/scripts/scan-go-packages.mjs +36 -0
  107. package/localized/zh-CN/skills/fec-drawio-studio/scripts/scan-js-modules.mjs +52 -0
  108. package/localized/zh-CN/skills/fec-drawio-studio/scripts/scan-python-classes.mjs +30 -0
  109. package/localized/zh-CN/skills/fec-drawio-studio/scripts/scan-python-modules.mjs +55 -0
  110. package/localized/zh-CN/skills/fec-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
  111. package/localized/zh-CN/skills/fec-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
  112. package/localized/zh-CN/skills/fec-drawio-studio/scripts/shape-query.mjs +121 -0
  113. package/localized/zh-CN/skills/fec-drawio-studio/scripts/studio-core.mjs +139 -0
  114. package/localized/zh-CN/skills/fec-drawio-studio/styles/built-in/corporate.json +49 -0
  115. package/localized/zh-CN/skills/fec-drawio-studio/styles/built-in/default.json +49 -0
  116. package/localized/zh-CN/skills/fec-drawio-studio/styles/built-in/handdrawn.json +49 -0
  117. package/localized/zh-CN/skills/fec-drawio-studio/styles/schema.json +112 -0
  118. package/localized/zh-CN/skills/fec-e2e-testing/SKILL.md +42 -0
  119. package/localized/zh-CN/skills/fec-e2e-testing/references/e2e-ci-reporting.md +62 -0
  120. package/localized/zh-CN/skills/fec-e2e-testing/references/e2e-special-scenarios.md +52 -0
  121. package/localized/zh-CN/skills/fec-e2e-testing/references/e2e-visual-regression.md +60 -0
  122. package/localized/zh-CN/skills/fec-e2e-testing/references/playwright-patterns.md +95 -0
  123. package/localized/zh-CN/skills/fec-form-handling/SKILL.md +76 -0
  124. package/localized/zh-CN/skills/fec-form-handling/references/advanced-form-patterns.md +101 -0
  125. package/localized/zh-CN/skills/fec-image-generation/SKILL.md +80 -0
  126. package/localized/zh-CN/skills/fec-image-generation/assets/interactive-diagram.html +371 -0
  127. package/localized/zh-CN/skills/fec-image-generation/references/artifact-routing.md +80 -0
  128. package/localized/zh-CN/skills/fec-image-generation/references/diagram-workflows.md +74 -0
  129. package/localized/zh-CN/skills/fec-image-generation/references/html-technical-diagrams.md +236 -0
  130. package/localized/zh-CN/skills/fec-image-generation/references/png-qa-autofix.md +30 -0
  131. package/localized/zh-CN/skills/fec-image-generation/scripts/export-diagram.mjs +364 -0
  132. package/localized/zh-CN/skills/fec-image-generation/scripts/interactive-diagram-server.mjs +654 -0
  133. package/localized/zh-CN/skills/fec-image-generation/scripts/png-qa.mjs +431 -0
  134. package/localized/zh-CN/skills/fec-image-generation/scripts/tech-diagram-render.mjs +1397 -0
  135. package/localized/zh-CN/skills/fec-implement-from-design/SKILL.md +129 -0
  136. package/localized/zh-CN/skills/fec-implement-from-design/references/design-plan-template.md +36 -0
  137. package/localized/zh-CN/skills/fec-legacy-to-modern-migration/SKILL.md +46 -0
  138. package/localized/zh-CN/skills/fec-legacy-to-modern-migration/references/migration-execution-checklist.md +110 -0
  139. package/localized/zh-CN/skills/fec-legacy-to-modern-migration/references/migration-report-template.md +41 -0
  140. package/localized/zh-CN/skills/fec-legacy-to-modern-migration/references/migration-strategy-and-mapping.md +48 -0
  141. package/localized/zh-CN/skills/fec-legacy-web-standard/SKILL.md +35 -0
  142. package/localized/zh-CN/skills/fec-legacy-web-standard/references/legacy-patterns.md +85 -0
  143. package/localized/zh-CN/skills/fec-list-virtualization/SKILL.md +34 -0
  144. package/localized/zh-CN/skills/fec-list-virtualization/references/virtualization-patterns.md +80 -0
  145. package/localized/zh-CN/skills/fec-monorepo-project-standard/SKILL.md +142 -0
  146. package/localized/zh-CN/skills/fec-motion-interaction/SKILL.md +73 -0
  147. package/localized/zh-CN/skills/fec-motion-interaction/references/motion-patterns.md +112 -0
  148. package/localized/zh-CN/skills/fec-nextjs-project-standard/SKILL.md +123 -0
  149. package/localized/zh-CN/skills/fec-nuxt-project-standard/SKILL.md +122 -0
  150. package/localized/zh-CN/skills/fec-performance-optimization/SKILL.md +80 -0
  151. package/localized/zh-CN/skills/fec-performance-optimization/references/framework-performance-patterns.md +38 -0
  152. package/localized/zh-CN/skills/fec-performance-optimization/references/report-template.md +59 -0
  153. package/localized/zh-CN/skills/fec-pwa-implementation/SKILL.md +35 -0
  154. package/localized/zh-CN/skills/fec-pwa-implementation/references/manifest-and-icons.md +53 -0
  155. package/localized/zh-CN/skills/fec-pwa-implementation/references/service-worker-workbox.md +68 -0
  156. package/localized/zh-CN/skills/fec-react-project-standard/SKILL.md +91 -0
  157. package/localized/zh-CN/skills/fec-react-project-standard/references/react-performance-patterns.md +35 -0
  158. package/localized/zh-CN/skills/fec-react-project-standard/references/react-project-structure.md +197 -0
  159. package/localized/zh-CN/skills/fec-react-project-standard/references/react-quality-patterns.md +131 -0
  160. package/localized/zh-CN/skills/fec-react-project-standard/references/react-runtime-patterns.md +293 -0
  161. package/localized/zh-CN/skills/fec-refactor-clean/SKILL.md +51 -0
  162. package/localized/zh-CN/skills/fec-refactor-clean/references/report-template.md +42 -0
  163. package/localized/zh-CN/skills/fec-responsive-layout/SKILL.md +34 -0
  164. package/localized/zh-CN/skills/fec-responsive-layout/references/responsive-layout-patterns.md +70 -0
  165. package/localized/zh-CN/skills/fec-route-protection/SKILL.md +135 -0
  166. package/localized/zh-CN/skills/fec-security-review/SKILL.md +38 -0
  167. package/localized/zh-CN/skills/fec-security-review/references/report-template.md +27 -0
  168. package/localized/zh-CN/skills/fec-security-review/references/security-checklist.md +64 -0
  169. package/localized/zh-CN/skills/fec-source-driven-development/SKILL.md +87 -0
  170. package/localized/zh-CN/skills/fec-state-management/SKILL.md +140 -0
  171. package/localized/zh-CN/skills/fec-state-management/references/state-patterns.md +109 -0
  172. package/localized/zh-CN/skills/fec-storybook-component-doc/SKILL.md +37 -0
  173. package/localized/zh-CN/skills/fec-storybook-component-doc/references/story-patterns.md +118 -0
  174. package/localized/zh-CN/skills/fec-svg-animation/SKILL.md +148 -0
  175. package/localized/zh-CN/skills/fec-tailwind-design-system/SKILL.md +34 -0
  176. package/localized/zh-CN/skills/fec-tailwind-design-system/references/tailwind-system-patterns.md +59 -0
  177. package/localized/zh-CN/skills/fec-tdd-workflow/SKILL.md +52 -0
  178. package/localized/zh-CN/skills/fec-testing-strategy/SKILL.md +45 -0
  179. package/localized/zh-CN/skills/fec-typescript-project-standard/SKILL.md +54 -0
  180. package/{skills/fec-typescript-type-safety → localized/zh-CN/skills/fec-typescript-project-standard}/references/type-safety-patterns.md +1 -1
  181. package/{skills/fec-typescript-type-safety/SKILL.md → localized/zh-CN/skills/fec-typescript-project-standard/references/type-safety.md} +5 -14
  182. package/localized/zh-CN/skills/fec-ui-design/SKILL.md +108 -0
  183. package/localized/zh-CN/skills/fec-ui-design/data/product-rules.json +231 -0
  184. package/localized/zh-CN/skills/fec-ui-design/data/stack-ui-rules.json +146 -0
  185. package/localized/zh-CN/skills/fec-ui-design/data/style-archetypes.json +373 -0
  186. package/localized/zh-CN/skills/fec-ui-design/data/ux-quality-rules.json +203 -0
  187. package/localized/zh-CN/skills/fec-ui-design/references/design-intelligence.md +97 -0
  188. package/localized/zh-CN/skills/fec-ui-design/references/master-page-overrides.md +30 -0
  189. package/localized/zh-CN/skills/fec-ui-design/references/pre-delivery-checklist.md +39 -0
  190. package/localized/zh-CN/skills/fec-ui-design/scripts/design-system.mjs +648 -0
  191. package/localized/zh-CN/skills/fec-validation-fix/SKILL.md +51 -0
  192. package/localized/zh-CN/skills/fec-validation-fix/references/report-template.md +43 -0
  193. package/localized/zh-CN/skills/fec-vite-project-standard/SKILL.md +83 -0
  194. package/localized/zh-CN/skills/fec-vue3-project-standard/SKILL.md +58 -0
  195. package/localized/zh-CN/skills/fec-vue3-project-standard/references/vue3-composables-routing.md +162 -0
  196. package/localized/zh-CN/skills/fec-vue3-project-standard/references/vue3-performance-patterns.md +13 -0
  197. package/localized/zh-CN/skills/fec-vue3-project-standard/references/vue3-project-structure.md +176 -0
  198. package/localized/zh-CN/skills/fec-vue3-project-standard/references/vue3-state-api-quality.md +216 -0
  199. package/localized/zh-CN/skills/fec-web-video-presentation/SKILL.md +61 -0
  200. package/localized/zh-CN/skills/fec-web-video-presentation/data/starter-themes.json +59 -0
  201. package/localized/zh-CN/skills/fec-web-video-presentation/references/chapter-craft.md +37 -0
  202. package/localized/zh-CN/skills/fec-web-video-presentation/references/content-plan.md +38 -0
  203. package/localized/zh-CN/skills/fec-web-video-presentation/references/recording-and-audio.md +33 -0
  204. package/localized/zh-CN/skills/fec-web-video-presentation/references/theme-system.md +36 -0
  205. package/localized/zh-CN/skills/fec-web-video-presentation/scripts/create-video-presentation.mjs +430 -0
  206. package/localized/zh-CN/skills/fec-web-workers/SKILL.md +35 -0
  207. package/localized/zh-CN/skills/fec-web-workers/references/advanced-workers.md +54 -0
  208. package/localized/zh-CN/skills/fec-web-workers/references/basic-worker.md +67 -0
  209. package/localized/zh-CN/skills/metadata.json +1646 -0
  210. package/localized/zh-CN/skills/relations.json +755 -0
  211. package/localized/zh-CN/templates/claude/CLAUDE.md +106 -0
  212. package/localized/zh-CN/templates/claude/settings.json +62 -0
  213. package/localized/zh-CN/templates/codex/AGENTS.md +105 -0
  214. package/localized/zh-CN/templates/codex/config.toml +32 -0
  215. package/localized/zh-CN/templates/openclaw/AGENTS.md +101 -0
  216. package/localized/zh-CN/templates/openclaw/OPENCLAW-CONFIG.md +12 -0
  217. package/localized/zh-CN/templates/shared/rules/fec-agent-workflow.md +54 -0
  218. package/localized/zh-CN/templates/shared/rules/fec-api-layer.md +99 -0
  219. package/localized/zh-CN/templates/shared/rules/fec-ci-cd.md +40 -0
  220. package/localized/zh-CN/templates/shared/rules/fec-code-comments.md +33 -0
  221. package/localized/zh-CN/templates/shared/rules/fec-design-system.md +72 -0
  222. package/localized/zh-CN/templates/shared/rules/fec-error-handling.md +56 -0
  223. package/localized/zh-CN/templates/shared/rules/fec-git-conventions.md +73 -0
  224. package/localized/zh-CN/templates/shared/rules/fec-i18n.md +64 -0
  225. package/localized/zh-CN/templates/shared/rules/fec-naming-conventions.md +120 -0
  226. package/localized/zh-CN/templates/shared/rules/fec-performance.md +101 -0
  227. package/localized/zh-CN/templates/shared/rules/fec-react.md +82 -0
  228. package/localized/zh-CN/templates/shared/rules/fec-refactoring.md +47 -0
  229. package/localized/zh-CN/templates/shared/rules/fec-rendering-patterns.md +40 -0
  230. package/localized/zh-CN/templates/shared/rules/fec-responsive-design.md +50 -0
  231. package/localized/zh-CN/templates/shared/rules/fec-source-driven-development.md +31 -0
  232. package/localized/zh-CN/templates/shared/rules/fec-state-management.md +60 -0
  233. package/localized/zh-CN/templates/shared/rules/fec-testing.md +98 -0
  234. package/localized/zh-CN/templates/shared/rules/fec-typescript.md +112 -0
  235. package/localized/zh-CN/templates/shared/rules/fec-vue.md +64 -0
  236. package/localized/zh-CN/templates/shared/rules/fec-working-modes.md +50 -0
  237. package/openclaw.plugin.json +1 -1
  238. package/package.json +4 -3
  239. package/skills/TEMPLATE.md +74 -74
  240. package/skills/eval_queries.json +717 -619
  241. package/skills/fec-accessibility-check/SKILL.md +21 -21
  242. package/skills/fec-accessibility-check/references/report-template.md +12 -12
  243. package/skills/fec-accessibility-check/references/screen-reader-testing.md +24 -24
  244. package/skills/fec-alchemy/SKILL.md +50 -50
  245. package/skills/fec-alchemy/assets/absorption-plan-template.md +32 -32
  246. package/skills/fec-alchemy/assets/intake-template.md +55 -55
  247. package/skills/fec-alchemy/assets/review-checklist.md +23 -23
  248. package/skills/fec-alchemy/references/methodology.md +66 -66
  249. package/skills/fec-alchemy/references/originality-and-licensing.md +42 -42
  250. package/skills/fec-api-integration/SKILL.md +61 -61
  251. package/skills/fec-api-integration/references/integration-patterns.md +30 -30
  252. package/skills/fec-backend-requirements-handoff/SKILL.md +45 -0
  253. package/skills/fec-browser-storage/SKILL.md +16 -16
  254. package/skills/fec-browser-storage/references/storage-patterns.md +16 -16
  255. package/skills/fec-canvas-threejs/SKILL.md +21 -21
  256. package/skills/fec-canvas-threejs/references/rendering-patterns.md +10 -10
  257. package/skills/fec-canvas-threejs/references/shader-webgl-patterns.md +37 -37
  258. package/skills/fec-code-review/SKILL.md +94 -94
  259. package/skills/fec-code-review/references/report-template.md +13 -13
  260. package/skills/fec-component-testing/SKILL.md +39 -39
  261. package/skills/fec-data-fetching/SKILL.md +16 -16
  262. package/skills/fec-data-fetching/references/query-patterns.md +27 -27
  263. package/skills/fec-debug-framework/SKILL.md +95 -95
  264. package/skills/fec-debug-framework/references/report-template.md +34 -34
  265. package/skills/fec-dependency-upgrade/SKILL.md +17 -17
  266. package/skills/fec-dependency-upgrade/references/dependency-upgrade-workflow.md +31 -31
  267. package/skills/fec-doc-sync/SKILL.md +23 -23
  268. package/skills/fec-drawio-studio/SKILL.md +73 -0
  269. package/skills/fec-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
  270. package/skills/fec-drawio-studio/data/brand-icons.json +878 -0
  271. package/skills/fec-drawio-studio/data/shape-index.json +83570 -0
  272. package/skills/fec-drawio-studio/references/data-residency.md +27 -0
  273. package/skills/fec-drawio-studio/references/diagram-patterns.md +60 -0
  274. package/skills/fec-drawio-studio/references/flowchart-quality.md +49 -0
  275. package/skills/fec-drawio-studio/references/layout-and-export.md +72 -0
  276. package/skills/fec-drawio-studio/references/troubleshooting.md +29 -0
  277. package/skills/fec-drawio-studio/references/xml-and-mermaid.md +58 -0
  278. package/skills/fec-drawio-studio/scripts/brand-symbols.mjs +99 -0
  279. package/skills/fec-drawio-studio/scripts/diagram-lint.mjs +205 -0
  280. package/skills/fec-drawio-studio/scripts/diagram-url.mjs +102 -0
  281. package/skills/fec-drawio-studio/scripts/drawio-export.mjs +134 -0
  282. package/skills/fec-drawio-studio/scripts/layout-graph.mjs +107 -0
  283. package/skills/fec-drawio-studio/scripts/png-embed-fix.mjs +26 -0
  284. package/skills/fec-drawio-studio/scripts/scan-go-packages.mjs +36 -0
  285. package/skills/fec-drawio-studio/scripts/scan-js-modules.mjs +52 -0
  286. package/skills/fec-drawio-studio/scripts/scan-python-classes.mjs +30 -0
  287. package/skills/fec-drawio-studio/scripts/scan-python-modules.mjs +55 -0
  288. package/skills/fec-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
  289. package/skills/fec-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
  290. package/skills/fec-drawio-studio/scripts/shape-query.mjs +121 -0
  291. package/skills/fec-drawio-studio/scripts/studio-core.mjs +139 -0
  292. package/skills/fec-drawio-studio/styles/built-in/corporate.json +49 -0
  293. package/skills/fec-drawio-studio/styles/built-in/default.json +49 -0
  294. package/skills/fec-drawio-studio/styles/built-in/handdrawn.json +49 -0
  295. package/skills/fec-drawio-studio/styles/schema.json +112 -0
  296. package/skills/fec-e2e-testing/SKILL.md +24 -22
  297. package/skills/fec-e2e-testing/references/e2e-ci-reporting.md +62 -0
  298. package/skills/fec-e2e-testing/references/e2e-special-scenarios.md +52 -0
  299. package/skills/fec-e2e-testing/references/e2e-visual-regression.md +60 -0
  300. package/skills/fec-e2e-testing/references/playwright-patterns.md +6 -6
  301. package/skills/fec-form-handling/SKILL.md +21 -21
  302. package/skills/fec-form-handling/references/advanced-form-patterns.md +41 -41
  303. package/skills/fec-image-generation/SKILL.md +81 -0
  304. package/skills/fec-image-generation/assets/interactive-diagram.html +371 -0
  305. package/skills/fec-image-generation/references/artifact-routing.md +80 -0
  306. package/skills/fec-image-generation/references/diagram-workflows.md +74 -0
  307. package/skills/fec-image-generation/references/html-technical-diagrams.md +236 -0
  308. package/skills/fec-image-generation/references/png-qa-autofix.md +30 -0
  309. package/skills/fec-image-generation/scripts/export-diagram.mjs +364 -0
  310. package/skills/fec-image-generation/scripts/interactive-diagram-server.mjs +654 -0
  311. package/skills/fec-image-generation/scripts/png-qa.mjs +431 -0
  312. package/skills/fec-image-generation/scripts/tech-diagram-render.mjs +1397 -0
  313. package/skills/fec-implement-from-design/SKILL.md +81 -74
  314. package/skills/fec-implement-from-design/references/design-plan-template.md +16 -16
  315. package/skills/fec-legacy-to-modern-migration/SKILL.md +29 -173
  316. package/skills/fec-legacy-to-modern-migration/references/migration-execution-checklist.md +110 -0
  317. package/skills/fec-legacy-to-modern-migration/references/migration-report-template.md +37 -37
  318. package/skills/fec-legacy-to-modern-migration/references/migration-strategy-and-mapping.md +48 -0
  319. package/skills/fec-legacy-web-standard/SKILL.md +17 -17
  320. package/skills/fec-legacy-web-standard/references/legacy-patterns.md +16 -16
  321. package/skills/fec-list-virtualization/SKILL.md +16 -16
  322. package/skills/fec-list-virtualization/references/virtualization-patterns.md +31 -31
  323. package/skills/fec-monorepo-project-standard/SKILL.md +52 -52
  324. package/skills/fec-motion-interaction/SKILL.md +51 -44
  325. package/skills/fec-motion-interaction/references/motion-patterns.md +11 -11
  326. package/skills/fec-nextjs-project-standard/SKILL.md +64 -58
  327. package/skills/fec-nuxt-project-standard/SKILL.md +57 -57
  328. package/skills/fec-performance-optimization/SKILL.md +54 -50
  329. package/skills/fec-performance-optimization/references/framework-performance-patterns.md +38 -0
  330. package/skills/fec-performance-optimization/references/report-template.md +34 -28
  331. package/skills/fec-pwa-implementation/SKILL.md +17 -17
  332. package/skills/fec-pwa-implementation/references/manifest-and-icons.md +12 -12
  333. package/skills/fec-pwa-implementation/references/service-worker-workbox.md +13 -13
  334. package/skills/fec-react-project-standard/SKILL.md +72 -64
  335. package/skills/fec-react-project-standard/references/react-performance-patterns.md +35 -0
  336. package/skills/fec-react-project-standard/references/react-project-structure.md +197 -0
  337. package/skills/fec-react-project-standard/references/react-quality-patterns.md +131 -0
  338. package/skills/fec-react-project-standard/references/react-runtime-patterns.md +293 -0
  339. package/skills/fec-refactor-clean/SKILL.md +30 -30
  340. package/skills/fec-refactor-clean/references/report-template.md +23 -23
  341. package/skills/fec-responsive-layout/SKILL.md +17 -17
  342. package/skills/fec-responsive-layout/references/responsive-layout-patterns.md +39 -28
  343. package/skills/fec-route-protection/SKILL.md +26 -26
  344. package/skills/fec-security-review/SKILL.md +20 -20
  345. package/skills/fec-security-review/references/report-template.md +12 -12
  346. package/skills/fec-security-review/references/security-checklist.md +27 -27
  347. package/skills/fec-source-driven-development/SKILL.md +66 -40
  348. package/skills/fec-state-management/SKILL.md +34 -34
  349. package/skills/fec-state-management/references/state-patterns.md +19 -19
  350. package/skills/fec-storybook-component-doc/SKILL.md +19 -19
  351. package/skills/fec-storybook-component-doc/references/story-patterns.md +12 -12
  352. package/skills/fec-svg-animation/SKILL.md +32 -32
  353. package/skills/fec-tailwind-design-system/SKILL.md +17 -17
  354. package/skills/fec-tailwind-design-system/references/tailwind-system-patterns.md +36 -32
  355. package/skills/fec-tdd-workflow/SKILL.md +28 -28
  356. package/skills/fec-testing-strategy/SKILL.md +29 -29
  357. package/skills/fec-typescript-project-standard/SKILL.md +54 -0
  358. package/skills/fec-typescript-project-standard/references/type-safety-patterns.md +102 -0
  359. package/skills/fec-typescript-project-standard/references/type-safety.md +148 -0
  360. package/skills/fec-ui-design/SKILL.md +90 -72
  361. package/skills/fec-ui-design/data/style-archetypes.json +40 -0
  362. package/skills/fec-ui-design/data/ux-quality-rules.json +35 -0
  363. package/skills/fec-ui-design/references/design-intelligence.md +81 -21
  364. package/skills/fec-ui-design/references/master-page-overrides.md +15 -15
  365. package/skills/fec-ui-design/references/pre-delivery-checklist.md +38 -32
  366. package/skills/fec-ui-design/scripts/design-system.mjs +119 -1
  367. package/skills/fec-validation-fix/SKILL.md +26 -26
  368. package/skills/fec-validation-fix/references/report-template.md +25 -25
  369. package/skills/fec-vite-project-standard/SKILL.md +39 -39
  370. package/skills/fec-vue3-project-standard/SKILL.md +39 -30
  371. package/skills/fec-vue3-project-standard/references/vue3-composables-routing.md +163 -0
  372. package/skills/fec-vue3-project-standard/references/vue3-performance-patterns.md +13 -0
  373. package/skills/fec-vue3-project-standard/references/vue3-project-structure.md +177 -0
  374. package/skills/fec-vue3-project-standard/references/vue3-state-api-quality.md +216 -0
  375. package/skills/fec-web-video-presentation/SKILL.md +61 -0
  376. package/skills/fec-web-video-presentation/data/starter-themes.json +59 -0
  377. package/skills/fec-web-video-presentation/references/chapter-craft.md +37 -0
  378. package/skills/fec-web-video-presentation/references/content-plan.md +38 -0
  379. package/skills/fec-web-video-presentation/references/recording-and-audio.md +33 -0
  380. package/skills/fec-web-video-presentation/references/theme-system.md +36 -0
  381. package/skills/fec-web-video-presentation/scripts/create-video-presentation.mjs +430 -0
  382. package/skills/fec-web-workers/SKILL.md +16 -16
  383. package/skills/fec-web-workers/references/advanced-workers.md +13 -13
  384. package/skills/fec-web-workers/references/basic-worker.md +8 -8
  385. package/skills/metadata.json +402 -220
  386. package/skills/relations.json +120 -25
  387. package/templates/claude/CLAUDE.md +60 -60
  388. package/templates/codex/AGENTS.md +76 -76
  389. package/templates/codex/config.toml +4 -4
  390. package/templates/openclaw/AGENTS.md +68 -68
  391. package/templates/openclaw/OPENCLAW-CONFIG.md +8 -8
  392. package/templates/shared/rules/fec-agent-workflow.md +40 -40
  393. package/templates/shared/rules/fec-api-layer.md +52 -52
  394. package/templates/shared/rules/fec-ci-cd.md +29 -117
  395. package/templates/shared/rules/fec-code-comments.md +22 -22
  396. package/templates/shared/rules/fec-design-system.md +65 -241
  397. package/templates/shared/rules/fec-error-handling.md +41 -185
  398. package/templates/shared/rules/fec-git-conventions.md +42 -42
  399. package/templates/shared/rules/fec-i18n.md +48 -96
  400. package/templates/shared/rules/fec-naming-conventions.md +91 -91
  401. package/templates/shared/rules/fec-performance.md +78 -78
  402. package/templates/shared/rules/fec-react.md +58 -58
  403. package/templates/shared/rules/fec-refactoring.md +34 -30
  404. package/templates/shared/rules/fec-rendering-patterns.md +40 -183
  405. package/templates/shared/rules/fec-responsive-design.md +37 -214
  406. package/templates/shared/rules/fec-source-driven-development.md +22 -22
  407. package/templates/shared/rules/fec-state-management.md +45 -148
  408. package/templates/shared/rules/fec-testing.md +64 -64
  409. package/templates/shared/rules/fec-typescript.md +98 -341
  410. package/templates/shared/rules/fec-vue.md +42 -42
  411. package/templates/shared/rules/fec-working-modes.md +34 -34
  412. package/skills/fec-e2e-testing/references/e2e-advanced.md +0 -176
  413. package/skills/fec-react-project-standard/references/react-project-details.md +0 -642
  414. package/skills/fec-vue3-project-standard/references/vue3-project-details.md +0 -547
@@ -0,0 +1,1397 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ const ALLOWED_TYPES = new Set(["workflow", "sequence", "dataflow", "lifecycle", "architecture"]);
8
+ const NODE_TYPES = new Set([
9
+ "frontend", "backend", "database", "cloud", "security", "messagebus", "external", "active", "waiting", "decision", "success", "failure", "neutral", "start",
10
+ "agent", "model", "memory", "vectorstore", "graphdb", "tool", "document", "queue", "browser", "user", "gateway",
11
+ ]);
12
+ const EDGE_VARIANTS = new Set(["default", "emphasis", "security", "dashed", "return"]);
13
+ const VISUAL_STYLES = new Set(["default", "terminal", "blueprint", "minimal", "glass", "warm", "openai", "editorial-dark"]);
14
+ const FLOW_TYPES = new Set(["control", "data", "read", "write", "async", "feedback"]);
15
+
16
+ const args = parseArgs(process.argv.slice(2));
17
+
18
+ try {
19
+ const inputPath = requireArg(args, "input");
20
+ const outputPath = requireArg(args, "output");
21
+ const type = requireArg(args, "type");
22
+ const format = args.format ?? "markdown";
23
+
24
+ if (!ALLOWED_TYPES.has(type)) {
25
+ throw usageError("type", `Unsupported diagram type "${type}". Expected workflow, sequence, dataflow, lifecycle, or architecture.`);
26
+ }
27
+ if (format !== "json" && format !== "markdown") {
28
+ throw usageError("format", `Unsupported report format "${format}". Expected json or markdown.`);
29
+ }
30
+
31
+ const diagram = readJson(inputPath);
32
+ validateCommon(diagram, type);
33
+ const renderResult = renderDiagram(diagram, type);
34
+ /** @type {SummaryCard[]} */
35
+ const summary = "summary" in renderResult ? /** @type {SummaryCard[]} */ (renderResult.summary) : [];
36
+ const style = normalizeVisualStyle(diagram.visual, "/visual");
37
+ const html = renderHtml(diagram.meta.title, diagram.meta.subtitle ?? "", renderResult.svg, type, summary, style);
38
+
39
+ fs.mkdirSync(path.dirname(path.resolve(outputPath)), { recursive: true });
40
+ fs.writeFileSync(outputPath, html, "utf8");
41
+
42
+ if (args.manifest) {
43
+ fs.mkdirSync(path.dirname(path.resolve(args.manifest)), { recursive: true });
44
+ fs.writeFileSync(args.manifest, `${JSON.stringify(renderResult.manifest, null, 2)}\n`, "utf8");
45
+ }
46
+
47
+ const report = {
48
+ ok: true,
49
+ type,
50
+ input: inputPath,
51
+ output: outputPath,
52
+ manifest: args.manifest ?? null,
53
+ nodes: renderResult.manifest.boxes.length,
54
+ connectors: renderResult.manifest.connectors.length,
55
+ };
56
+ printReport(report, format);
57
+ } catch (error) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ if (args.format === "json") {
60
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
61
+ } else {
62
+ console.error(message);
63
+ }
64
+ process.exitCode = 1;
65
+ }
66
+
67
+ /**
68
+ * @param {string[]} argv
69
+ * @returns {Record<string, string | undefined>}
70
+ */
71
+ function parseArgs(argv) {
72
+ /** @type {Record<string, string | undefined>} */
73
+ const parsed = {};
74
+ for (let index = 0; index < argv.length; index += 1) {
75
+ const token = argv[index];
76
+ if (!token.startsWith("--")) {
77
+ throw usageError("argv", `Unexpected positional argument "${token}".`);
78
+ }
79
+ const key = token.slice(2);
80
+ const value = argv[index + 1];
81
+ if (!value || value.startsWith("--")) {
82
+ throw usageError(key, `Missing value for --${key}.`);
83
+ }
84
+ parsed[key] = value;
85
+ index += 1;
86
+ }
87
+ return parsed;
88
+ }
89
+
90
+ /**
91
+ * @param {Record<string, string | undefined>} parsed
92
+ * @param {string} key
93
+ */
94
+ function requireArg(parsed, key) {
95
+ const value = parsed[key];
96
+ if (!value) throw usageError(key, `Missing required --${key}.`);
97
+ return value;
98
+ }
99
+
100
+ /**
101
+ * @param {string} filePath
102
+ */
103
+ function readJson(filePath) {
104
+ try {
105
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
106
+ } catch (error) {
107
+ throw usageError("input", `Invalid JSON at /: ${error instanceof Error ? error.message : String(error)}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * @param {unknown} diagram
113
+ * @param {string} type
114
+ */
115
+ function validateCommon(diagram, type) {
116
+ assertObject(diagram, "/");
117
+ const model = /** @type {Record<string, unknown>} */ (diagram);
118
+ if (model.schema_version !== 1) {
119
+ throw usageError("/schema_version", "Expected schema_version to equal 1.");
120
+ }
121
+ if (model.diagram_type !== type) {
122
+ throw usageError("/diagram_type", `Expected diagram_type to equal "${type}".`);
123
+ }
124
+ assertObject(model.meta, "/meta");
125
+ requireString(/** @type {Record<string, unknown>} */ (model.meta), "title", "/meta/title");
126
+ normalizeVisualStyle(model.visual, "/visual");
127
+ }
128
+
129
+ /**
130
+ * @param {unknown} diagram
131
+ * @param {string} type
132
+ */
133
+ function renderDiagram(diagram, type) {
134
+ if (type === "workflow") return renderLaneDiagram(/** @type {LaneDiagram} */ (diagram), "nodes", "edges");
135
+ if (type === "lifecycle") return renderLaneDiagram(/** @type {LaneDiagram} */ (diagram), "states", "transitions");
136
+ if (type === "sequence") return renderSequence(/** @type {SequenceDiagram} */ (diagram));
137
+ if (type === "architecture") return renderArchitecture(/** @type {ArchitectureDiagram} */ (diagram));
138
+ return renderDataflow(/** @type {DataflowDiagram} */ (diagram));
139
+ }
140
+
141
+ /**
142
+ * @param {ArchitectureDiagram} diagram
143
+ */
144
+ function renderArchitecture(diagram) {
145
+ const nodes = requireObjectArray(diagram.nodes, "/nodes");
146
+ const connections = requireObjectArray(diagram.connections, "/connections");
147
+ const groups = diagram.groups === undefined ? [] : requireObjectArray(diagram.groups, "/groups");
148
+ const groupIds = new Set();
149
+ /** @type {Map<string, Box>} */
150
+ const boxes = new Map();
151
+ /** @type {Box[]} */
152
+ const groupBoxes = [];
153
+
154
+ for (const [index, group] of groups.entries()) {
155
+ const id = requireId(group, "id", `/groups/${index}/id`);
156
+ ensureUnique(groupIds, id, `/groups/${index}/id`);
157
+ const label = requireString(group, "label", `/groups/${index}/label`);
158
+ const type = normalizeType(group.type, `/groups/${index}/type`);
159
+ const box = {
160
+ id,
161
+ x: requireNumber(group, "x", `/groups/${index}/x`),
162
+ y: requireNumber(group, "y", `/groups/${index}/y`),
163
+ width: requireNumber(group, "width", `/groups/${index}/width`),
164
+ height: requireNumber(group, "height", `/groups/${index}/height`),
165
+ label,
166
+ sublabel: "",
167
+ type,
168
+ };
169
+ groupBoxes.push(box);
170
+ }
171
+
172
+ for (const [index, node] of nodes.entries()) {
173
+ const id = requireId(node, "id", `/nodes/${index}/id`);
174
+ ensureUnique(boxes, id, `/nodes/${index}/id`);
175
+ if (typeof node.group === "string" && !groupIds.has(node.group)) {
176
+ throw usageError(`/nodes/${index}/group`, `Unknown group "${node.group}".`);
177
+ }
178
+ boxes.set(id, {
179
+ id,
180
+ x: requireNumber(node, "x", `/nodes/${index}/x`),
181
+ y: requireNumber(node, "y", `/nodes/${index}/y`),
182
+ width: typeof node.width === "number" ? node.width : 132,
183
+ height: typeof node.height === "number" ? node.height : 62,
184
+ label: requireString(node, "label", `/nodes/${index}/label`),
185
+ sublabel: typeof node.sublabel === "string" ? node.sublabel : "",
186
+ type: normalizeType(node.type, `/nodes/${index}/type`),
187
+ });
188
+ }
189
+
190
+ /** @type {Connector[]} */
191
+ const connectors = [];
192
+ const flowSet = new Set();
193
+ const connectionSvg = connections.map((connection, index) => {
194
+ const from = requireId(connection, "from", `/connections/${index}/from`);
195
+ const to = requireId(connection, "to", `/connections/${index}/to`);
196
+ const fromBox = boxes.get(from);
197
+ const toBox = boxes.get(to);
198
+ if (!fromBox) throw usageError(`/connections/${index}/from`, `Unknown endpoint "${from}".`);
199
+ if (!toBox) throw usageError(`/connections/${index}/to`, `Unknown endpoint "${to}".`);
200
+ const variant = normalizeVariant(connection.variant, `/connections/${index}/variant`);
201
+ const flow = normalizeFlow(connection.flow, `/connections/${index}/flow`);
202
+ if (flow) flowSet.add(flow);
203
+ const points = architectureRoute(fromBox, toBox, connection.waypoints, `/connections/${index}/waypoints`);
204
+ connectors.push({ id: `${from}-${to}-${index}`, from, to, points });
205
+ return renderConnector(points, variant, typeof connection.label === "string" ? connection.label : "", flow);
206
+ }).join("\n");
207
+
208
+ const allBoxes = [...groupBoxes, ...boxes.values()];
209
+ const contentBottom = Math.max(360, maxBoxExtent(allBoxes, "y") + 64);
210
+ const legend = normalizeLegend(diagram.legend, [...boxes.values()]);
211
+ 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);
217
+ const groupSvg = groupBoxes.map(renderGroup).join("\n");
218
+ const boxSvg = [...boxes.values()].map(renderBox).join("\n");
219
+ const svg = wrapSvg(width, height, `${groupSvg}\n${connectionSvg}\n${boxSvg}\n${legendSvg}\n${flowLegendSvg}`);
220
+
221
+ return {
222
+ svg,
223
+ manifest: manifestFrom(width, height, allBoxes, connectors),
224
+ summary: normalizeSummary(diagram.summary),
225
+ };
226
+ }
227
+
228
+ /**
229
+ * @param {LaneDiagram} diagram
230
+ * @param {"nodes" | "states"} itemKey
231
+ * @param {"edges" | "transitions"} edgeKey
232
+ */
233
+ function renderLaneDiagram(diagram, itemKey, edgeKey) {
234
+ const lanes = requireObjectArray(diagram.lanes, "/lanes");
235
+ const items = requireObjectArray(diagram[itemKey], `/${itemKey}`);
236
+ const edges = requireObjectArray(diagram[edgeKey], `/${edgeKey}`);
237
+ const isWorkflow = itemKey === "nodes";
238
+ const laneIds = new Set();
239
+ for (const [index, lane] of lanes.entries()) {
240
+ assertObject(lane, `/lanes/${index}`);
241
+ const id = requireId(lane, "id", `/lanes/${index}/id`);
242
+ requireString(lane, "label", `/lanes/${index}/label`);
243
+ ensureUnique(laneIds, id, `/lanes/${index}/id`);
244
+ }
245
+
246
+ /** @type {Map<string, Box>} */
247
+ const boxes = new Map();
248
+ const maxCol = Math.max(0, ...items.map((item, index) => {
249
+ assertObject(item, `/${itemKey}/${index}`);
250
+ return requireNumber(item, "col", `/${itemKey}/${index}/col`);
251
+ }));
252
+ const width = Math.max(780, 190 + (maxCol + 1) * 180);
253
+ const height = Math.max(420, 140 + lanes.length * 145);
254
+ const laneHeight = 118;
255
+ const top = 72;
256
+ const left = 142;
257
+ const colGap = 172;
258
+
259
+ const laneIndex = new Map(lanes.map((lane, index) => [String(lane.id), index]));
260
+ for (const [index, item] of items.entries()) {
261
+ const id = requireId(item, "id", `/${itemKey}/${index}/id`);
262
+ ensureUnique(boxes, id, `/${itemKey}/${index}/id`);
263
+ const lane = requireId(item, "lane", `/${itemKey}/${index}/lane`);
264
+ if (!laneIds.has(lane)) throw usageError(`/${itemKey}/${index}/lane`, `Unknown lane "${lane}".`);
265
+ const type = normalizeType(item.type, `/${itemKey}/${index}/type`);
266
+ const label = requireString(item, "label", `/${itemKey}/${index}/label`);
267
+ const sublabel = typeof item.sublabel === "string" ? item.sublabel : "";
268
+ const col = requireNumber(item, "col", `/${itemKey}/${index}/col`);
269
+ const yOffset = typeof item.yOffset === "number" ? item.yOffset : 0;
270
+ const width = typeof item.width === "number" ? item.width : defaultWorkflowWidth(type, isWorkflow);
271
+ const height = typeof item.height === "number" ? item.height : defaultWorkflowHeight(type, isWorkflow);
272
+ const box = {
273
+ id,
274
+ x: left + col * colGap,
275
+ y: top + Number(laneIndex.get(lane)) * laneHeight + 38 + yOffset,
276
+ width,
277
+ height,
278
+ label,
279
+ sublabel,
280
+ type,
281
+ actor: isWorkflow && typeof item.actor === "string" ? item.actor : "",
282
+ step: isWorkflow ? normalizeStep(item.step, index + 1, `/${itemKey}/${index}/step`) : "",
283
+ shape: isWorkflow ? shapeForWorkflowType(type) : "box",
284
+ };
285
+ boxes.set(id, box);
286
+ }
287
+
288
+ /** @type {Connector[]} */
289
+ const connectors = [];
290
+ const edgeSvg = edges.map((edge, index) => {
291
+ assertObject(edge, `/${edgeKey}/${index}`);
292
+ const from = requireId(edge, "from", `/${edgeKey}/${index}/from`);
293
+ const to = requireId(edge, "to", `/${edgeKey}/${index}/to`);
294
+ const fromBox = boxes.get(from);
295
+ const toBox = boxes.get(to);
296
+ if (!fromBox) throw usageError(`/${edgeKey}/${index}/from`, `Unknown endpoint "${from}".`);
297
+ if (!toBox) throw usageError(`/${edgeKey}/${index}/to`, `Unknown endpoint "${to}".`);
298
+ const variant = normalizeVariant(edge.variant, `/${edgeKey}/${index}/variant`);
299
+ const points = edge.waypoints === undefined ? routeBoxes(fromBox, toBox) : routeWithWaypoints(fromBox, toBox, edge.waypoints, `/${edgeKey}/${index}/waypoints`);
300
+ const connectorId = `${from}-${to}-${index}`;
301
+ connectors.push({ id: connectorId, from, to, points });
302
+ return renderConnector(points, variant, typeof edge.label === "string" ? edge.label : "");
303
+ }).join("\n");
304
+
305
+ 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>`;
308
+ }).join("\n");
309
+ const boxSvg = [...boxes.values()].map(renderBox).join("\n");
310
+ const svg = wrapSvg(width, height, `${laneSvg}\n${edgeSvg}\n${boxSvg}`);
311
+
312
+ return {
313
+ svg,
314
+ manifest: manifestFrom(width, height, [...boxes.values()], connectors),
315
+ summary: isWorkflow ? normalizeSummary(diagram.summary) : [],
316
+ };
317
+ }
318
+
319
+ /**
320
+ * @param {SequenceDiagram} diagram
321
+ */
322
+ function renderSequence(diagram) {
323
+ const participants = requireObjectArray(diagram.participants, "/participants");
324
+ const messages = requireObjectArray(diagram.messages, "/messages");
325
+ const ids = new Set();
326
+ const width = Math.max(760, 160 + participants.length * 170);
327
+ const height = Math.max(420, 170 + messages.length * 82);
328
+ const top = 74;
329
+ const laneGap = (width - 160) / Math.max(1, participants.length - 1);
330
+ /** @type {Map<string, Box>} */
331
+ const boxes = new Map();
332
+
333
+ for (const [index, participant] of participants.entries()) {
334
+ assertObject(participant, `/participants/${index}`);
335
+ const id = requireId(participant, "id", `/participants/${index}/id`);
336
+ ensureUnique(ids, id, `/participants/${index}/id`);
337
+ const type = normalizeType(participant.type, `/participants/${index}/type`);
338
+ const label = requireString(participant, "label", `/participants/${index}/label`);
339
+ const x = participants.length === 1 ? width / 2 - 64 : 80 + index * laneGap - 64;
340
+ boxes.set(id, {
341
+ id,
342
+ x,
343
+ y: top,
344
+ width: 128,
345
+ height: 56,
346
+ label,
347
+ sublabel: typeof participant.sublabel === "string" ? participant.sublabel : "",
348
+ type,
349
+ });
350
+ }
351
+
352
+ /** @type {Connector[]} */
353
+ 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");
355
+ const messageSvg = messages.map((message, index) => {
356
+ assertObject(message, `/messages/${index}`);
357
+ const from = requireId(message, "from", `/messages/${index}/from`);
358
+ const to = requireId(message, "to", `/messages/${index}/to`);
359
+ const fromBox = boxes.get(from);
360
+ const toBox = boxes.get(to);
361
+ if (!fromBox) throw usageError(`/messages/${index}/from`, `Unknown endpoint "${from}".`);
362
+ if (!toBox) throw usageError(`/messages/${index}/to`, `Unknown endpoint "${to}".`);
363
+ const y = top + 104 + index * 72;
364
+ /** @type {Array<[number, number]>} */
365
+ const points = [[centerX(fromBox), y], [centerX(toBox), y]];
366
+ const variant = normalizeVariant(message.variant, `/messages/${index}/variant`);
367
+ connectors.push({ id: `${from}-${to}-${index}`, from, to, points });
368
+ return renderConnector(points, variant, typeof message.label === "string" ? message.label : "");
369
+ }).join("\n");
370
+ const boxSvg = [...boxes.values()].map(renderBox).join("\n");
371
+ const svg = wrapSvg(width, height, `${lifelines}\n${messageSvg}\n${boxSvg}`);
372
+
373
+ return { svg, manifest: manifestFrom(width, height, [...boxes.values()], connectors) };
374
+ }
375
+
376
+ /**
377
+ * @param {DataflowDiagram} diagram
378
+ */
379
+ function renderDataflow(diagram) {
380
+ const stages = requireObjectArray(diagram.stages, "/stages");
381
+ const nodes = requireObjectArray(diagram.nodes, "/nodes");
382
+ const flows = requireObjectArray(diagram.flows, "/flows");
383
+ const width = Math.max(860, 150 + stages.length * 180);
384
+ const height = Math.max(460, 180 + maxRowsByStage(nodes) * 110);
385
+ const stageWidth = (width - 84) / Math.max(1, stages.length);
386
+ /** @type {Map<string, Box>} */
387
+ const boxes = new Map();
388
+
389
+ for (const [index, stage] of stages.entries()) {
390
+ assertObject(stage, `/stages/${index}`);
391
+ requireString(stage, "label", `/stages/${index}/label`);
392
+ }
393
+ for (const [index, node] of nodes.entries()) {
394
+ assertObject(node, `/nodes/${index}`);
395
+ const id = requireId(node, "id", `/nodes/${index}/id`);
396
+ ensureUnique(boxes, id, `/nodes/${index}/id`);
397
+ const stage = requireNumber(node, "stage", `/nodes/${index}/stage`);
398
+ if (stage < 0 || stage >= stages.length) throw usageError(`/nodes/${index}/stage`, `Stage index ${stage} is outside /stages.`);
399
+ const row = typeof node.row === "number" ? node.row : countStageBefore(nodes, index, stage);
400
+ const type = normalizeType(node.type, `/nodes/${index}/type`);
401
+ boxes.set(id, {
402
+ id,
403
+ x: 54 + stage * stageWidth + Math.max(8, stageWidth / 2 - 64),
404
+ y: 118 + row * 96,
405
+ width: 128,
406
+ height: 58,
407
+ label: requireString(node, "label", `/nodes/${index}/label`),
408
+ sublabel: typeof node.sublabel === "string" ? node.sublabel : "",
409
+ type,
410
+ });
411
+ }
412
+
413
+ /** @type {Connector[]} */
414
+ const connectors = [];
415
+ const stageSvg = stages.map((stage, index) => {
416
+ const x = 42 + index * stageWidth;
417
+ 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>`;
418
+ }).join("\n");
419
+ const flowSvg = flows.map((flow, index) => {
420
+ assertObject(flow, `/flows/${index}`);
421
+ const from = requireId(flow, "from", `/flows/${index}/from`);
422
+ const to = requireId(flow, "to", `/flows/${index}/to`);
423
+ const fromBox = boxes.get(from);
424
+ const toBox = boxes.get(to);
425
+ if (!fromBox) throw usageError(`/flows/${index}/from`, `Unknown endpoint "${from}".`);
426
+ if (!toBox) throw usageError(`/flows/${index}/to`, `Unknown endpoint "${to}".`);
427
+ const variant = normalizeVariant(flow.variant, `/flows/${index}/variant`);
428
+ const points = routeBoxes(fromBox, toBox);
429
+ connectors.push({ id: `${from}-${to}-${index}`, from, to, points });
430
+ return renderConnector(points, variant, typeof flow.label === "string" ? flow.label : "");
431
+ }).join("\n");
432
+ const boxSvg = [...boxes.values()].map(renderBox).join("\n");
433
+ const svg = wrapSvg(width, height, `${stageSvg}\n${flowSvg}\n${boxSvg}`);
434
+
435
+ return { svg, manifest: manifestFrom(width, height, [...boxes.values()], connectors) };
436
+ }
437
+
438
+ /**
439
+ * @param {Array<Record<string, unknown>>} nodes
440
+ */
441
+ function maxRowsByStage(nodes) {
442
+ /** @type {Map<number, number>} */
443
+ const rows = new Map();
444
+ for (const node of nodes) {
445
+ if (typeof node.stage === "number") {
446
+ rows.set(node.stage, (rows.get(node.stage) ?? 0) + 1);
447
+ }
448
+ }
449
+ return Math.max(1, ...rows.values());
450
+ }
451
+
452
+ /**
453
+ * @param {Array<Record<string, unknown>>} nodes
454
+ * @param {number} endIndex
455
+ * @param {number} stage
456
+ */
457
+ function countStageBefore(nodes, endIndex, stage) {
458
+ let count = 0;
459
+ for (let index = 0; index < endIndex; index += 1) {
460
+ const node = nodes[index];
461
+ if (node.stage === stage) count += 1;
462
+ }
463
+ return count;
464
+ }
465
+
466
+ /**
467
+ * @param {Box} box
468
+ */
469
+ function renderBox(box) {
470
+ const labelLines = wrapLabel(box.label, 18).slice(0, 2);
471
+ const shape = box.shape ?? "box";
472
+ 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>` : "";
478
+ const shapeSvg = renderSemanticShape(box) ?? (shape === "diamond"
479
+ ? `<polygon points="${centerX(box)},${box.y} ${box.x + box.width},${centerY(box)} ${centerX(box)},${box.y + box.height} ${box.x},${centerY(box)}"/>`
480
+ : `<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>`;
482
+ }
483
+
484
+ /**
485
+ * @param {Box} box
486
+ * @returns {string | null}
487
+ */
488
+ function renderSemanticShape(box) {
489
+ const cx = centerX(box);
490
+ const cy = centerY(box);
491
+ if (box.type === "agent") {
492
+ const inset = Math.min(28, box.width * 0.18);
493
+ 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"/>`;
494
+ }
495
+ if (box.type === "model") {
496
+ 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"/>`;
497
+ }
498
+ if (box.type === "memory") {
499
+ return `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="12" stroke-dasharray="6 4"/>`;
500
+ }
501
+ if (box.type === "database" || box.type === "vectorstore") {
502
+ const ry = Math.min(14, box.height / 5);
503
+ 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"/>` : "";
504
+ 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}"/>`;
505
+ }
506
+ if (box.type === "graphdb") {
507
+ 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"/>`;
508
+ }
509
+ if (box.type === "tool") {
510
+ 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"/>`;
511
+ }
512
+ if (box.type === "document") {
513
+ const fold = Math.min(16, box.width * 0.16);
514
+ 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
+ }
516
+ 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}"/>`;
519
+ }
520
+ if (box.type === "browser") {
521
+ 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"/>`;
522
+ }
523
+ if (box.type === "user") {
524
+ 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"/>`;
525
+ }
526
+ if (box.type === "gateway") {
527
+ const inset = Math.min(22, box.width * 0.18);
528
+ 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}"/>`;
529
+ }
530
+ return null;
531
+ }
532
+
533
+ /**
534
+ * @param {Box} box
535
+ */
536
+ 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>`;
538
+ }
539
+
540
+ /**
541
+ * @param {Array<[number, number]>} points
542
+ * @param {string} variant
543
+ * @param {string} label
544
+ */
545
+ function renderConnector(points, variant, label, flow = "") {
546
+ 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>` : "";
549
+ const marker = flow ? `flow-${flow}` : (variant === "return" ? "default" : variant);
550
+ return `<g class="edge edge-${variant}${flow ? ` flow-${flow}` : ""}"><path d="${pathData}" marker-end="url(#arrow-${marker})"/>\n${labelSvg}</g>`;
551
+ }
552
+
553
+ /**
554
+ * @param {number} width
555
+ * @param {number} height
556
+ * @param {string} body
557
+ */
558
+ 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">
560
+ <defs>
561
+ <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
+ <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>
563
+ <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>
564
+ <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>
565
+ <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>
566
+ <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>
567
+ <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>
568
+ <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>
569
+ <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>
570
+ <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>
571
+ </defs>
572
+ <rect x="16" y="18" width="${width - 32}" height="${height - 36}" rx="18" class="surface"/>
573
+ ${body}
574
+ </svg>`;
575
+ }
576
+
577
+ /**
578
+ * @param {string} title
579
+ * @param {string} subtitle
580
+ * @param {string} svg
581
+ * @param {string} type
582
+ * @param {SummaryCard[]} [summary]
583
+ */
584
+ function renderHtml(title, subtitle, svg, type, summary = [], style = "default") {
585
+ const summaryHtml = renderSummaryCards(summary);
586
+ return `<!doctype html>
587
+ <html lang="en" data-theme="auto" data-visual-style="${escapeHtml(style)}">
588
+ <head>
589
+ <meta charset="utf-8">
590
+ <meta name="viewport" content="width=device-width, initial-scale=1">
591
+ <title>${escapeHtml(title)}</title>
592
+ <style>
593
+ :root {
594
+ color-scheme: dark light;
595
+ --bg: #0f172a;
596
+ --panel: #111827;
597
+ --surface: #172033;
598
+ --text: #f8fafc;
599
+ --muted: #94a3b8;
600
+ --line: #64748b;
601
+ --frontend: #38bdf8;
602
+ --backend: #34d399;
603
+ --database: #a78bfa;
604
+ --cloud: #fbbf24;
605
+ --security: #fb7185;
606
+ --messagebus: #fb923c;
607
+ --external: #cbd5e1;
608
+ --agent: #22d3ee;
609
+ --model: #c084fc;
610
+ --memory: #34d399;
611
+ --vectorstore: #60a5fa;
612
+ --graphdb: #f472b6;
613
+ --tool: #f59e0b;
614
+ --document: #cbd5e1;
615
+ --queue: #fb923c;
616
+ --browser: #38bdf8;
617
+ --user: #fda4af;
618
+ --gateway: #fbbf24;
619
+ --flow-control: #f59e0b;
620
+ --flow-data: #38bdf8;
621
+ --flow-read: #34d399;
622
+ --flow-write: #22c55e;
623
+ --flow-async: #94a3b8;
624
+ --flow-feedback: #c084fc;
625
+ }
626
+ [data-theme="light"] {
627
+ --bg: #f8fafc;
628
+ --panel: #ffffff;
629
+ --surface: #eef2f7;
630
+ --text: #0f172a;
631
+ --muted: #64748b;
632
+ --line: #475569;
633
+ }
634
+ [data-visual-style="terminal"] {
635
+ --bg: #0b1020;
636
+ --panel: #0f172a;
637
+ --surface: #111827;
638
+ --text: #e2e8f0;
639
+ --muted: #94a3b8;
640
+ --frontend: #38bdf8;
641
+ --backend: #22c55e;
642
+ --security: #f472b6;
643
+ }
644
+ [data-visual-style="blueprint"] {
645
+ --bg: #082f49;
646
+ --panel: #0b3b5e;
647
+ --surface: #0c4a6e;
648
+ --text: #e0f2fe;
649
+ --muted: #7dd3fc;
650
+ --line: #67e8f9;
651
+ --frontend: #38bdf8;
652
+ --backend: #67e8f9;
653
+ --database: #fde047;
654
+ --cloud: #bae6fd;
655
+ }
656
+ [data-visual-style="minimal"] {
657
+ --bg: #f8fafc;
658
+ --panel: #ffffff;
659
+ --surface: #f1f5f9;
660
+ --text: #111827;
661
+ --muted: #64748b;
662
+ --line: #94a3b8;
663
+ }
664
+ [data-visual-style="glass"] {
665
+ --bg: #111827;
666
+ --panel: color-mix(in srgb, #1f2937, transparent 18%);
667
+ --surface: color-mix(in srgb, #334155, transparent 18%);
668
+ --text: #f8fafc;
669
+ --muted: #cbd5e1;
670
+ }
671
+ [data-visual-style="warm"] {
672
+ --bg: #f8f6f3;
673
+ --panel: #fffcf7;
674
+ --surface: #f1ede6;
675
+ --text: #141413;
676
+ --muted: #7c756c;
677
+ --line: #8b7355;
678
+ --frontend: #d97757;
679
+ --backend: #7b8b5c;
680
+ --database: #8c6f5a;
681
+ --cloud: #b45309;
682
+ }
683
+ [data-visual-style="openai"] {
684
+ --bg: #ffffff;
685
+ --panel: #ffffff;
686
+ --surface: #f8fafc;
687
+ --text: #0f172a;
688
+ --muted: #64748b;
689
+ --line: #10a37f;
690
+ --frontend: #10a37f;
691
+ --backend: #0f766e;
692
+ --database: #0891b2;
693
+ }
694
+ [data-visual-style="editorial-dark"] {
695
+ --bg: #0a0a0a;
696
+ --panel: #111111;
697
+ --surface: #181818;
698
+ --text: #f5f0e8;
699
+ --muted: #b8a88f;
700
+ --line: #d4a574;
701
+ --frontend: #38bdf8;
702
+ --backend: #34d399;
703
+ --database: #a78bfa;
704
+ --cloud: #d4a574;
705
+ --security: #fb7185;
706
+ }
707
+ * { box-sizing: border-box; }
708
+ body {
709
+ margin: 0;
710
+ min-height: 100vh;
711
+ background: var(--bg);
712
+ color: var(--text);
713
+ font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
714
+ }
715
+ main { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 28px 0; }
716
+ header { display: flex; align-items: end; justify-content: space-between; gap: 20px; margin-bottom: 18px; }
717
+ h1 { font-size: 24px; margin: 0; letter-spacing: 0; }
718
+ p { margin: 6px 0 0; color: var(--muted); }
719
+ .toolbar { display: flex; gap: 8px; flex-wrap: wrap; }
720
+ button {
721
+ border: 1px solid color-mix(in srgb, var(--muted), transparent 55%);
722
+ background: var(--panel);
723
+ color: var(--text);
724
+ border-radius: 8px;
725
+ padding: 8px 11px;
726
+ cursor: pointer;
727
+ }
728
+ .diagram-frame { background: var(--panel); border: 1px solid color-mix(in srgb, var(--muted), transparent 70%); border-radius: 12px; overflow: auto; }
729
+ .tech-diagram { display: block; min-width: 720px; width: 100%; height: auto; }
730
+ .group rect { fill: color-mix(in srgb, currentColor, transparent 94%); stroke: currentColor; stroke-width: 1.2; stroke-dasharray: 8 5; }
731
+ .group-frontend { color: var(--frontend); }
732
+ .group-backend, .group-active, .group-start { color: var(--backend); }
733
+ .group-database { color: var(--database); }
734
+ .group-cloud, .group-decision { color: var(--cloud); }
735
+ .group-security, .group-failure, .group-waiting { color: var(--security); }
736
+ .group-messagebus { color: var(--messagebus); }
737
+ .group-external, .group-neutral, .group-success { color: var(--external); }
738
+ .surface { fill: var(--surface); stroke: color-mix(in srgb, var(--muted), transparent 70%); }
739
+ .lane rect, .stage rect { fill: color-mix(in srgb, var(--surface), transparent 25%); stroke: color-mix(in srgb, var(--muted), transparent 72%); }
740
+ .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; }
741
+ .node-frontend { color: var(--frontend); }
742
+ .node-backend, .node-active, .node-start, .node-success { color: var(--backend); }
743
+ .node-database { color: var(--database); }
744
+ .node-cloud, .node-decision { color: var(--cloud); }
745
+ .node-security, .node-failure, .node-waiting { color: var(--security); }
746
+ .node-messagebus { color: var(--messagebus); }
747
+ .node-external, .node-neutral { color: var(--external); }
748
+ .node-agent { color: var(--agent); }
749
+ .node-model { color: var(--model); }
750
+ .node-memory { color: var(--memory); }
751
+ .node-vectorstore { color: var(--vectorstore); }
752
+ .node-graphdb { color: var(--graphdb); }
753
+ .node-tool { color: var(--tool); }
754
+ .node-document { color: var(--document); }
755
+ .node-queue { color: var(--queue); }
756
+ .node-browser { color: var(--browser); }
757
+ .node-user { color: var(--user); }
758
+ .node-gateway { color: var(--gateway); }
759
+ .node-glyph { fill: currentColor; stroke: none; }
760
+ .node-glyph-outline, .node-glyph-line { fill: none; stroke: currentColor; }
761
+ .node-chrome { fill: currentColor; opacity: 0.25; stroke: none; }
762
+ .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; }
765
+ .actor { font-weight: 650; }
766
+ .step-badge circle { fill: var(--panel); stroke: currentColor; stroke-width: 1.3; }
767
+ .step-badge text { fill: var(--text); font-size: 9px; font-weight: 750; }
768
+ .edge path { fill: none; stroke: var(--line); stroke-width: 1.6; }
769
+ .edge-emphasis path { stroke: var(--frontend); stroke-width: 2; }
770
+ .edge-security path { stroke: var(--security); stroke-dasharray: 5 4; }
771
+ .edge-dashed path, .edge-return path { stroke-dasharray: 5 4; }
772
+ .flow-control path { stroke: var(--flow-control); }
773
+ .flow-data path { stroke: var(--flow-data); stroke-width: 2; }
774
+ .flow-read path { stroke: var(--flow-read); }
775
+ .flow-write path { stroke: var(--flow-write); stroke-dasharray: 5 4; }
776
+ .flow-async path { stroke: var(--flow-async); stroke-dasharray: 4 4; }
777
+ .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; }
779
+ .lifeline { stroke: color-mix(in srgb, var(--muted), transparent 48%); stroke-dasharray: 4 5; }
780
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin-top: 12px; }
781
+ .summary-card { background: var(--panel); border: 1px solid color-mix(in srgb, var(--muted), transparent 74%); border-radius: 8px; padding: 12px; }
782
+ .summary-card h2 { margin: 0 0 7px; font-size: 13px; letter-spacing: 0; }
783
+ .summary-card ul { margin: 0; padding-left: 18px; color: var(--muted); font-size: 12px; }
784
+ .summary-card li + li { margin-top: 4px; }
785
+ marker path { fill: var(--line); }
786
+ #arrow-emphasis path { fill: var(--frontend); }
787
+ #arrow-security path { fill: var(--security); }
788
+ #arrow-dashed path { fill: var(--line); }
789
+ #arrow-flow-control path { fill: var(--flow-control); }
790
+ #arrow-flow-data path { fill: var(--flow-data); }
791
+ #arrow-flow-read path { fill: var(--flow-read); }
792
+ #arrow-flow-write path { fill: var(--flow-write); }
793
+ #arrow-flow-async path { fill: var(--flow-async); }
794
+ #arrow-flow-feedback path { fill: var(--flow-feedback); }
795
+ footer { margin-top: 12px; color: var(--muted); font-size: 12px; }
796
+ @media print {
797
+ body { background: #fff; }
798
+ main { width: 100%; padding: 0; }
799
+ .toolbar, footer { display: none; }
800
+ .diagram-frame { border: 0; }
801
+ }
802
+ </style>
803
+ </head>
804
+ <body>
805
+ <main>
806
+ <header>
807
+ <div>
808
+ <h1>${escapeHtml(title)}</h1>
809
+ <p>${escapeHtml(subtitle || `${type} technical diagram`)}</p>
810
+ </div>
811
+ <div class="toolbar" aria-label="Diagram actions">
812
+ <button type="button" data-action="theme">Toggle theme</button>
813
+ <button type="button" data-action="svg">Download SVG</button>
814
+ <button type="button" data-action="print">Print / Save PDF</button>
815
+ </div>
816
+ </header>
817
+ <section class="diagram-frame" aria-label="${escapeHtml(title)}">
818
+ ${svg}
819
+ </section>
820
+ ${summaryHtml}
821
+ <footer>Generated as a self-contained Frontend Craft HTML technical diagram. Export raster images from the browser after visual QA.</footer>
822
+ </main>
823
+ <script>
824
+ const root = document.documentElement;
825
+ const stored = localStorage.getItem("fec-tech-diagram-theme");
826
+ if (stored === "dark" || stored === "light") root.dataset.theme = stored;
827
+ document.querySelector('[data-action="theme"]').addEventListener("click", () => {
828
+ const next = root.dataset.theme === "light" ? "dark" : "light";
829
+ root.dataset.theme = next;
830
+ localStorage.setItem("fec-tech-diagram-theme", next);
831
+ });
832
+ document.querySelector('[data-action="print"]').addEventListener("click", () => window.print());
833
+ document.querySelector('[data-action="svg"]').addEventListener("click", () => {
834
+ const svg = document.querySelector("[data-export-root]");
835
+ const source = new XMLSerializer().serializeToString(svg);
836
+ const blob = new Blob([source], { type: "image/svg+xml" });
837
+ const link = document.createElement("a");
838
+ link.href = URL.createObjectURL(blob);
839
+ link.download = document.title.replace(/[^a-z0-9_-]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() + ".svg";
840
+ link.click();
841
+ URL.revokeObjectURL(link.href);
842
+ });
843
+ </script>
844
+ </body>
845
+ </html>
846
+ `;
847
+ }
848
+
849
+ /**
850
+ * @param {SummaryCard[]} summary
851
+ */
852
+ function renderSummaryCards(summary) {
853
+ if (summary.length === 0) return "";
854
+ return ` <section class="summary-grid" aria-label="Diagram summary">
855
+ ${summary.map((card) => ` <article class="summary-card summary-${card.type}">
856
+ <h2>${escapeHtml(card.title)}</h2>
857
+ <ul>${card.items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
858
+ </article>`).join("\n")}
859
+ </section>`;
860
+ }
861
+
862
+ /**
863
+ * @param {number} width
864
+ * @param {number} height
865
+ * @param {Box[]} boxes
866
+ * @param {Connector[]} connectors
867
+ */
868
+ function manifestFrom(width, height, boxes, connectors) {
869
+ 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 })),
872
+ connectors,
873
+ };
874
+ }
875
+
876
+ /**
877
+ * @param {Box[]} boxes
878
+ * @param {"x" | "y"} axis
879
+ */
880
+ function maxBoxExtent(boxes, axis) {
881
+ const sizeKey = axis === "x" ? "width" : "height";
882
+ return Math.max(0, ...boxes.map((box) => box[axis] + box[sizeKey]));
883
+ }
884
+
885
+ /**
886
+ * @param {Box} from
887
+ * @param {Box} to
888
+ * @param {unknown} waypoints
889
+ * @param {string} pointer
890
+ * @returns {Array<[number, number]>}
891
+ */
892
+ function architectureRoute(from, to, waypoints, pointer) {
893
+ return routeWithWaypoints(from, to, waypoints, pointer);
894
+ }
895
+
896
+ /**
897
+ * @param {Box} from
898
+ * @param {Box} to
899
+ * @param {unknown} waypoints
900
+ * @param {string} pointer
901
+ * @returns {Array<[number, number]>}
902
+ */
903
+ function routeWithWaypoints(from, to, waypoints, pointer) {
904
+ if (waypoints === undefined) return routeBoxes(from, to);
905
+ if (!Array.isArray(waypoints)) throw usageError(pointer, "Expected array of [x, y] waypoints.");
906
+ /** @type {Array<[number, number]>} */
907
+ const parsed = waypoints.map((point, index) => {
908
+ if (!Array.isArray(point) || point.length !== 2 || typeof point[0] !== "number" || typeof point[1] !== "number") {
909
+ throw usageError(`${pointer}/${index}`, "Expected [x, y] number tuple.");
910
+ }
911
+ return [point[0], point[1]];
912
+ });
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)];
916
+ }
917
+
918
+ /**
919
+ * @param {string} type
920
+ * @param {boolean} isWorkflow
921
+ */
922
+ function defaultWorkflowWidth(type, isWorkflow) {
923
+ if (!isWorkflow) return 132;
924
+ if (type === "decision") return 92;
925
+ if (type === "start" || type === "success" || type === "failure") return 128;
926
+ return 132;
927
+ }
928
+
929
+ /**
930
+ * @param {string} type
931
+ * @param {boolean} isWorkflow
932
+ */
933
+ function defaultWorkflowHeight(type, isWorkflow) {
934
+ if (!isWorkflow) return 58;
935
+ if (type === "decision") return 92;
936
+ if (type === "start" || type === "success" || type === "failure") return 46;
937
+ return 58;
938
+ }
939
+
940
+ /**
941
+ * @param {string} type
942
+ * @returns {"box" | "diamond" | "terminal"}
943
+ */
944
+ function shapeForWorkflowType(type) {
945
+ if (type === "decision") return "diamond";
946
+ if (type === "start" || type === "success" || type === "failure") return "terminal";
947
+ return "box";
948
+ }
949
+
950
+ /**
951
+ * @param {unknown} value
952
+ * @param {number} fallback
953
+ * @param {string} pointer
954
+ */
955
+ function normalizeStep(value, fallback, pointer) {
956
+ if (value === undefined) return String(fallback);
957
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
958
+ if (typeof value === "string" && value.length > 0) return value;
959
+ throw usageError(pointer, "Expected non-empty string or finite number.");
960
+ }
961
+
962
+ /**
963
+ * @param {Box} box
964
+ * @param {[number, number]} target
965
+ * @returns {[number, number]}
966
+ */
967
+ function edgePointToward(box, target) {
968
+ const cx = centerX(box);
969
+ const cy = centerY(box);
970
+ const dx = target[0] - cx;
971
+ const dy = target[1] - cy;
972
+ if (Math.abs(dx) / Math.max(1, box.width) > Math.abs(dy) / Math.max(1, box.height)) {
973
+ return [dx >= 0 ? box.x + box.width : box.x, cy];
974
+ }
975
+ return [cx, dy >= 0 ? box.y + box.height : box.y];
976
+ }
977
+
978
+ /**
979
+ * @param {unknown} legend
980
+ * @param {Box[]} boxes
981
+ * @returns {LegendItem[]}
982
+ */
983
+ function normalizeLegend(legend, boxes) {
984
+ if (legend === false) return [];
985
+ if (legend !== undefined) {
986
+ const items = requireObjectArray(legend, "/legend");
987
+ return items.map((item, index) => ({
988
+ label: requireString(item, "label", `/legend/${index}/label`),
989
+ type: normalizeType(item.type, `/legend/${index}/type`),
990
+ }));
991
+ }
992
+ const seen = new Set();
993
+ /** @type {LegendItem[]} */
994
+ const items = [];
995
+ for (const box of boxes) {
996
+ if (!seen.has(box.type)) {
997
+ seen.add(box.type);
998
+ items.push({ label: labelForType(box.type), type: box.type });
999
+ }
1000
+ }
1001
+ return items;
1002
+ }
1003
+
1004
+ /**
1005
+ * @param {LegendItem[]} items
1006
+ * @param {number} x
1007
+ * @param {number} y
1008
+ */
1009
+ function renderArchitectureLegend(items, x, y) {
1010
+ if (items.length === 0) return "";
1011
+ 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>`;
1014
+ }).join("\n");
1015
+ return `<g class="legend"><text x="${x}" y="${y}" class="label">Legend</text>\n${itemSvg}</g>`;
1016
+ }
1017
+
1018
+ /**
1019
+ * @param {LegendItem[]} items
1020
+ * @param {number} x
1021
+ * @param {number} y
1022
+ */
1023
+ function renderFlowLegend(items, x, y) {
1024
+ if (items.length === 0) return "";
1025
+ 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>`;
1028
+ }).join("\n");
1029
+ return `<g class="flow-legend"><text x="${x}" y="${y}" class="label">Flow Legend</text>\n${itemSvg}</g>`;
1030
+ }
1031
+
1032
+ /**
1033
+ * @param {string} type
1034
+ */
1035
+ function labelForType(type) {
1036
+ return ({
1037
+ frontend: "Frontend",
1038
+ backend: "Backend",
1039
+ database: "Database",
1040
+ cloud: "Cloud",
1041
+ security: "Security",
1042
+ messagebus: "Message bus",
1043
+ external: "External",
1044
+ agent: "Agent",
1045
+ model: "Model",
1046
+ memory: "Memory",
1047
+ vectorstore: "Vector store",
1048
+ graphdb: "Graph DB",
1049
+ tool: "Tool",
1050
+ document: "Document",
1051
+ queue: "Queue",
1052
+ browser: "Browser",
1053
+ user: "User",
1054
+ gateway: "Gateway",
1055
+ })[type] ?? type;
1056
+ }
1057
+
1058
+ /**
1059
+ * @param {string} flow
1060
+ */
1061
+ function labelForFlow(flow) {
1062
+ return ({
1063
+ control: "Control",
1064
+ data: "Data",
1065
+ read: "Read",
1066
+ write: "Write",
1067
+ async: "Async",
1068
+ feedback: "Feedback",
1069
+ })[flow] ?? flow;
1070
+ }
1071
+
1072
+ /**
1073
+ * @param {unknown} legend
1074
+ * @param {string[]} flows
1075
+ * @returns {LegendItem[]}
1076
+ */
1077
+ function normalizeFlowLegend(legend, flows) {
1078
+ if (legend === false) return [];
1079
+ if (legend !== undefined) {
1080
+ const items = requireObjectArray(legend, "/flowLegend");
1081
+ return items.map((item, index) => ({
1082
+ label: requireString(item, "label", `/flowLegend/${index}/label`),
1083
+ type: normalizeFlow(item.type, `/flowLegend/${index}/type`) ?? "control",
1084
+ }));
1085
+ }
1086
+ if (flows.length < 2) return [];
1087
+ return flows.map((flow) => ({ label: labelForFlow(flow), type: flow }));
1088
+ }
1089
+
1090
+ /**
1091
+ * @param {unknown} summary
1092
+ * @returns {SummaryCard[]}
1093
+ */
1094
+ function normalizeSummary(summary) {
1095
+ if (summary === undefined) return [];
1096
+ const cards = requireObjectArray(summary, "/summary").slice(0, 3);
1097
+ return cards.map((card, index) => {
1098
+ const items = requireArray(card.items, `/summary/${index}/items`);
1099
+ return {
1100
+ title: requireString(card, "title", `/summary/${index}/title`),
1101
+ type: normalizeType(card.type, `/summary/${index}/type`),
1102
+ items: items.map((item, itemIndex) => {
1103
+ if (typeof item !== "string" || item.length === 0) {
1104
+ throw usageError(`/summary/${index}/items/${itemIndex}`, "Expected non-empty string.");
1105
+ }
1106
+ return item;
1107
+ }),
1108
+ };
1109
+ });
1110
+ }
1111
+
1112
+ /**
1113
+ * @param {Box} from
1114
+ * @param {Box} to
1115
+ * @returns {Array<[number, number]>}
1116
+ */
1117
+ 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]]];
1125
+ }
1126
+
1127
+ /**
1128
+ * @param {Box} box
1129
+ */
1130
+ function centerX(box) {
1131
+ return box.x + box.width / 2;
1132
+ }
1133
+
1134
+ /**
1135
+ * @param {Box} box
1136
+ */
1137
+ function centerY(box) {
1138
+ return box.y + box.height / 2;
1139
+ }
1140
+
1141
+ /**
1142
+ * @param {string} value
1143
+ * @param {number} size
1144
+ */
1145
+ function wrapLabel(value, size) {
1146
+ const words = value.split(/\s+/).filter(Boolean);
1147
+ /** @type {string[]} */
1148
+ const lines = [];
1149
+ let line = "";
1150
+ for (const word of words) {
1151
+ if ((line ? `${line} ${word}` : word).length > size) {
1152
+ if (line) lines.push(line);
1153
+ line = word;
1154
+ } else {
1155
+ line = line ? `${line} ${word}` : word;
1156
+ }
1157
+ }
1158
+ if (line) lines.push(line);
1159
+ return lines.length > 0 ? lines : [value];
1160
+ }
1161
+
1162
+ /**
1163
+ * @param {string} type
1164
+ */
1165
+ function isSemanticNodeType(type) {
1166
+ return ["agent", "model", "memory", "vectorstore", "graphdb", "tool", "document", "queue", "browser", "user", "gateway"].includes(type);
1167
+ }
1168
+
1169
+ /**
1170
+ * @param {unknown} visual
1171
+ * @param {string} pointer
1172
+ */
1173
+ function normalizeVisualStyle(visual, pointer) {
1174
+ if (visual === undefined) return "default";
1175
+ assertObject(visual, pointer);
1176
+ const style = /** @type {Record<string, unknown>} */ (visual).style;
1177
+ if (style === undefined) return "default";
1178
+ if (typeof style !== "string" || !VISUAL_STYLES.has(style)) {
1179
+ throw usageError(`${pointer}/style`, `Unsupported visual style "${String(style)}".`);
1180
+ }
1181
+ return style;
1182
+ }
1183
+
1184
+ /**
1185
+ * @param {unknown} value
1186
+ * @param {string} pointer
1187
+ */
1188
+ function normalizeFlow(value, pointer) {
1189
+ if (value === undefined) return "";
1190
+ if (typeof value !== "string" || !FLOW_TYPES.has(value)) {
1191
+ throw usageError(pointer, `Unsupported flow "${String(value)}".`);
1192
+ }
1193
+ return value;
1194
+ }
1195
+
1196
+ /**
1197
+ * @param {unknown} value
1198
+ * @param {string} pointer
1199
+ */
1200
+ function normalizeType(value, pointer) {
1201
+ if (typeof value !== "string") return "external";
1202
+ if (!NODE_TYPES.has(value)) throw usageError(pointer, `Unsupported node type "${value}".`);
1203
+ return value;
1204
+ }
1205
+
1206
+ /**
1207
+ * @param {unknown} value
1208
+ * @param {string} pointer
1209
+ */
1210
+ function normalizeVariant(value, pointer) {
1211
+ if (value === undefined) return "default";
1212
+ if (typeof value !== "string" || !EDGE_VARIANTS.has(value)) {
1213
+ throw usageError(pointer, `Unsupported edge variant "${String(value)}".`);
1214
+ }
1215
+ return value;
1216
+ }
1217
+
1218
+ /**
1219
+ * @param {unknown} value
1220
+ * @param {string} pointer
1221
+ */
1222
+ function assertObject(value, pointer) {
1223
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1224
+ throw usageError(pointer, "Expected object.");
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * @param {unknown} value
1230
+ * @param {string} pointer
1231
+ * @returns {unknown[]}
1232
+ */
1233
+ function requireArray(value, pointer) {
1234
+ if (!Array.isArray(value)) throw usageError(pointer, "Expected array.");
1235
+ return value;
1236
+ }
1237
+
1238
+ /**
1239
+ * @param {unknown} value
1240
+ * @param {string} pointer
1241
+ * @returns {Array<Record<string, unknown>>}
1242
+ */
1243
+ function requireObjectArray(value, pointer) {
1244
+ const items = requireArray(value, pointer);
1245
+ for (const [index, item] of items.entries()) {
1246
+ assertObject(item, `${pointer}/${index}`);
1247
+ }
1248
+ return /** @type {Array<Record<string, unknown>>} */ (items);
1249
+ }
1250
+
1251
+ /**
1252
+ * @param {Record<string, unknown>} object
1253
+ * @param {string} key
1254
+ * @param {string} pointer
1255
+ */
1256
+ function requireString(object, key, pointer) {
1257
+ const value = object[key];
1258
+ if (typeof value !== "string" || value.length === 0) {
1259
+ throw usageError(pointer, "Expected non-empty string.");
1260
+ }
1261
+ return value;
1262
+ }
1263
+
1264
+ /**
1265
+ * @param {Record<string, unknown>} object
1266
+ * @param {string} key
1267
+ * @param {string} pointer
1268
+ */
1269
+ function requireId(object, key, pointer) {
1270
+ const value = requireString(object, key, pointer);
1271
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
1272
+ throw usageError(pointer, "Expected id to match ^[a-zA-Z][a-zA-Z0-9_-]*$.");
1273
+ }
1274
+ return value;
1275
+ }
1276
+
1277
+ /**
1278
+ * @param {Record<string, unknown>} object
1279
+ * @param {string} key
1280
+ * @param {string} pointer
1281
+ */
1282
+ function requireNumber(object, key, pointer) {
1283
+ const value = object[key];
1284
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1285
+ throw usageError(pointer, "Expected finite number.");
1286
+ }
1287
+ return value;
1288
+ }
1289
+
1290
+ /**
1291
+ * @param {Set<string> | Map<string, unknown>} seen
1292
+ * @param {string} id
1293
+ * @param {string} pointer
1294
+ */
1295
+ function ensureUnique(seen, id, pointer) {
1296
+ if (seen.has(id)) throw usageError(pointer, `Duplicate id "${id}".`);
1297
+ if (seen instanceof Set) seen.add(id);
1298
+ else seen.set(id, undefined);
1299
+ }
1300
+
1301
+ /**
1302
+ * @param {string} pointer
1303
+ * @param {string} message
1304
+ */
1305
+ function usageError(pointer, message) {
1306
+ return new Error(`${pointer}: ${message}`);
1307
+ }
1308
+
1309
+ /**
1310
+ * @param {string} value
1311
+ */
1312
+ function escapeHtml(value) {
1313
+ return value.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[char] ?? char));
1314
+ }
1315
+
1316
+ /**
1317
+ * @param {{ ok: boolean; type: string; input: string; output: string; manifest: string | null; nodes: number; connectors: number }} report
1318
+ * @param {string} format
1319
+ */
1320
+ function printReport(report, format) {
1321
+ if (format === "json") {
1322
+ console.log(JSON.stringify(report, null, 2));
1323
+ return;
1324
+ }
1325
+ console.log(`Rendered ${report.type} technical diagram`);
1326
+ console.log(`- Output: ${report.output}`);
1327
+ if (report.manifest) console.log(`- Manifest: ${report.manifest}`);
1328
+ console.log(`- Nodes: ${report.nodes}`);
1329
+ console.log(`- Connectors: ${report.connectors}`);
1330
+ }
1331
+
1332
+ /**
1333
+ * @typedef {{
1334
+ * id: string;
1335
+ * x: number;
1336
+ * y: number;
1337
+ * width: number;
1338
+ * height: number;
1339
+ * label: string;
1340
+ * sublabel: string;
1341
+ * type: string;
1342
+ * actor?: string;
1343
+ * step?: string;
1344
+ * shape?: "box" | "diamond" | "terminal";
1345
+ * }} Box
1346
+ *
1347
+ * @typedef {{
1348
+ * id: string;
1349
+ * from: string;
1350
+ * to: string;
1351
+ * points: Array<[number, number]>;
1352
+ * }} Connector
1353
+ *
1354
+ * @typedef {{
1355
+ * meta: { title: string; subtitle?: string };
1356
+ * lanes: Array<Record<string, unknown>>;
1357
+ * nodes?: Array<Record<string, unknown>>;
1358
+ * edges?: Array<Record<string, unknown>>;
1359
+ * states?: Array<Record<string, unknown>>;
1360
+ * transitions?: Array<Record<string, unknown>>;
1361
+ * summary?: Array<Record<string, unknown>>;
1362
+ * }} LaneDiagram
1363
+ *
1364
+ * @typedef {{
1365
+ * meta: { title: string; subtitle?: string };
1366
+ * participants: Array<Record<string, unknown>>;
1367
+ * messages: Array<Record<string, unknown>>;
1368
+ * }} SequenceDiagram
1369
+ *
1370
+ * @typedef {{
1371
+ * meta: { title: string; subtitle?: string };
1372
+ * stages: Array<Record<string, unknown>>;
1373
+ * nodes: Array<Record<string, unknown>>;
1374
+ * flows: Array<Record<string, unknown>>;
1375
+ * }} DataflowDiagram
1376
+ *
1377
+ * @typedef {{
1378
+ * meta: { title: string; subtitle?: string };
1379
+ * groups?: Array<Record<string, unknown>>;
1380
+ * nodes: Array<Record<string, unknown>>;
1381
+ * connections: Array<Record<string, unknown>>;
1382
+ * legend?: Array<Record<string, unknown>> | false;
1383
+ * flowLegend?: Array<Record<string, unknown>> | false;
1384
+ * summary?: Array<Record<string, unknown>>;
1385
+ * }} ArchitectureDiagram
1386
+ *
1387
+ * @typedef {{
1388
+ * label: string;
1389
+ * type: string;
1390
+ * }} LegendItem
1391
+ *
1392
+ * @typedef {{
1393
+ * title: string;
1394
+ * type: string;
1395
+ * items: string[];
1396
+ * }} SummaryCard
1397
+ */