@hybridlabor-api/aos 4.6.5 → 4.7.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 (486) hide show
  1. package/.agents/agents.md +1 -0
  2. package/.agents/nodes.json +5 -1
  3. package/.agents/state.schema.json +2 -1
  4. package/.agents/vendor-manifest.json +50 -0
  5. package/.claude/agents/architect.md +4 -0
  6. package/.claude/agents/security-reviewer.md +1 -1
  7. package/.claude/hooks/memb-inject.mjs +257 -135
  8. package/.claude/hooks/trail-relay.mjs +18 -0
  9. package/.claude/settings.json +70 -0
  10. package/.claude/workflows/startcycle-dispatch.mjs +29 -1
  11. package/.opencode/agents/architect.md +4 -0
  12. package/.opencode/plugins/bdb-aos.js +75 -0
  13. package/THIRD_PARTY_NOTICES.md +48 -0
  14. package/bin/aos-config.mjs +3 -1
  15. package/bin/aos-uninstall.mjs +55 -1
  16. package/docs/handover/agenttrail-for-ao.md +58 -0
  17. package/docs/handover/deja-for-ao.md +51 -0
  18. package/installer.js +257 -56
  19. package/mcp_config.json +4 -0
  20. package/mcps/mcsc/README.md +24 -0
  21. package/mcps/mcsc/package-lock.json +1234 -0
  22. package/mcps/mcsc/package.json +14 -0
  23. package/mcps/mcsc/packages/core/bin/mcsc.js +162 -0
  24. package/mcps/mcsc/packages/core/rulebook.default.yaml +49 -0
  25. package/mcps/mcsc/packages/core/schema/rulebook.schema.json +56 -0
  26. package/mcps/mcsc/packages/core/src/adapters/agy.js +107 -0
  27. package/mcps/mcsc/packages/core/src/adapters/codex.js +56 -0
  28. package/mcps/mcsc/packages/core/src/adapters/native.js +14 -0
  29. package/mcps/mcsc/packages/core/src/adapters/opencode.js +183 -0
  30. package/mcps/mcsc/packages/core/src/adapters/types.js +21 -0
  31. package/mcps/mcsc/packages/core/src/capabilities/buildOfferTable.js +34 -0
  32. package/mcps/mcsc/packages/core/src/capabilities/registry.js +99 -0
  33. package/mcps/mcsc/packages/core/src/capabilities/types.js +24 -0
  34. package/mcps/mcsc/packages/core/src/detect/cache.js +47 -0
  35. package/mcps/mcsc/packages/core/src/detect/inventory.js +73 -0
  36. package/mcps/mcsc/packages/core/src/detect/probes.js +158 -0
  37. package/mcps/mcsc/packages/core/src/detect/types.js +26 -0
  38. package/mcps/mcsc/packages/core/src/rulebook/load.js +53 -0
  39. package/mcps/mcsc/packages/core/src/rulebook/resolve.js +227 -0
  40. package/mcps/mcsc/packages/core/src/rulebook/rulebook.types.js +89 -0
  41. package/mcps/mcsc/packages/core/src/trail.js +17 -0
  42. package/mcps/mcsc/packages/mcp/server.js +302 -0
  43. package/mcps/mcsc/server.js +2 -0
  44. package/package.json +3 -1
  45. package/skills/basic/bdbmediastorm/SKILL.md +1 -1
  46. package/skills/basic/godmode-ui-ux/SKILL.md +2 -2
  47. package/skills/basic/godmode-ui-ux/references/anti-slop-rules.md +44 -40
  48. package/skills/basic/godmode-ui-ux/references/taste-archetypes.md +49 -0
  49. package/skills/basic/startcycle/SKILL.md +7 -0
  50. package/skills/basic/startcycle-graph/SKILL.md +12 -0
  51. package/skills/basic/startcycle-graph-user/SKILL.md +3 -0
  52. package/skills/basic/teamwork-preview/SKILL.md +2 -0
  53. package/skills/bdbrainstorm/SKILL.md +1 -0
  54. package/skills/global_config/agenttrail/LICENSE +21 -0
  55. package/skills/global_config/agenttrail/SKILL.md +48 -0
  56. package/skills/global_config/agenttrail/bin/agenttrail.mjs +1002 -0
  57. package/skills/global_config/agenttrail/public/index.html +661 -0
  58. package/skills/global_config/aos-project-init/SKILL.md +25 -5
  59. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +11 -0
  60. package/skills/global_config/aos-setup/SKILL.md +34 -1
  61. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +115 -1
  62. package/skills/global_config/archify/LICENSE +22 -0
  63. package/skills/global_config/archify/SKILL.md +140 -0
  64. package/skills/global_config/archify/THIRD_PARTY_NOTICES.md +69 -0
  65. package/skills/global_config/archify/assets/JetBrainsMono-OFL.txt +93 -0
  66. package/skills/global_config/archify/assets/template.html +14937 -0
  67. package/skills/global_config/archify/bin/archify.mjs +2139 -0
  68. package/skills/global_config/archify/bin/open-artifact.mjs +86 -0
  69. package/skills/global_config/archify/bin/preview.mjs +653 -0
  70. package/skills/global_config/archify/bin/visual-check.mjs +829 -0
  71. package/skills/global_config/archify/brand-marks/README.md +31 -0
  72. package/skills/global_config/archify/brand-marks/catalog.json +131 -0
  73. package/skills/global_config/archify/delta/architecture-delta.mjs +1232 -0
  74. package/skills/global_config/archify/examples/agent-run.lifecycle.json +60 -0
  75. package/skills/global_config/archify/examples/agent-tool-call.workflow.json +94 -0
  76. package/skills/global_config/archify/examples/async-job-roundtrip.sequence.json +61 -0
  77. package/skills/global_config/archify/examples/brand-aware-delivery.architecture.json +47 -0
  78. package/skills/global_config/archify/examples/cache-miss-request.sequence.json +82 -0
  79. package/skills/global_config/archify/examples/checkout-platform.base.architecture.json +31 -0
  80. package/skills/global_config/archify/examples/checkout-platform.head.architecture.json +31 -0
  81. package/skills/global_config/archify/examples/deployment-release.lifecycle.json +49 -0
  82. package/skills/global_config/archify/examples/event-stream.dataflow.json +57 -0
  83. package/skills/global_config/archify/examples/incident-response.workflow.json +64 -0
  84. package/skills/global_config/archify/examples/product-analytics.dataflow.json +76 -0
  85. package/skills/global_config/archify/examples/production-deployment.architecture.json +71 -0
  86. package/skills/global_config/archify/examples/release-delivery.workflow.json +62 -0
  87. package/skills/global_config/archify/examples/web-app.architecture.json +46 -0
  88. package/skills/global_config/archify/migrations/workflow-v2.mjs +279 -0
  89. package/skills/global_config/archify/package.json +39 -0
  90. package/skills/global_config/archify/recipes/scenarios.mjs +391 -0
  91. package/skills/global_config/archify/references/authoring-contract.md +247 -0
  92. package/skills/global_config/archify/references/brand-marks.md +65 -0
  93. package/skills/global_config/archify/references/delivery-contract.md +138 -0
  94. package/skills/global_config/archify/references/viewer-runtime.md +45 -0
  95. package/skills/global_config/archify/renderers/architecture/grid.mjs +62 -0
  96. package/skills/global_config/archify/renderers/architecture/render-architecture.mjs +1078 -0
  97. package/skills/global_config/archify/renderers/dataflow/README.md +104 -0
  98. package/skills/global_config/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  99. package/skills/global_config/archify/renderers/lifecycle/README.md +115 -0
  100. package/skills/global_config/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  101. package/skills/global_config/archify/renderers/sequence/README.md +114 -0
  102. package/skills/global_config/archify/renderers/sequence/render-sequence.mjs +464 -0
  103. package/skills/global_config/archify/renderers/shared/brand-marks.mjs +563 -0
  104. package/skills/global_config/archify/renderers/shared/cli.mjs +218 -0
  105. package/skills/global_config/archify/renderers/shared/desktop-readability.mjs +26 -0
  106. package/skills/global_config/archify/renderers/shared/diagnostics.mjs +160 -0
  107. package/skills/global_config/archify/renderers/shared/engineering-profiles.mjs +157 -0
  108. package/skills/global_config/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  109. package/skills/global_config/archify/renderers/shared/generated-validators.mjs +13 -0
  110. package/skills/global_config/archify/renderers/shared/geometry.mjs +1423 -0
  111. package/skills/global_config/archify/renderers/shared/i18n.mjs +595 -0
  112. package/skills/global_config/archify/renderers/shared/layout-report.mjs +40 -0
  113. package/skills/global_config/archify/renderers/shared/legend.mjs +217 -0
  114. package/skills/global_config/archify/renderers/shared/output-path.mjs +340 -0
  115. package/skills/global_config/archify/renderers/shared/repository-evidence.mjs +239 -0
  116. package/skills/global_config/archify/renderers/shared/repository-location.mjs +58 -0
  117. package/skills/global_config/archify/renderers/shared/text-fit.mjs +49 -0
  118. package/skills/global_config/archify/renderers/shared/utils.mjs +232 -0
  119. package/skills/global_config/archify/renderers/shared/validator.mjs +86 -0
  120. package/skills/global_config/archify/renderers/workflow/README.md +223 -0
  121. package/skills/global_config/archify/renderers/workflow/render-workflow.mjs +35 -0
  122. package/skills/global_config/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  123. package/skills/global_config/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  124. package/skills/global_config/archify/schemas/README.md +211 -0
  125. package/skills/global_config/archify/schemas/architecture.schema.json +178 -0
  126. package/skills/global_config/archify/schemas/common.schema.json +115 -0
  127. package/skills/global_config/archify/schemas/dataflow.schema.json +243 -0
  128. package/skills/global_config/archify/schemas/lifecycle.schema.json +266 -0
  129. package/skills/global_config/archify/schemas/sequence.schema.json +223 -0
  130. package/skills/global_config/archify/schemas/workflow.schema.json +428 -0
  131. package/skills/global_config/archify/scripts/check-render-output.mjs +916 -0
  132. package/skills/global_config/archify/scripts/generate-brand-marks.mjs +141 -0
  133. package/skills/global_config/archify/scripts/generate-validators.mjs +66 -0
  134. package/skills/global_config/archify/scripts/render-examples.mjs +26 -0
  135. package/skills/global_config/archify/skill-release.json +10 -0
  136. package/skills/global_config/bdb-security-audit/SKILL.md +109 -0
  137. package/skills/global_config/bdb-security-audit/references/audit-checklists.md +172 -0
  138. package/skills/global_config/bdb-security-audit/references/skylos-guide.md +139 -0
  139. package/skills/global_config/bdb-security-audit/references/xalgorix-bridge.md +110 -0
  140. package/skills/global_config/bdbdesignpro/SKILL.md +4 -4
  141. package/skills/global_config/bdbdesignpro/references/bdb-motion-tokens.md +52 -31
  142. package/skills/global_config/bdbresilience/SKILL.md +10 -0
  143. package/skills/global_config/brag/SKILL.md +179 -0
  144. package/skills/global_config/brag/assets/music/README.md +20 -0
  145. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.json +3846 -0
  146. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.md +35 -0
  147. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.json +1826 -0
  148. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.md +35 -0
  149. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.json +2266 -0
  150. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.md +35 -0
  151. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.json +2806 -0
  152. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.md +35 -0
  153. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.json +2796 -0
  154. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.md +35 -0
  155. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-1-by-ende-dot-app.mp3 +0 -0
  156. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-10-by-ende-dot-app.mp3 +0 -0
  157. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-11-by-ende-dot-app.mp3 +0 -0
  158. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-12-by-ende-dot-app.mp3 +0 -0
  159. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-9-by-ende-dot-app.mp3 +0 -0
  160. package/skills/global_config/brag/assets/sfx/casino/card-fan-1.ogg +0 -0
  161. package/skills/global_config/brag/assets/sfx/casino/card-fan-2.ogg +0 -0
  162. package/skills/global_config/brag/assets/sfx/casino/card-place-1.ogg +0 -0
  163. package/skills/global_config/brag/assets/sfx/casino/card-place-2.ogg +0 -0
  164. package/skills/global_config/brag/assets/sfx/casino/card-place-3.ogg +0 -0
  165. package/skills/global_config/brag/assets/sfx/casino/card-place-4.ogg +0 -0
  166. package/skills/global_config/brag/assets/sfx/casino/card-shove-1.ogg +0 -0
  167. package/skills/global_config/brag/assets/sfx/casino/card-shove-2.ogg +0 -0
  168. package/skills/global_config/brag/assets/sfx/casino/card-shove-3.ogg +0 -0
  169. package/skills/global_config/brag/assets/sfx/casino/card-shove-4.ogg +0 -0
  170. package/skills/global_config/brag/assets/sfx/casino/card-shuffle.ogg +0 -0
  171. package/skills/global_config/brag/assets/sfx/casino/card-slide-1.ogg +0 -0
  172. package/skills/global_config/brag/assets/sfx/casino/card-slide-2.ogg +0 -0
  173. package/skills/global_config/brag/assets/sfx/casino/card-slide-3.ogg +0 -0
  174. package/skills/global_config/brag/assets/sfx/casino/card-slide-4.ogg +0 -0
  175. package/skills/global_config/brag/assets/sfx/casino/card-slide-5.ogg +0 -0
  176. package/skills/global_config/brag/assets/sfx/casino/card-slide-6.ogg +0 -0
  177. package/skills/global_config/brag/assets/sfx/casino/card-slide-7.ogg +0 -0
  178. package/skills/global_config/brag/assets/sfx/casino/card-slide-8.ogg +0 -0
  179. package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-1.ogg +0 -0
  180. package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-2.ogg +0 -0
  181. package/skills/global_config/brag/assets/sfx/casino/chip-lay-1.ogg +0 -0
  182. package/skills/global_config/brag/assets/sfx/casino/chip-lay-2.ogg +0 -0
  183. package/skills/global_config/brag/assets/sfx/casino/chip-lay-3.ogg +0 -0
  184. package/skills/global_config/brag/assets/sfx/casino/chips-collide-1.ogg +0 -0
  185. package/skills/global_config/brag/assets/sfx/casino/chips-collide-2.ogg +0 -0
  186. package/skills/global_config/brag/assets/sfx/casino/chips-collide-3.ogg +0 -0
  187. package/skills/global_config/brag/assets/sfx/casino/chips-collide-4.ogg +0 -0
  188. package/skills/global_config/brag/assets/sfx/casino/chips-handle-1.ogg +0 -0
  189. package/skills/global_config/brag/assets/sfx/casino/chips-handle-2.ogg +0 -0
  190. package/skills/global_config/brag/assets/sfx/casino/chips-handle-3.ogg +0 -0
  191. package/skills/global_config/brag/assets/sfx/casino/chips-handle-4.ogg +0 -0
  192. package/skills/global_config/brag/assets/sfx/casino/chips-handle-6.ogg +0 -0
  193. package/skills/global_config/brag/assets/sfx/casino/chips-stack-1.ogg +0 -0
  194. package/skills/global_config/brag/assets/sfx/casino/chips-stack-2.ogg +0 -0
  195. package/skills/global_config/brag/assets/sfx/casino/chips-stack-3.ogg +0 -0
  196. package/skills/global_config/brag/assets/sfx/casino/chips-stack-4.ogg +0 -0
  197. package/skills/global_config/brag/assets/sfx/casino/chips-stack-5.ogg +0 -0
  198. package/skills/global_config/brag/assets/sfx/casino/chips-stack-6.ogg +0 -0
  199. package/skills/global_config/brag/assets/sfx/casino/dice-grab-1.ogg +0 -0
  200. package/skills/global_config/brag/assets/sfx/casino/dice-grab-2.ogg +0 -0
  201. package/skills/global_config/brag/assets/sfx/casino/dice-shake-1.ogg +0 -0
  202. package/skills/global_config/brag/assets/sfx/casino/dice-shake-2.ogg +0 -0
  203. package/skills/global_config/brag/assets/sfx/casino/dice-shake-3.ogg +0 -0
  204. package/skills/global_config/brag/assets/sfx/casino/dice-throw-1.ogg +0 -0
  205. package/skills/global_config/brag/assets/sfx/casino/dice-throw-2.ogg +0 -0
  206. package/skills/global_config/brag/assets/sfx/casino/dice-throw-3.ogg +0 -0
  207. package/skills/global_config/brag/assets/sfx/casino/die-throw-1.ogg +0 -0
  208. package/skills/global_config/brag/assets/sfx/casino/die-throw-2.ogg +0 -0
  209. package/skills/global_config/brag/assets/sfx/casino/die-throw-3.ogg +0 -0
  210. package/skills/global_config/brag/assets/sfx/casino/die-throw-4.ogg +0 -0
  211. package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_000.ogg +0 -0
  212. package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_003.ogg +0 -0
  213. package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_004.ogg +0 -0
  214. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_000.ogg +0 -0
  215. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_001.ogg +0 -0
  216. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_002.ogg +0 -0
  217. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_003.ogg +0 -0
  218. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_004.ogg +0 -0
  219. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_000.ogg +0 -0
  220. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_001.ogg +0 -0
  221. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_002.ogg +0 -0
  222. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_003.ogg +0 -0
  223. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_004.ogg +0 -0
  224. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_000.ogg +0 -0
  225. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_001.ogg +0 -0
  226. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_002.ogg +0 -0
  227. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_003.ogg +0 -0
  228. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_004.ogg +0 -0
  229. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_000.ogg +0 -0
  230. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_001.ogg +0 -0
  231. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_002.ogg +0 -0
  232. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_003.ogg +0 -0
  233. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_004.ogg +0 -0
  234. package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_000.ogg +0 -0
  235. package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_003.ogg +0 -0
  236. package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_004.ogg +0 -0
  237. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_000.ogg +0 -0
  238. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_001.ogg +0 -0
  239. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_002.ogg +0 -0
  240. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_003.ogg +0 -0
  241. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_004.ogg +0 -0
  242. package/skills/global_config/brag/assets/sfx/impact/impactGlass_heavy_002.ogg +0 -0
  243. package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_001.ogg +0 -0
  244. package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_002.ogg +0 -0
  245. package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_003.ogg +0 -0
  246. package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_000.ogg +0 -0
  247. package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_002.ogg +0 -0
  248. package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_004.ogg +0 -0
  249. package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_000.ogg +0 -0
  250. package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_002.ogg +0 -0
  251. package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_004.ogg +0 -0
  252. package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_002.ogg +0 -0
  253. package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_003.ogg +0 -0
  254. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_000.ogg +0 -0
  255. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_001.ogg +0 -0
  256. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_002.ogg +0 -0
  257. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_003.ogg +0 -0
  258. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_004.ogg +0 -0
  259. package/skills/global_config/brag/assets/sfx/impact/impactMining_001.ogg +0 -0
  260. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_000.ogg +0 -0
  261. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_001.ogg +0 -0
  262. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_002.ogg +0 -0
  263. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_003.ogg +0 -0
  264. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_004.ogg +0 -0
  265. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_000.ogg +0 -0
  266. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_001.ogg +0 -0
  267. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_002.ogg +0 -0
  268. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_003.ogg +0 -0
  269. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_004.ogg +0 -0
  270. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_000.ogg +0 -0
  271. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_001.ogg +0 -0
  272. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_002.ogg +0 -0
  273. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_003.ogg +0 -0
  274. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_004.ogg +0 -0
  275. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_000.ogg +0 -0
  276. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_001.ogg +0 -0
  277. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_002.ogg +0 -0
  278. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_003.ogg +0 -0
  279. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_004.ogg +0 -0
  280. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_000.ogg +0 -0
  281. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_001.ogg +0 -0
  282. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_002.ogg +0 -0
  283. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_003.ogg +0 -0
  284. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_004.ogg +0 -0
  285. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_000.ogg +0 -0
  286. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_001.ogg +0 -0
  287. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_002.ogg +0 -0
  288. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_003.ogg +0 -0
  289. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_004.ogg +0 -0
  290. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_000.ogg +0 -0
  291. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_001.ogg +0 -0
  292. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_002.ogg +0 -0
  293. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_003.ogg +0 -0
  294. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_004.ogg +0 -0
  295. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_000.ogg +0 -0
  296. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_001.ogg +0 -0
  297. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_002.ogg +0 -0
  298. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_003.ogg +0 -0
  299. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_004.ogg +0 -0
  300. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_000.ogg +0 -0
  301. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_001.ogg +0 -0
  302. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_002.ogg +0 -0
  303. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_003.ogg +0 -0
  304. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_004.ogg +0 -0
  305. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_000.ogg +0 -0
  306. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_001.ogg +0 -0
  307. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_002.ogg +0 -0
  308. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_003.ogg +0 -0
  309. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_004.ogg +0 -0
  310. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_000.ogg +0 -0
  311. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_001.ogg +0 -0
  312. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_002.ogg +0 -0
  313. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_003.ogg +0 -0
  314. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_004.ogg +0 -0
  315. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_000.ogg +0 -0
  316. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_001.ogg +0 -0
  317. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_002.ogg +0 -0
  318. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_003.ogg +0 -0
  319. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_004.ogg +0 -0
  320. package/skills/global_config/brag/assets/sfx/interface/bong_001.ogg +0 -0
  321. package/skills/global_config/brag/assets/sfx/interface/click_001.ogg +0 -0
  322. package/skills/global_config/brag/assets/sfx/interface/click_002.ogg +0 -0
  323. package/skills/global_config/brag/assets/sfx/interface/click_003.ogg +0 -0
  324. package/skills/global_config/brag/assets/sfx/interface/click_004.ogg +0 -0
  325. package/skills/global_config/brag/assets/sfx/interface/click_005.ogg +0 -0
  326. package/skills/global_config/brag/assets/sfx/interface/drop_001.ogg +0 -0
  327. package/skills/global_config/brag/assets/sfx/interface/drop_002.ogg +0 -0
  328. package/skills/global_config/brag/assets/sfx/interface/drop_003.ogg +0 -0
  329. package/skills/global_config/brag/assets/sfx/interface/error_005.ogg +0 -0
  330. package/skills/global_config/brag/assets/sfx/interface/error_006.ogg +0 -0
  331. package/skills/global_config/brag/assets/sfx/interface/glitch_002.ogg +0 -0
  332. package/skills/global_config/brag/assets/sfx/interface/glitch_004.ogg +0 -0
  333. package/skills/global_config/brag/assets/sfx/interface/select_008.ogg +0 -0
  334. package/skills/global_config/brag/assets/sfx/interface/switch_001.ogg +0 -0
  335. package/skills/global_config/brag/assets/sfx/interface/switch_002.ogg +0 -0
  336. package/skills/global_config/brag/assets/sfx/interface/switch_004.ogg +0 -0
  337. package/skills/global_config/brag/assets/sfx/interface/switch_005.ogg +0 -0
  338. package/skills/global_config/brag/assets/sfx/interface/switch_006.ogg +0 -0
  339. package/skills/global_config/brag/assets/sfx/interface/switch_007.ogg +0 -0
  340. package/skills/global_config/brag/assets/sfx/keyboard/keypress-001.wav +0 -0
  341. package/skills/global_config/brag/assets/sfx/keyboard/keypress-002.wav +0 -0
  342. package/skills/global_config/brag/assets/sfx/keyboard/keypress-003.wav +0 -0
  343. package/skills/global_config/brag/assets/sfx/keyboard/keypress-004.wav +0 -0
  344. package/skills/global_config/brag/assets/sfx/keyboard/keypress-005.wav +0 -0
  345. package/skills/global_config/brag/assets/sfx/keyboard/keypress-006.wav +0 -0
  346. package/skills/global_config/brag/assets/sfx/keyboard/keypress-007.wav +0 -0
  347. package/skills/global_config/brag/assets/sfx/keyboard/keypress-008.wav +0 -0
  348. package/skills/global_config/brag/assets/sfx/keyboard/keypress-009.wav +0 -0
  349. package/skills/global_config/brag/assets/sfx/keyboard/keypress-010.wav +0 -0
  350. package/skills/global_config/brag/assets/sfx/keyboard/keypress-011.wav +0 -0
  351. package/skills/global_config/brag/assets/sfx/keyboard/keypress-012.wav +0 -0
  352. package/skills/global_config/brag/assets/sfx/keyboard/keypress-013.wav +0 -0
  353. package/skills/global_config/brag/assets/sfx/keyboard/keypress-014.wav +0 -0
  354. package/skills/global_config/brag/assets/sfx/keyboard/keypress-015.wav +0 -0
  355. package/skills/global_config/brag/assets/sfx/keyboard/keypress-016.wav +0 -0
  356. package/skills/global_config/brag/assets/sfx/keyboard/keypress-017.wav +0 -0
  357. package/skills/global_config/brag/assets/sfx/keyboard/keypress-018.wav +0 -0
  358. package/skills/global_config/brag/assets/sfx/keyboard/keypress-019.wav +0 -0
  359. package/skills/global_config/brag/assets/sfx/keyboard/keypress-020.wav +0 -0
  360. package/skills/global_config/brag/assets/sfx/keyboard/keypress-021.wav +0 -0
  361. package/skills/global_config/brag/assets/sfx/keyboard/keypress-022.wav +0 -0
  362. package/skills/global_config/brag/assets/sfx/keyboard/keypress-023.wav +0 -0
  363. package/skills/global_config/brag/assets/sfx/keyboard/keypress-024.wav +0 -0
  364. package/skills/global_config/brag/assets/sfx/keyboard/keypress-025.wav +0 -0
  365. package/skills/global_config/brag/assets/sfx/keyboard/keypress-026.wav +0 -0
  366. package/skills/global_config/brag/assets/sfx/keyboard/keypress-027.wav +0 -0
  367. package/skills/global_config/brag/assets/sfx/keyboard/keypress-028.wav +0 -0
  368. package/skills/global_config/brag/assets/sfx/keyboard/keypress-029.wav +0 -0
  369. package/skills/global_config/brag/assets/sfx/keyboard/keypress-030.wav +0 -0
  370. package/skills/global_config/brag/assets/sfx/keyboard/keypress-031.wav +0 -0
  371. package/skills/global_config/brag/assets/sfx/keyboard/keypress-032.wav +0 -0
  372. package/skills/global_config/brag/assets/sfx/sfx-analysis.json +15584 -0
  373. package/skills/global_config/brag/assets/sfx/sfx-analysis.md +75 -0
  374. package/skills/global_config/brag/assets/sfx/ui/click1.ogg +0 -0
  375. package/skills/global_config/brag/assets/sfx/ui/click2.ogg +0 -0
  376. package/skills/global_config/brag/assets/sfx/ui/click3.ogg +0 -0
  377. package/skills/global_config/brag/assets/sfx/ui/click4.ogg +0 -0
  378. package/skills/global_config/brag/assets/sfx/ui/click5.ogg +0 -0
  379. package/skills/global_config/brag/assets/sfx/ui/mouseclick1.ogg +0 -0
  380. package/skills/global_config/brag/assets/sfx/ui/rollover1.ogg +0 -0
  381. package/skills/global_config/brag/assets/sfx/ui/rollover2.ogg +0 -0
  382. package/skills/global_config/brag/assets/sfx/ui/rollover4.ogg +0 -0
  383. package/skills/global_config/brag/assets/sfx/ui/rollover5.ogg +0 -0
  384. package/skills/global_config/brag/assets/sfx/ui/switch1.ogg +0 -0
  385. package/skills/global_config/brag/assets/sfx/ui/switch10.ogg +0 -0
  386. package/skills/global_config/brag/assets/sfx/ui/switch11.ogg +0 -0
  387. package/skills/global_config/brag/assets/sfx/ui/switch12.ogg +0 -0
  388. package/skills/global_config/brag/assets/sfx/ui/switch13.ogg +0 -0
  389. package/skills/global_config/brag/assets/sfx/ui/switch14.ogg +0 -0
  390. package/skills/global_config/brag/assets/sfx/ui/switch15.ogg +0 -0
  391. package/skills/global_config/brag/assets/sfx/ui/switch16.ogg +0 -0
  392. package/skills/global_config/brag/assets/sfx/ui/switch17.ogg +0 -0
  393. package/skills/global_config/brag/assets/sfx/ui/switch18.ogg +0 -0
  394. package/skills/global_config/brag/assets/sfx/ui/switch19.ogg +0 -0
  395. package/skills/global_config/brag/assets/sfx/ui/switch2.ogg +0 -0
  396. package/skills/global_config/brag/assets/sfx/ui/switch20.ogg +0 -0
  397. package/skills/global_config/brag/assets/sfx/ui/switch21.ogg +0 -0
  398. package/skills/global_config/brag/assets/sfx/ui/switch22.ogg +0 -0
  399. package/skills/global_config/brag/assets/sfx/ui/switch23.ogg +0 -0
  400. package/skills/global_config/brag/assets/sfx/ui/switch24.ogg +0 -0
  401. package/skills/global_config/brag/assets/sfx/ui/switch25.ogg +0 -0
  402. package/skills/global_config/brag/assets/sfx/ui/switch26.ogg +0 -0
  403. package/skills/global_config/brag/assets/sfx/ui/switch27.ogg +0 -0
  404. package/skills/global_config/brag/assets/sfx/ui/switch28.ogg +0 -0
  405. package/skills/global_config/brag/assets/sfx/ui/switch29.ogg +0 -0
  406. package/skills/global_config/brag/assets/sfx/ui/switch3.ogg +0 -0
  407. package/skills/global_config/brag/assets/sfx/ui/switch30.ogg +0 -0
  408. package/skills/global_config/brag/assets/sfx/ui/switch31.ogg +0 -0
  409. package/skills/global_config/brag/assets/sfx/ui/switch32.ogg +0 -0
  410. package/skills/global_config/brag/assets/sfx/ui/switch33.ogg +0 -0
  411. package/skills/global_config/brag/assets/sfx/ui/switch34.ogg +0 -0
  412. package/skills/global_config/brag/assets/sfx/ui/switch35.ogg +0 -0
  413. package/skills/global_config/brag/assets/sfx/ui/switch36.ogg +0 -0
  414. package/skills/global_config/brag/assets/sfx/ui/switch37.ogg +0 -0
  415. package/skills/global_config/brag/assets/sfx/ui/switch38.ogg +0 -0
  416. package/skills/global_config/brag/assets/sfx/ui/switch4.ogg +0 -0
  417. package/skills/global_config/brag/assets/sfx/ui/switch5.ogg +0 -0
  418. package/skills/global_config/brag/assets/sfx/ui/switch6.ogg +0 -0
  419. package/skills/global_config/brag/assets/sfx/ui/switch7.ogg +0 -0
  420. package/skills/global_config/brag/assets/sfx/ui/switch8.ogg +0 -0
  421. package/skills/global_config/brag/assets/sfx/ui/switch9.ogg +0 -0
  422. package/skills/global_config/brag/references/audio.md +307 -0
  423. package/skills/global_config/brag/references/step-1-inspect.md +133 -0
  424. package/skills/global_config/brag/references/step-2-plan.md +234 -0
  425. package/skills/global_config/brag/references/step-3-compose.md +210 -0
  426. package/skills/global_config/brag/references/step-4-deliver.md +168 -0
  427. package/skills/global_config/brag/references/tones.md +231 -0
  428. package/skills/global_config/brag/scripts/analyze_music_cues.py +309 -0
  429. package/skills/global_config/brag/scripts/pyproject.toml +14 -0
  430. package/skills/global_config/brag/scripts/uv.lock +819 -0
  431. package/skills/global_config/deja-memory/SKILL.md +42 -0
  432. package/skills/global_config/editable-design/LICENSE +202 -0
  433. package/skills/global_config/editable-design/README.md +34 -0
  434. package/skills/global_config/editable-design/SKILL.md +630 -0
  435. package/skills/global_config/editable-design/THIRD_PARTY_NOTICES.md +10 -0
  436. package/skills/global_config/editable-design/agents/openai.yaml +4 -0
  437. package/skills/global_config/editable-design/assets/editor/layer-editor.css +262 -0
  438. package/skills/global_config/editable-design/assets/editor/layer-editor.js +1929 -0
  439. package/skills/global_config/editable-design/assets/poster-starter/README.md +92 -0
  440. package/skills/global_config/editable-design/assets/poster-starter/assets/.gitkeep +0 -0
  441. package/skills/global_config/editable-design/assets/poster-starter/index.html +91 -0
  442. package/skills/global_config/editable-design/assets/poster-starter/poster.json +6 -0
  443. package/skills/global_config/editable-design/assets/poster-starter/reference/.gitkeep +1 -0
  444. package/skills/global_config/editable-design/assets/replay/icons/assets.png +0 -0
  445. package/skills/global_config/editable-design/assets/replay/icons/html.png +0 -0
  446. package/skills/global_config/editable-design/assets/replay/icons/input.png +0 -0
  447. package/skills/global_config/editable-design/assets/replay/icons/layers.png +0 -0
  448. package/skills/global_config/editable-design/assets/replay/icons/plan.png +0 -0
  449. package/skills/global_config/editable-design/assets/replay/icons/reference.png +0 -0
  450. package/skills/global_config/editable-design/assets/replay/icons/review.png +0 -0
  451. package/skills/global_config/editable-design/assets/replay/index.html +59 -0
  452. package/skills/global_config/editable-design/assets/replay/replay-motion.js +410 -0
  453. package/skills/global_config/editable-design/assets/replay/replay.css +380 -0
  454. package/skills/global_config/editable-design/assets/replay/replay.js +374 -0
  455. package/skills/global_config/editable-design/font-kit/package-lock.json +115 -0
  456. package/skills/global_config/editable-design/font-kit/package.json +19 -0
  457. package/skills/global_config/editable-design/pack.sh +20 -0
  458. package/skills/global_config/editable-design/references/asset-architecture.md +72 -0
  459. package/skills/global_config/editable-design/references/editor-pitfalls.md +70 -0
  460. package/skills/global_config/editable-design/references/editor-runtime.md +131 -0
  461. package/skills/global_config/editable-design/references/font-system.md +89 -0
  462. package/skills/global_config/editable-design/references/imagery.md +145 -0
  463. package/skills/global_config/editable-design/references/layout-typography.md +116 -0
  464. package/skills/global_config/editable-design/references/replay-contract.md +169 -0
  465. package/skills/global_config/editable-design/scripts/_browser.mjs +247 -0
  466. package/skills/global_config/editable-design/scripts/bake.mjs +87 -0
  467. package/skills/global_config/editable-design/scripts/build-replay.mjs +316 -0
  468. package/skills/global_config/editable-design/scripts/check-contract.mjs +293 -0
  469. package/skills/global_config/editable-design/scripts/check-fonts.sh +252 -0
  470. package/skills/global_config/editable-design/scripts/check-poster.sh +70 -0
  471. package/skills/global_config/editable-design/scripts/doctor.sh +78 -0
  472. package/skills/global_config/editable-design/scripts/explode.mjs +115 -0
  473. package/skills/global_config/editable-design/scripts/font-kit.mjs +187 -0
  474. package/skills/global_config/editable-design/scripts/import-asset.sh +23 -0
  475. package/skills/global_config/editable-design/scripts/init-poster.sh +64 -0
  476. package/skills/global_config/editable-design/scripts/install-font-kit.sh +13 -0
  477. package/skills/global_config/editable-design/scripts/package-lock.json +334 -0
  478. package/skills/global_config/editable-design/scripts/package.json +14 -0
  479. package/skills/global_config/editable-design/scripts/remove-chroma-key.py +201 -0
  480. package/skills/global_config/editable-design/scripts/render-poster.sh +202 -0
  481. package/skills/global_config/editable-design/scripts/trace.sh +159 -0
  482. package/skills/global_config/editable-design/scripts/verify-replay.mjs +97 -0
  483. package/skills/global_config/editable-design/scripts/verify.mjs +782 -0
  484. package/skills/global_config/editable-design/scripts/wire-editor.mjs +104 -0
  485. package/skills/global_config/editable-design/tests/fixtures/small-explode/index.html +86 -0
  486. package/skills/global_config/plan-canvas/SKILL.md +8 -0
@@ -0,0 +1,916 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { collectAmbiguousCorridors, collectBorderRuns, collectLabelRouteClearance, collectRouteRhythmIssues, routeBudgetMetrics } from '../renderers/shared/geometry.mjs';
6
+ import {
7
+ DESKTOP_READABILITY_VIEWPORT,
8
+ DESKTOP_READER_DIAGRAM_WIDTH,
9
+ MIN_PROJECTED_NODE_TEXT_PX,
10
+ projectedNodeTextPx,
11
+ } from '../renderers/shared/desktop-readability.mjs';
12
+
13
+ const input = process.argv[2];
14
+
15
+ if (!input || input === '-h' || input === '--help') {
16
+ console.error('Usage: node scripts/check-render-output.mjs <diagram.html>');
17
+ process.exit(input ? 0 : 2);
18
+ }
19
+
20
+ const htmlPath = path.resolve(input);
21
+ let html;
22
+ try {
23
+ html = fs.readFileSync(htmlPath, 'utf8');
24
+ } catch (err) {
25
+ console.error(JSON.stringify({
26
+ ok: false,
27
+ file: htmlPath,
28
+ checks: [{ name: 'file_readable', ok: false, details: [err.message] }],
29
+ }, null, 2));
30
+ process.exit(1);
31
+ }
32
+
33
+ const checks = [];
34
+ let composition = {
35
+ schemaVersion: 1,
36
+ profile: 'standard',
37
+ status: 'pass',
38
+ summary: { errors: 0, warnings: 0 },
39
+ metrics: {
40
+ properCrossings: 0,
41
+ ambiguousCorridors: 0,
42
+ containerBorderRuns: 0,
43
+ labelRouteClearanceIssues: 0,
44
+ minLabelRouteClearance: null,
45
+ maxBends: 0,
46
+ routesOverSuggestedBends: 0,
47
+ maxStretch: null,
48
+ routesOverSuggestedStretch: 0,
49
+ minSegmentPx: null,
50
+ minInteriorSegmentPx: null,
51
+ shortSegmentCount: 0,
52
+ shortEndpointSegmentCount: 0,
53
+ shortInteriorSegmentCount: 0,
54
+ microSegmentCount: 0,
55
+ desktopReadabilityIssues: 0,
56
+ minProjectedNodeTextPx: null,
57
+ },
58
+ suggestedLimits: { bendsPerRelationship: 2, stretch: 1.35, segmentPx: 16, microSegmentPx: 8 },
59
+ issues: [],
60
+ };
61
+
62
+ const NON_FINITE_TOKEN = /\b(?:NaN|undefined|Infinity)\b/;
63
+ // Consume comments/CDATA as whole tokens, including any tag-like prose.
64
+ const SVG_TAG_TOKEN = /<!--[\s\S]*?(?:-->|$)|<!\[CDATA\[[\s\S]*?(?:\]\]>|$)|<(\/?)([A-Za-z][\w:-]*)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>/g;
65
+ const HTML_VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
66
+ const SVG_HTML_INTEGRATION_POINTS = new Set(['foreignobject', 'desc', 'title']);
67
+ const HTML_ATTRIBUTE = /([A-Za-z_:][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
68
+ const NUMERIC_ATTRS = new Set([
69
+ 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'dx', 'dy', 'cx', 'cy', 'r', 'rx', 'ry', 'fx', 'fy',
70
+ 'fr', 'width', 'height', 'd', 'points', 'pathlength', 'transform', 'viewbox', 'offset',
71
+ 'opacity', 'fill-opacity', 'flood-opacity', 'stop-opacity', 'stroke-opacity', 'font-size',
72
+ 'font-size-adjust', 'font-weight', 'letter-spacing', 'word-spacing', 'kerning', 'stroke-width',
73
+ 'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit', 'textlength', 'startoffset',
74
+ 'rotate', 'markerwidth', 'markerheight', 'refx', 'refy', 'orient', 'patterntransform',
75
+ 'gradienttransform', 'filterres', 'stddeviation', 'basefrequency', 'numoctaves', 'seed',
76
+ 'surfacescale', 'diffuseconstant', 'specularconstant', 'specularexponent',
77
+ 'limitingconeangle', 'azimuth', 'elevation', 'pointsatx', 'pointsaty', 'pointsatz',
78
+ 'kernelmatrix', 'order', 'divisor', 'bias', 'targetx', 'targety', 'kernelunitlength',
79
+ 'scale', 'radius', 'k1', 'k2', 'k3', 'k4', 'tablevalues', 'slope', 'intercept',
80
+ 'amplitude', 'exponent',
81
+ ]);
82
+ const ELEMENT_NUMERIC_ATTRS = new Map([
83
+ ['fecolormatrix', new Set(['values'])],
84
+ ['fepointlight', new Set(['z'])],
85
+ ['fespotlight', new Set(['z'])],
86
+ ]);
87
+
88
+ function addCheck(name, ok, details = []) {
89
+ checks.push({ name, ok, details });
90
+ }
91
+
92
+ const svgMatches = [...html.matchAll(/<svg\b[\s\S]*?<\/svg>/gi)];
93
+ addCheck('single_svg', svgMatches.length === 1, [`found ${svgMatches.length} <svg> block(s)`]);
94
+
95
+ if (svgMatches.length === 1) {
96
+ const svg = svgMatches[0][0];
97
+ const svgRoot = svg.match(/<svg\b[^>]*>/i)?.[0] || '';
98
+ const svgAttrs = parseAttrs(svgRoot);
99
+ const qualityProfile = svgAttrs['data-quality-profile'] || 'standard';
100
+ const qualityGatesEnforced = svgAttrs['data-quality-gates'] !== 'advisory';
101
+ const nonFiniteAttrs = collectNonFiniteAttrs(svg);
102
+ addCheck('finite_svg', nonFiniteAttrs.length === 0, nonFiniteAttrs);
103
+ const legendStart = svg.indexOf('<!-- Legend -->');
104
+ const beforeLegend = legendStart >= 0 ? svg.slice(0, legendStart) : svg;
105
+ const desktopReadabilityIssue = collectDesktopReadability(svgAttrs, beforeLegend);
106
+ const arrows = collectArrows(beforeLegend);
107
+ const diagonal = arrows.flatMap((arrow) => diagonalStraightSegments(arrow).map((segment) => ({ arrow, ...segment })));
108
+ addCheck(
109
+ 'orthogonal_arrows',
110
+ diagonal.length === 0,
111
+ diagonal.map(({ arrow, segmentIndex }) => `${arrow.kind} ${arrow.index} segment ${segmentIndex + 1}: ${arrow.raw}`),
112
+ );
113
+ const relationshipCrossings = collectRelationshipCrossings(arrows);
114
+ const compositionFrames = collectCompositionFrames(beforeLegend);
115
+ const containerBorderRuns = collectBorderRuns({
116
+ routedRelations: arrows
117
+ .filter((arrow) => arrow.from && arrow.to && arrow.borderSegments.length)
118
+ .map((arrow) => ({
119
+ relation: arrow,
120
+ relationIndex: arrow.index,
121
+ segments: arrow.borderSegments,
122
+ })),
123
+ frames: compositionFrames,
124
+ });
125
+ const routedRelationships = arrows
126
+ .filter((arrow) => arrow.from && arrow.to && arrow.routePoints.length)
127
+ .map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints }));
128
+ const routeMetrics = routeBudgetMetrics({ routedRelations: routedRelationships });
129
+ const routeRhythmIssues = collectRouteRhythmIssues({ routedRelations: routedRelationships });
130
+ const ambiguousCorridors = collectAmbiguousCorridors({ routedRelations: routedRelationships });
131
+ const relationshipLabels = collectRelationshipLabelMasks(beforeLegend, arrows);
132
+ const labelClearanceThreshold = qualityProfile === 'showcase' ? 4 : 2;
133
+ const labelRouteMeasurements = collectLabelRouteClearance({
134
+ labels: relationshipLabels,
135
+ routedRelations: arrows.map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints })),
136
+ threshold: Number.MAX_VALUE,
137
+ });
138
+ const labelRouteClearance = collectLabelRouteClearance({
139
+ labels: relationshipLabels,
140
+ routedRelations: arrows.map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints })),
141
+ threshold: labelClearanceThreshold,
142
+ });
143
+ const crossingIsError = qualityProfile === 'showcase';
144
+ const corridorIsError = qualityProfile === 'showcase';
145
+ const rhythmIsError = qualityProfile === 'showcase';
146
+ const labelClearanceIsError = qualityProfile === 'showcase';
147
+ const desktopReadabilityIsError = qualityProfile === 'showcase';
148
+ const compositionErrors = (qualityGatesEnforced ? containerBorderRuns.length : 0)
149
+ + (crossingIsError ? relationshipCrossings.length : 0)
150
+ + (corridorIsError ? ambiguousCorridors.length : 0)
151
+ + (labelClearanceIsError ? labelRouteClearance.length : 0)
152
+ + (rhythmIsError ? routeRhythmIssues.length : 0)
153
+ + (desktopReadabilityIsError && desktopReadabilityIssue ? 1 : 0);
154
+ const compositionWarnings = (qualityGatesEnforced ? 0 : containerBorderRuns.length)
155
+ + (crossingIsError ? 0 : relationshipCrossings.length)
156
+ + (corridorIsError ? 0 : ambiguousCorridors.length)
157
+ + (labelClearanceIsError ? 0 : labelRouteClearance.length)
158
+ + (rhythmIsError ? 0 : routeRhythmIssues.length)
159
+ + (desktopReadabilityIsError || !desktopReadabilityIssue ? 0 : 1);
160
+ composition = {
161
+ schemaVersion: 1,
162
+ profile: qualityProfile,
163
+ status: compositionErrors ? 'fail' : 'pass',
164
+ summary: {
165
+ errors: compositionErrors,
166
+ warnings: compositionWarnings,
167
+ },
168
+ metrics: {
169
+ properCrossings: relationshipCrossings.length,
170
+ ambiguousCorridors: ambiguousCorridors.length,
171
+ containerBorderRuns: containerBorderRuns.length,
172
+ labelRouteClearanceIssues: labelRouteClearance.length,
173
+ minLabelRouteClearance: labelRouteMeasurements.length
174
+ ? Math.round(Math.min(...labelRouteMeasurements.map((hit) => hit.clearance)) * 10) / 10
175
+ : null,
176
+ desktopReadabilityIssues: desktopReadabilityIssue ? 1 : 0,
177
+ minProjectedNodeTextPx: desktopReadabilityIssue?.projectedFontPx ?? null,
178
+ ...roundedRouteMetrics(routeMetrics),
179
+ },
180
+ suggestedLimits: { bendsPerRelationship: 2, stretch: 1.35, segmentPx: 16, microSegmentPx: 8 },
181
+ issues: [
182
+ ...containerBorderRuns.map((hit) => ({
183
+ severity: qualityGatesEnforced ? 'error' : 'warning',
184
+ code: 'composition/container-border-run',
185
+ relationship: relationshipRecord(hit.relation),
186
+ frame: frameRecord(hit.frame),
187
+ side: hit.side,
188
+ segmentIndex: hit.segmentIndex,
189
+ overlapLength: Math.round(hit.overlapLength * 10) / 10,
190
+ from: hit.overlapStart.map((value) => Math.round(value * 10) / 10),
191
+ to: hit.overlapEnd.map((value) => Math.round(value * 10) / 10),
192
+ })),
193
+ ...labelRouteClearance.map((hit) => ({
194
+ severity: labelClearanceIsError ? 'error' : 'warning',
195
+ code: 'composition/label-route-clearance',
196
+ label: hit.label?.label || hit.labelRelation?.label || '',
197
+ labelRelationship: relationshipRecord(hit.labelRelation),
198
+ otherRelationship: relationshipRecord(hit.otherRelation),
199
+ segmentIndex: hit.segmentIndex,
200
+ labelRect: roundedRect(hit.rect),
201
+ clearance: Math.round(hit.clearance * 10) / 10,
202
+ intersectionLength: Math.round((hit.intersectionLength || 0) * 10) / 10,
203
+ threshold: hit.threshold,
204
+ from: hit.start.map((value) => Math.round(value * 10) / 10),
205
+ to: hit.end.map((value) => Math.round(value * 10) / 10),
206
+ })),
207
+ ...relationshipCrossings.map((hit) => ({
208
+ severity: crossingIsError ? 'error' : 'warning',
209
+ code: 'composition/proper-crossing',
210
+ relationship: relationshipRecord(hit.left),
211
+ otherRelationship: relationshipRecord(hit.right),
212
+ point: hit.point.map((value) => Math.round(value * 10) / 10),
213
+ })),
214
+ ...ambiguousCorridors.map((hit) => ({
215
+ severity: corridorIsError ? 'error' : 'warning',
216
+ code: 'composition/ambiguous-corridor',
217
+ relationship: relationshipRecord(hit.left.relation),
218
+ otherRelationship: relationshipRecord(hit.right.relation),
219
+ segmentIndex: hit.leftSegment,
220
+ otherSegmentIndex: hit.rightSegment,
221
+ overlapLength: Math.round(hit.overlapLength * 10) / 10,
222
+ from: hit.overlapStart.map((value) => Math.round(value * 10) / 10),
223
+ to: hit.overlapEnd.map((value) => Math.round(value * 10) / 10),
224
+ })),
225
+ ...routeRhythmIssues.map((hit) => ({
226
+ severity: rhythmIsError ? 'error' : 'warning',
227
+ code: hit.code,
228
+ relationship: relationshipRecord(hit.relation),
229
+ segmentIndex: hit.segmentIndex,
230
+ position: hit.position,
231
+ length: Math.round(hit.length * 10) / 10,
232
+ from: hit.start.map((value) => Math.round(value * 10) / 10),
233
+ to: hit.end.map((value) => Math.round(value * 10) / 10),
234
+ })),
235
+ ...(desktopReadabilityIssue ? [{
236
+ severity: desktopReadabilityIsError ? 'error' : 'warning',
237
+ code: 'composition/desktop-readability',
238
+ viewportWidth: DESKTOP_READABILITY_VIEWPORT.width,
239
+ viewportHeight: DESKTOP_READABILITY_VIEWPORT.height,
240
+ availableDiagramWidth: DESKTOP_READER_DIAGRAM_WIDTH,
241
+ viewBoxWidth: desktopReadabilityIssue.viewBoxWidth,
242
+ scale: desktopReadabilityIssue.scale,
243
+ text: desktopReadabilityIssue.text,
244
+ detail: desktopReadabilityIssue.detail,
245
+ sourceFontPx: desktopReadabilityIssue.sourceFontPx,
246
+ projectedFontPx: desktopReadabilityIssue.projectedFontPx,
247
+ minimumProjectedFontPx: MIN_PROJECTED_NODE_TEXT_PX,
248
+ }] : []),
249
+ ],
250
+ };
251
+ addCheck(
252
+ 'label_route_clearance',
253
+ !labelClearanceIsError || labelRouteClearance.length === 0,
254
+ labelRouteClearance.map((hit) => (
255
+ `[composition/label-route-clearance] ${qualityProfile} label "${hit.label?.label || hit.labelRelation?.label || ''}" on ${relationshipName(hit.labelRelation)} is ${Math.round(hit.clearance * 10) / 10}px from ${relationshipName(hit.otherRelation)} segment ${hit.segmentIndex} [${formatPoint(hit.start)}] -> [${formatPoint(hit.end)}]${hit.intersectionLength > 0 ? ` with ${Math.round(hit.intersectionLength * 10) / 10}px hidden by the mask` : ''} (minimum ${hit.threshold}px) — use renderer-supported label controls (message y for sequence; otherwise labelAt, labelDx, labelDy, or labelSegment), or adjust the other relationship route/via/channel.`
256
+ )),
257
+ );
258
+ addCheck(
259
+ 'relationship_crossings',
260
+ !crossingIsError || relationshipCrossings.length === 0,
261
+ relationshipCrossings.map((hit) => (
262
+ `[composition/proper-crossing] ${qualityProfile} ${relationshipName(hit.left)} crosses ${relationshipName(hit.right)} at [${formatPoint(hit.point)}]`
263
+ )),
264
+ );
265
+ addCheck(
266
+ 'relationship_corridors',
267
+ !corridorIsError || ambiguousCorridors.length === 0,
268
+ ambiguousCorridors.map((hit) => (
269
+ `[composition/ambiguous-corridor] ${qualityProfile} ${relationshipName(hit.left.relation)} shares a ${Math.round(hit.overlapLength * 10) / 10}px corridor with ${relationshipName(hit.right.relation)} at [${formatPoint(hit.overlapStart)}] -> [${formatPoint(hit.overlapEnd)}]`
270
+ )),
271
+ );
272
+ addCheck(
273
+ 'container_border_runs',
274
+ !qualityGatesEnforced || containerBorderRuns.length === 0,
275
+ containerBorderRuns.map((hit) => (
276
+ `[composition/container-border-run] ${relationshipName(hit.relation)} follows ${frameName(hit.frame)} ${hit.side} border for ${Math.round(hit.overlapLength * 10) / 10}px on segment ${hit.segmentIndex} [${formatPoint(hit.overlapStart)}] -> [${formatPoint(hit.overlapEnd)}]`
277
+ )),
278
+ );
279
+ addCheck(
280
+ 'route_rhythm',
281
+ !rhythmIsError || routeRhythmIssues.length === 0,
282
+ routeRhythmIssues.map((hit) => (
283
+ `[${hit.code}] ${qualityProfile} ${relationshipName(hit.relation)} has a ${Math.round(hit.length * 10) / 10}px ${hit.position} segment ${hit.segmentIndex} [${formatPoint(hit.start)}] -> [${formatPoint(hit.end)}]`
284
+ )),
285
+ );
286
+
287
+ if (legendStart >= 0) {
288
+ const legendFragment = svg.slice(legendStart);
289
+ const legendBoxes = collectLegendBoxes(legendFragment);
290
+ const collisions = collectLegendCollisions(arrows, legendBoxes);
291
+ addCheck(
292
+ 'legend_clearance',
293
+ collisions.length === 0,
294
+ collisions.map((hit) => `${hit.arrow.kind} ${hit.arrow.index} crosses legend ${hit.box.label}`),
295
+ );
296
+ } else {
297
+ addCheck('legend_clearance', true, ['no legend marker found']);
298
+ }
299
+ }
300
+
301
+ const ok = checks.every((check) => check.ok) && composition.status !== 'fail';
302
+ console.log(JSON.stringify({ ok, file: htmlPath, checks, composition }, null, 2));
303
+ // Let pending stdout writes drain: large receipts are asynchronous when piped.
304
+ process.exitCode = ok ? 0 : 1;
305
+
306
+ function collectArrows(fragment) {
307
+ const arrows = [];
308
+ let index = 0;
309
+
310
+ for (const tag of fragment.matchAll(/<(path|line)\b[^>]*>/gi)) {
311
+ const raw = tag[0];
312
+ if (!/\bclass="[^"]*\ba-(?:default|emphasis|security|dashed)\b/.test(raw)) continue;
313
+ if (!/\bmarker-end=/.test(raw)) continue;
314
+ const attrs = parseAttrs(raw);
315
+ const segments = tag[1].toLowerCase() === 'line'
316
+ ? lineSegments(attrs)
317
+ : pathSegments(attrs.d || '');
318
+ const borderSegments = tag[1].toLowerCase() === 'line'
319
+ ? segments
320
+ : straightPathSegments(attrs.d || '');
321
+ arrows.push({
322
+ kind: tag[1].toLowerCase(),
323
+ index: index += 1,
324
+ raw,
325
+ segments,
326
+ borderSegments,
327
+ routePoints: parseRoutePoints(attrs['data-composition-points']) || (
328
+ borderSegments.length ? [borderSegments[0].start, ...borderSegments.map((segment) => segment.end)] : []
329
+ ),
330
+ from: attrs['data-edge-from'] || attrs['data-composition-edge-from'],
331
+ to: attrs['data-edge-to'] || attrs['data-composition-edge-to'],
332
+ id: attrs['data-edge-id'] || attrs['data-composition-edge-id'],
333
+ key: attrs['data-edge-key'],
334
+ label: attrs['data-edge-label'],
335
+ offset: tag.index,
336
+ });
337
+ }
338
+
339
+ return arrows;
340
+ }
341
+
342
+ function collectRelationshipLabelMasks(fragment, arrows) {
343
+ const labels = [];
344
+ for (const match of fragment.matchAll(/<g\b[^>]*\bdata-edge-(?:key|id|from)="[^"]*"[^>]*>[\s\S]*?<\/g>/gi)) {
345
+ const group = match[0];
346
+ const groupAttrs = parseAttrs(group.match(/<g\b[^>]*>/i)?.[0] || '');
347
+ const rectTag = [...group.matchAll(/<rect\b[^>]*>/gi)]
348
+ .map((item) => item[0])
349
+ .find((tag) => /\bclass="[^"]*\bc-mask\b/.test(tag));
350
+ if (!rectTag) continue;
351
+ const attrs = parseAttrs(rectTag);
352
+ const rect = {
353
+ x: numberAttr(attrs, 'x'),
354
+ y: numberAttr(attrs, 'y'),
355
+ width: numberAttr(attrs, 'width'),
356
+ height: numberAttr(attrs, 'height'),
357
+ };
358
+ if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) continue;
359
+ const groupStart = match.index;
360
+ const groupEnd = groupStart + group.length;
361
+ const containedOwner = arrows.find((arrow) => (
362
+ arrow.offset > groupStart
363
+ && arrow.offset < groupEnd
364
+ && arrow.from === groupAttrs['data-edge-from']
365
+ && arrow.to === groupAttrs['data-edge-to']
366
+ && (!groupAttrs['data-edge-id'] || !arrow.id || arrow.id === groupAttrs['data-edge-id'])
367
+ ));
368
+ const owner = arrows.find((arrow) => (
369
+ groupAttrs['data-edge-key'] !== undefined && arrow.key === groupAttrs['data-edge-key']
370
+ )) || containedOwner || arrows.find((arrow) => (
371
+ arrow.id === groupAttrs['data-edge-id']
372
+ && arrow.from === groupAttrs['data-edge-from']
373
+ && arrow.to === groupAttrs['data-edge-to']
374
+ ));
375
+ if (!owner) continue;
376
+ if (owner.key === undefined && groupAttrs['data-edge-key'] !== undefined) owner.key = groupAttrs['data-edge-key'];
377
+ if (!owner.id && groupAttrs['data-edge-id']) owner.id = groupAttrs['data-edge-id'];
378
+ if (!owner.label && groupAttrs['data-edge-label']) owner.label = groupAttrs['data-edge-label'];
379
+ labels.push({
380
+ relation: owner,
381
+ relationIndex: owner.index,
382
+ label: groupAttrs['data-edge-label'] || '',
383
+ rect,
384
+ });
385
+ }
386
+ return labels;
387
+ }
388
+
389
+ function roundedRect(rect) {
390
+ return Object.fromEntries(Object.entries(rect).map(([key, value]) => [key, Math.round(value * 10) / 10]));
391
+ }
392
+
393
+ function roundedRouteMetrics(metrics) {
394
+ return {
395
+ ...metrics,
396
+ maxStretch: metrics.maxStretch == null ? null : Math.round(metrics.maxStretch * 1000) / 1000,
397
+ minSegmentPx: metrics.minSegmentPx == null ? null : Math.round(metrics.minSegmentPx * 10) / 10,
398
+ minInteriorSegmentPx: metrics.minInteriorSegmentPx == null ? null : Math.round(metrics.minInteriorSegmentPx * 10) / 10,
399
+ };
400
+ }
401
+
402
+ function parseRoutePoints(value) {
403
+ if (!value) return null;
404
+ const points = value.split(';').map((pair) => pair.split(',').map(Number));
405
+ return points.length >= 2 && points.every(isPoint) ? points : null;
406
+ }
407
+
408
+ function collectRelationshipCrossings(arrows) {
409
+ const relationships = arrows.filter((arrow) => arrow.from && arrow.to && arrow.segments.length);
410
+ const crossings = [];
411
+ for (let leftIndex = 0; leftIndex < relationships.length; leftIndex += 1) {
412
+ const left = relationships[leftIndex];
413
+ for (let rightIndex = leftIndex + 1; rightIndex < relationships.length; rightIndex += 1) {
414
+ const right = relationships[rightIndex];
415
+ if ([left.from, left.to].some((id) => id === right.from || id === right.to)) continue;
416
+ let point = null;
417
+ for (const leftSegment of left.segments) {
418
+ for (const rightSegment of right.segments) {
419
+ point = properSegmentIntersection(leftSegment.start, leftSegment.end, rightSegment.start, rightSegment.end);
420
+ if (point) break;
421
+ }
422
+ if (point) break;
423
+ }
424
+ if (point) crossings.push({ left, right, point });
425
+ }
426
+ }
427
+ return crossings;
428
+ }
429
+
430
+ function relationshipName(arrow) {
431
+ return arrow.id
432
+ ? `relationship id "${arrow.id}" ("${arrow.from}" -> "${arrow.to}")`
433
+ : `relationship "${arrow.from}" -> "${arrow.to}"`;
434
+ }
435
+
436
+ function relationshipRecord(arrow) {
437
+ const stableIndex = Number(arrow.key);
438
+ return {
439
+ id: arrow.id,
440
+ from: arrow.from,
441
+ to: arrow.to,
442
+ label: arrow.label || '',
443
+ collectionIndex: Number.isInteger(stableIndex) && stableIndex >= 0 ? stableIndex : arrow.index - 1,
444
+ artifactIndex: arrow.index,
445
+ };
446
+ }
447
+
448
+ function collectCompositionFrames(fragment) {
449
+ const frames = [];
450
+ for (const match of fragment.matchAll(/<(rect|path|line)\b[^>]*>/gi)) {
451
+ const attrs = parseAttrs(match[0]);
452
+ const kind = attrs['data-composition-frame-kind'];
453
+ if (!kind) continue;
454
+ const identity = attrs['data-composition-frame-id'] || frames.length;
455
+ if (match[1].toLowerCase() === 'rect') {
456
+ const frame = {
457
+ kind,
458
+ id: identity,
459
+ x: numberAttr(attrs, 'x'),
460
+ y: numberAttr(attrs, 'y'),
461
+ width: numberAttr(attrs, 'width'),
462
+ height: numberAttr(attrs, 'height'),
463
+ radius: numberAttr(attrs, 'rx') || 0,
464
+ };
465
+ if ([frame.x, frame.y, frame.width, frame.height].every(Number.isFinite)) frames.push(frame);
466
+ continue;
467
+ }
468
+ const segments = match[1].toLowerCase() === 'line'
469
+ ? lineSegments(attrs)
470
+ : pathSegments(attrs.d || '');
471
+ for (const [segmentIndex, segment] of segments.entries()) {
472
+ frames.push({
473
+ kind,
474
+ id: segments.length > 1 ? `${identity}:${segmentIndex}` : identity,
475
+ shape: 'line',
476
+ start: segment.start,
477
+ end: segment.end,
478
+ });
479
+ }
480
+ }
481
+ return frames;
482
+ }
483
+
484
+ function frameName(frame) {
485
+ return `${frame.kind || 'frame'} "${frame.id}"`;
486
+ }
487
+
488
+ function frameRecord(frame) {
489
+ return { kind: frame.kind, id: frame.id };
490
+ }
491
+
492
+ function formatPoint(point) {
493
+ return point.map((value) => Math.round(value * 10) / 10).join(', ');
494
+ }
495
+
496
+ function lineSegments(attrs) {
497
+ const start = [numberAttr(attrs, 'x1'), numberAttr(attrs, 'y1')];
498
+ const end = [numberAttr(attrs, 'x2'), numberAttr(attrs, 'y2')];
499
+ if (!isPoint(start) || !isPoint(end)) return [];
500
+ return [{ start, end }];
501
+ }
502
+
503
+ function pathSegments(d) {
504
+ const points = pointsFromPath(d);
505
+ const segments = [];
506
+ for (let i = 1; i < points.length; i += 1) {
507
+ segments.push({ start: points[i - 1], end: points[i] });
508
+ }
509
+ return segments;
510
+ }
511
+
512
+ // Border runs use exact visible primitives. Non-collinear Q curves are never
513
+ // flattened into chords here: a tangent or sampled near-horizontal curve is
514
+ // not a structural border run. A fully collinear Q remains a straight visible
515
+ // primitive and is included.
516
+ function straightPathSegments(d) {
517
+ const tokens = d.match(/[MLHVQZmlhvqz]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/g) || [];
518
+ const segments = [];
519
+ let i = 0;
520
+ let command = '';
521
+ let current = [0, 0];
522
+ let start = null;
523
+ while (i < tokens.length) {
524
+ if (isCommand(tokens[i])) command = tokens[i++];
525
+ if (!command) break;
526
+ const absolute = command === command.toUpperCase();
527
+ switch (command.toUpperCase()) {
528
+ case 'M':
529
+ case 'L': {
530
+ let first = true;
531
+ while (i + 1 < tokens.length && !isCommand(tokens[i])) {
532
+ const point = [Number.parseFloat(tokens[i++]), Number.parseFloat(tokens[i++])];
533
+ if (!point.every(Number.isFinite)) break;
534
+ const next = absolute ? point : [current[0] + point[0], current[1] + point[1]];
535
+ if (command.toUpperCase() === 'L' || !first) segments.push({ start: current, end: next });
536
+ current = next;
537
+ if (!start) start = current;
538
+ first = false;
539
+ }
540
+ break;
541
+ }
542
+ case 'H': {
543
+ while (i < tokens.length && !isCommand(tokens[i])) {
544
+ const value = Number.parseFloat(tokens[i++]);
545
+ if (!Number.isFinite(value)) break;
546
+ const next = [absolute ? value : current[0] + value, current[1]];
547
+ segments.push({ start: current, end: next });
548
+ current = next;
549
+ }
550
+ break;
551
+ }
552
+ case 'V': {
553
+ while (i < tokens.length && !isCommand(tokens[i])) {
554
+ const value = Number.parseFloat(tokens[i++]);
555
+ if (!Number.isFinite(value)) break;
556
+ const next = [current[0], absolute ? value : current[1] + value];
557
+ segments.push({ start: current, end: next });
558
+ current = next;
559
+ }
560
+ break;
561
+ }
562
+ case 'Q': {
563
+ while (i + 3 < tokens.length && !isCommand(tokens[i])) {
564
+ const values = [0, 0, 0, 0].map(() => Number.parseFloat(tokens[i++]));
565
+ if (!values.every(Number.isFinite)) break;
566
+ const control = absolute ? values.slice(0, 2) : [current[0] + values[0], current[1] + values[1]];
567
+ const end = absolute ? values.slice(2, 4) : [current[0] + values[2], current[1] + values[3]];
568
+ if (Math.abs(crossProduct(current, control, end)) <= 1e-9) segments.push({ start: current, end });
569
+ current = end;
570
+ }
571
+ break;
572
+ }
573
+ case 'Z':
574
+ if (start) segments.push({ start: current, end: start });
575
+ current = start || current;
576
+ command = '';
577
+ break;
578
+ default:
579
+ return [];
580
+ }
581
+ }
582
+ return segments.filter(({ start: a, end: b }) => isPoint(a) && isPoint(b));
583
+ }
584
+
585
+ function diagonalStraightSegments(arrow) {
586
+ return arrow.borderSegments.flatMap(({ start, end }, segmentIndex) => (
587
+ Math.abs(start[0] - end[0]) > 0.01 && Math.abs(start[1] - end[1]) > 0.01
588
+ ? [{ segmentIndex, start, end }]
589
+ : []
590
+ ));
591
+ }
592
+
593
+ function collectLegendBoxes(fragment) {
594
+ const boxes = [];
595
+
596
+ for (const match of fragment.matchAll(/<rect\b[^>]*>/gi)) {
597
+ const attrs = parseAttrs(match[0]);
598
+ const x = numberAttr(attrs, 'x');
599
+ const y = numberAttr(attrs, 'y');
600
+ const width = numberAttr(attrs, 'width');
601
+ const height = numberAttr(attrs, 'height');
602
+ if ([x, y, width, height].every(Number.isFinite)) {
603
+ boxes.push({ x1: x, y1: y, x2: x + width, y2: y + height, label: `rect@${x},${y}` });
604
+ }
605
+ }
606
+
607
+ for (const match of fragment.matchAll(/<text\b([^>]*)>([\s\S]*?)<\/text>/gi)) {
608
+ const attrs = parseAttrs(match[1]);
609
+ const box = textBox(attrs, stripTags(match[2]).trim());
610
+ if (box) boxes.push(box);
611
+ }
612
+
613
+ return boxes;
614
+ }
615
+
616
+ function collectLegendCollisions(arrows, boxes) {
617
+ const collisions = [];
618
+ for (const arrow of arrows) {
619
+ for (const segment of arrow.segments) {
620
+ for (const box of boxes) {
621
+ if (segmentIntersectsBox(segment, padBox(box, 2))) {
622
+ collisions.push({ arrow, box });
623
+ }
624
+ }
625
+ }
626
+ }
627
+ return collisions;
628
+ }
629
+
630
+ function textBox(attrs, text) {
631
+ const x = numberAttr(attrs, 'x');
632
+ const y = numberAttr(attrs, 'y');
633
+ const fontSize = Number.parseFloat(attrs['font-size'] || '10');
634
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(fontSize)) return null;
635
+ const width = estimatedTextWidth(text, fontSize);
636
+ const anchor = attrs['text-anchor'] || 'start';
637
+ let x1 = x;
638
+ if (anchor === 'middle') x1 = x - width / 2;
639
+ if (anchor === 'end') x1 = x - width;
640
+ return {
641
+ x1,
642
+ y1: y - fontSize,
643
+ x2: x1 + width,
644
+ y2: y + fontSize * 0.25,
645
+ label: text || `text@${x},${y}`,
646
+ };
647
+ }
648
+
649
+ function collectDesktopReadability(svgAttrs, fragment) {
650
+ const viewBox = String(svgAttrs.viewBox || '').trim().split(/[\s,]+/).map(Number);
651
+ const viewBoxWidth = viewBox.length === 4 ? viewBox[2] : Number.NaN;
652
+ if (!Number.isFinite(viewBoxWidth) || viewBoxWidth <= 0) return null;
653
+ const scale = Math.min(1, DESKTOP_READER_DIAGRAM_WIDTH / viewBoxWidth);
654
+ let worst = null;
655
+ for (const match of fragment.matchAll(/<text\b([^>]*)>([\s\S]*?)<\/text>/gi)) {
656
+ const primary = /\bdata-node-label(?:\s*=|\s|$)/i.test(match[1]);
657
+ const boundary = /\bdata-boundary-label(?:\s*=|\s|$)/i.test(match[1]);
658
+ const context = /\bdata-detail\s*=\s*"context"/i.test(match[1]);
659
+ if (!primary && !boundary && !context) continue;
660
+ const attrs = parseAttrs(match[1]);
661
+ const fontSize = Number.parseFloat(attrs['font-size'] || '');
662
+ if (!Number.isFinite(fontSize)) continue;
663
+ const projected = projectedNodeTextPx(fontSize, viewBoxWidth);
664
+ if (projected >= MIN_PROJECTED_NODE_TEXT_PX) continue;
665
+ const candidate = {
666
+ viewBoxWidth,
667
+ scale,
668
+ text: stripTags(match[2]).trim(),
669
+ detail: primary
670
+ ? 'primary'
671
+ : boundary ? 'boundary' : 'context',
672
+ sourceFontPx: fontSize,
673
+ projectedFontPx: projected,
674
+ };
675
+ if (!worst || candidate.projectedFontPx < worst.projectedFontPx) worst = candidate;
676
+ }
677
+ return worst;
678
+ }
679
+
680
+ function estimatedTextWidth(text, fontSize) {
681
+ let units = 0;
682
+ for (const char of text) units += char.charCodeAt(0) > 255 ? 1.8 : 0.62;
683
+ return Math.max(fontSize, units * fontSize);
684
+ }
685
+
686
+ function pointsFromPath(d) {
687
+ const tokens = d.match(/[MLHVQZmlhvqz]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/g) || [];
688
+ const points = [];
689
+ let i = 0;
690
+ let command = '';
691
+ let current = [0, 0];
692
+ let start = null;
693
+
694
+ while (i < tokens.length) {
695
+ if (isCommand(tokens[i])) command = tokens[i++];
696
+ if (!command) break;
697
+
698
+ const absolute = command === command.toUpperCase();
699
+ switch (command.toUpperCase()) {
700
+ case 'M':
701
+ case 'L': {
702
+ while (i + 1 < tokens.length && !isCommand(tokens[i])) {
703
+ const x = Number.parseFloat(tokens[i++]);
704
+ const y = Number.parseFloat(tokens[i++]);
705
+ if (!Number.isFinite(x) || !Number.isFinite(y)) break;
706
+ current = absolute ? [x, y] : [current[0] + x, current[1] + y];
707
+ points.push(current);
708
+ if (!start) start = current;
709
+ }
710
+ break;
711
+ }
712
+ case 'H': {
713
+ while (i < tokens.length && !isCommand(tokens[i])) {
714
+ const x = Number.parseFloat(tokens[i++]);
715
+ if (!Number.isFinite(x)) break;
716
+ current = absolute ? [x, current[1]] : [current[0] + x, current[1]];
717
+ points.push(current);
718
+ }
719
+ break;
720
+ }
721
+ case 'V': {
722
+ while (i < tokens.length && !isCommand(tokens[i])) {
723
+ const y = Number.parseFloat(tokens[i++]);
724
+ if (!Number.isFinite(y)) break;
725
+ current = absolute ? [current[0], y] : [current[0], current[1] + y];
726
+ points.push(current);
727
+ }
728
+ break;
729
+ }
730
+ case 'Q': {
731
+ while (i + 3 < tokens.length && !isCommand(tokens[i])) {
732
+ const controlX = Number.parseFloat(tokens[i++]);
733
+ const controlY = Number.parseFloat(tokens[i++]);
734
+ const endX = Number.parseFloat(tokens[i++]);
735
+ const endY = Number.parseFloat(tokens[i++]);
736
+ if (![controlX, controlY, endX, endY].every(Number.isFinite)) break;
737
+ const control = absolute
738
+ ? [controlX, controlY]
739
+ : [current[0] + controlX, current[1] + controlY];
740
+ const end = absolute
741
+ ? [endX, endY]
742
+ : [current[0] + endX, current[1] + endY];
743
+ const startPoint = current;
744
+ for (let step = 1; step <= 8; step += 1) {
745
+ const amount = step / 8;
746
+ const remaining = 1 - amount;
747
+ points.push([
748
+ remaining * remaining * startPoint[0] + 2 * remaining * amount * control[0] + amount * amount * end[0],
749
+ remaining * remaining * startPoint[1] + 2 * remaining * amount * control[1] + amount * amount * end[1],
750
+ ]);
751
+ }
752
+ current = end;
753
+ }
754
+ break;
755
+ }
756
+ case 'Z': {
757
+ if (start) points.push(start);
758
+ break;
759
+ }
760
+ default:
761
+ return [];
762
+ }
763
+ }
764
+
765
+ return points.filter(isPoint);
766
+ }
767
+
768
+ function properSegmentIntersection(a, b, c, d) {
769
+ const abC = crossProduct(a, b, c);
770
+ const abD = crossProduct(a, b, d);
771
+ const cdA = crossProduct(c, d, a);
772
+ const cdB = crossProduct(c, d, b);
773
+ const epsilon = 1e-9;
774
+ const opposite = (left, right) => (left > epsilon && right < -epsilon) || (left < -epsilon && right > epsilon);
775
+ if (!opposite(abC, abD) || !opposite(cdA, cdB)) return null;
776
+ const denominator = (a[0] - b[0]) * (c[1] - d[1]) - (a[1] - b[1]) * (c[0] - d[0]);
777
+ if (Math.abs(denominator) < epsilon) return null;
778
+ const ab = a[0] * b[1] - a[1] * b[0];
779
+ const cd = c[0] * d[1] - c[1] * d[0];
780
+ return [
781
+ (ab * (c[0] - d[0]) - (a[0] - b[0]) * cd) / denominator,
782
+ (ab * (c[1] - d[1]) - (a[1] - b[1]) * cd) / denominator,
783
+ ];
784
+ }
785
+
786
+ function crossProduct(a, b, c) {
787
+ return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
788
+ }
789
+
790
+ function segmentIntersectsBox(segment, box) {
791
+ const { start, end } = segment;
792
+ if (pointInsideBox(start, box) || pointInsideBox(end, box)) return true;
793
+ const edges = [
794
+ [[box.x1, box.y1], [box.x2, box.y1]],
795
+ [[box.x2, box.y1], [box.x2, box.y2]],
796
+ [[box.x2, box.y2], [box.x1, box.y2]],
797
+ [[box.x1, box.y2], [box.x1, box.y1]],
798
+ ];
799
+ return edges.some(([a, b]) => segmentsIntersect(start, end, a, b));
800
+ }
801
+
802
+ function segmentsIntersect(a, b, c, d) {
803
+ const o1 = orientation(a, b, c);
804
+ const o2 = orientation(a, b, d);
805
+ const o3 = orientation(c, d, a);
806
+ const o4 = orientation(c, d, b);
807
+
808
+ if (o1 !== o2 && o3 !== o4) return true;
809
+ if (o1 === 0 && onSegment(a, c, b)) return true;
810
+ if (o2 === 0 && onSegment(a, d, b)) return true;
811
+ if (o3 === 0 && onSegment(c, a, d)) return true;
812
+ if (o4 === 0 && onSegment(c, b, d)) return true;
813
+ return false;
814
+ }
815
+
816
+ function orientation(a, b, c) {
817
+ const value = (b[1] - a[1]) * (c[0] - b[0]) - (b[0] - a[0]) * (c[1] - b[1]);
818
+ if (Math.abs(value) < 1e-9) return 0;
819
+ return value > 0 ? 1 : 2;
820
+ }
821
+
822
+ function onSegment(a, b, c) {
823
+ return b[0] <= Math.max(a[0], c[0]) + 1e-9
824
+ && b[0] + 1e-9 >= Math.min(a[0], c[0])
825
+ && b[1] <= Math.max(a[1], c[1]) + 1e-9
826
+ && b[1] + 1e-9 >= Math.min(a[1], c[1]);
827
+ }
828
+
829
+ function pointInsideBox(point, box) {
830
+ return point[0] >= box.x1 && point[0] <= box.x2 && point[1] >= box.y1 && point[1] <= box.y2;
831
+ }
832
+
833
+ function padBox(box, padding) {
834
+ return {
835
+ ...box,
836
+ x1: box.x1 - padding,
837
+ y1: box.y1 - padding,
838
+ x2: box.x2 + padding,
839
+ y2: box.y2 + padding,
840
+ };
841
+ }
842
+
843
+ // Only geometry/numeric attributes are scanned: authored prose such as node
844
+ // tags, <title> text, aria-labels, or data-* attributes may legitimately
845
+ // mention "NaN" or "Infinity" without any coordinate being non-finite.
846
+ function collectNonFiniteAttrs(svg) {
847
+ const details = [];
848
+ const stack = [];
849
+ for (const match of svg.matchAll(SVG_TAG_TOKEN)) {
850
+ if (!match[2]) continue;
851
+ const element = match[2].toLowerCase();
852
+ if (match[1]) {
853
+ const index = stack.map(entry => entry.element).lastIndexOf(element);
854
+ if (index >= 0) stack.length = index;
855
+ continue;
856
+ }
857
+ const parent = stack[stack.length - 1];
858
+ const inSvg = element === 'svg'
859
+ || Boolean(parent?.inSvg && !SVG_HTML_INTEGRATION_POINTS.has(parent.element));
860
+ if (inSvg) {
861
+ for (const [name, value] of attrEntries(match[0])) {
862
+ if (!isNumericAttr(element, name) || !NON_FINITE_TOKEN.test(decodeNumericReferences(value))) continue;
863
+ details.push(`${match[2]} ${name}="${value}"`);
864
+ }
865
+ }
866
+ // HTML void elements do not open a context; SVG self-closing tags do not
867
+ // either. A nested <svg> inside foreignObject restores SVG checking.
868
+ if (!(inSvg ? /\/\s*>$/.test(match[0]) : HTML_VOID_ELEMENTS.has(element))) {
869
+ stack.push({ element, inSvg });
870
+ }
871
+ }
872
+ return details;
873
+ }
874
+
875
+ function decodeNumericReferences(value) {
876
+ // Numeric references can encode every letter of NaN/Infinity/undefined.
877
+ // Decode once, locally: other checks and diagnostic evidence keep raw values.
878
+ return value.replace(/&#(?:x([0-9a-f]+)|([0-9]+));?/gi, (_, hex, decimal) => {
879
+ const point = Number.parseInt(hex || decimal, hex ? 16 : 10);
880
+ return point > 0 && point <= 0x10ffff && !(point >= 0xd800 && point <= 0xdfff)
881
+ ? String.fromCodePoint(point) : '\uFFFD';
882
+ });
883
+ }
884
+
885
+ function isNumericAttr(elementName, attrName) {
886
+ const normalizedAttr = attrName.toLowerCase();
887
+ return NUMERIC_ATTRS.has(normalizedAttr)
888
+ || ELEMENT_NUMERIC_ATTRS.get(elementName.toLowerCase())?.has(normalizedAttr);
889
+ }
890
+
891
+ function attrEntries(tag) {
892
+ return [...tag.matchAll(HTML_ATTRIBUTE)].map((match) => [
893
+ match[1],
894
+ match[2] ?? match[3] ?? match[4],
895
+ ]);
896
+ }
897
+
898
+ function parseAttrs(tag) {
899
+ return Object.fromEntries(attrEntries(tag));
900
+ }
901
+
902
+ function numberAttr(attrs, name) {
903
+ return Number.parseFloat(attrs[name]);
904
+ }
905
+
906
+ function isCommand(token) {
907
+ return /^[A-Za-z]$/.test(token);
908
+ }
909
+
910
+ function isPoint(point) {
911
+ return Array.isArray(point) && point.length === 2 && point.every(Number.isFinite);
912
+ }
913
+
914
+ function stripTags(value) {
915
+ return value.replace(/<[^>]*>/g, '');
916
+ }