@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,829 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import {
8
+ DESKTOP_READABILITY_VIEWPORT,
9
+ MIN_PROJECTED_NODE_TEXT_PX,
10
+ } from '../renderers/shared/desktop-readability.mjs';
11
+
12
+ export const VISUAL_CHECK_VIEWPORTS = Object.freeze([
13
+ DESKTOP_READABILITY_VIEWPORT,
14
+ Object.freeze({ width: 1600, height: 1000 }),
15
+ Object.freeze({ width: 1920, height: 1080 }),
16
+ Object.freeze({ width: 2048, height: 1320 }),
17
+ ]);
18
+
19
+ const CAPTURE_VIEWPORTS = Object.freeze([
20
+ VISUAL_CHECK_VIEWPORTS[0],
21
+ VISUAL_CHECK_VIEWPORTS[VISUAL_CHECK_VIEWPORTS.length - 1],
22
+ ]);
23
+ const THEMES = Object.freeze(['light', 'dark']);
24
+ const EXIT = Object.freeze({ pass: 0, fail: 1, skipped: 2 });
25
+ export const CHROME_NO_SANDBOX_ENV = 'ARCHIFY_CHROME_NO_SANDBOX';
26
+
27
+ function sha256(buffer) {
28
+ return createHash('sha256').update(buffer).digest('hex');
29
+ }
30
+
31
+ function htmlEscape(value) {
32
+ return String(value).replace(/[&<>"']/g, (char) => ({
33
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
34
+ })[char]);
35
+ }
36
+
37
+ function safeUnlink(file) {
38
+ try {
39
+ fs.rmSync(file, { force: true });
40
+ } catch {
41
+ // A stale optional sidecar must never make the delivered HTML mutable.
42
+ }
43
+ }
44
+
45
+ function writeAtomic(file, contents) {
46
+ const temporary = `${file}.tmp-${process.pid}`;
47
+ try {
48
+ fs.writeFileSync(temporary, contents, { flag: 'w' });
49
+ fs.renameSync(temporary, file);
50
+ } finally {
51
+ safeUnlink(temporary);
52
+ }
53
+ }
54
+
55
+ function screenshotKey(width, height, theme) {
56
+ return `${width}x${height}:${theme}`;
57
+ }
58
+
59
+ export function sidecarPaths(artifactPath) {
60
+ const artifact = path.resolve(artifactPath);
61
+ const stem = artifact.replace(/\.html?$/i, '');
62
+ const base = `${stem}.visual-check`;
63
+ const screenshots = CAPTURE_VIEWPORTS.flatMap(({ width, height }) => THEMES.map((theme) => ({
64
+ width,
65
+ height,
66
+ theme,
67
+ path: `${base}.${width}x${height}.${theme}.png`,
68
+ })));
69
+ return {
70
+ base,
71
+ receipt: `${base}.json`,
72
+ contactSheet: `${base}.html`,
73
+ screenshots,
74
+ };
75
+ }
76
+
77
+ function cleanupCaptureSidecars(paths) {
78
+ safeUnlink(paths.contactSheet);
79
+ for (const screenshot of paths.screenshots) safeUnlink(screenshot.path);
80
+ }
81
+
82
+ function executable(file, platform = process.platform) {
83
+ if (!file) return null;
84
+ try {
85
+ fs.accessSync(file, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
86
+ return path.resolve(file);
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ function findOnPath(command, env, platform) {
93
+ const directories = String(env.PATH || '').split(path.delimiter).filter(Boolean);
94
+ const extensions = platform === 'win32'
95
+ ? String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
96
+ : [''];
97
+ for (const directory of directories) {
98
+ for (const extension of extensions) {
99
+ const candidate = path.join(directory, `${command}${extension}`);
100
+ const resolved = executable(candidate, platform);
101
+ if (resolved) return resolved;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ export function findChrome({ env = process.env, platform = process.platform } = {}) {
108
+ if (Object.prototype.hasOwnProperty.call(env, 'ARCHIFY_CHROME')) {
109
+ return executable(env.ARCHIFY_CHROME, platform);
110
+ }
111
+
112
+ const fixed = [];
113
+ const commands = [];
114
+ if (platform === 'darwin') {
115
+ fixed.push(
116
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
117
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
118
+ );
119
+ } else if (platform === 'win32') {
120
+ for (const root of [env.PROGRAMFILES, env['PROGRAMFILES(X86)'], env.LOCALAPPDATA].filter(Boolean)) {
121
+ fixed.push(
122
+ path.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'),
123
+ path.join(root, 'Chromium', 'Application', 'chrome.exe'),
124
+ );
125
+ }
126
+ } else {
127
+ commands.push('google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser');
128
+ }
129
+
130
+ for (const candidate of fixed) {
131
+ const resolved = executable(candidate, platform);
132
+ if (resolved) return resolved;
133
+ }
134
+ for (const command of commands) {
135
+ const resolved = findOnPath(command, env, platform);
136
+ if (resolved) return resolved;
137
+ }
138
+ return null;
139
+ }
140
+
141
+ class PipeCdp {
142
+ constructor(child, { failureDetails = () => '' } = {}) {
143
+ this.child = child;
144
+ this.failureDetails = failureDetails;
145
+ this.nextId = 1;
146
+ this.buffer = '';
147
+ this.pending = new Map();
148
+ this.waiters = [];
149
+ this.writePipe = child.stdio[3];
150
+ this.readPipe = child.stdio[4];
151
+ this.readPipe.setEncoding('utf8');
152
+ this.readPipe.on('data', (chunk) => this.consume(chunk));
153
+ this.writePipe.on('error', (error) => this.failAll(this.failure('write pipe', error)));
154
+ this.readPipe.on('error', (error) => this.failAll(this.failure('read pipe', error)));
155
+ child.once('error', (error) => this.failAll(this.failure('process launch', error)));
156
+ child.once('close', (code, signal) => {
157
+ const ending = signal ? `signal ${signal}` : `exit code ${code}`;
158
+ this.failAll(this.failure('process exit', new Error(`Chrome closed with ${ending}`)));
159
+ });
160
+ }
161
+
162
+ failure(stage, error) {
163
+ const code = error?.code ? ` [${error.code}]` : '';
164
+ const details = this.failureDetails();
165
+ return new Error([
166
+ `Chrome DevTools ${stage} failed: ${error?.message || String(error)}${code}`,
167
+ details,
168
+ ].filter(Boolean).join('\n'));
169
+ }
170
+
171
+ consume(chunk) {
172
+ this.buffer += chunk;
173
+ let boundary;
174
+ while ((boundary = this.buffer.indexOf('\0')) >= 0) {
175
+ const raw = this.buffer.slice(0, boundary);
176
+ this.buffer = this.buffer.slice(boundary + 1);
177
+ if (!raw) continue;
178
+ let message;
179
+ try {
180
+ message = JSON.parse(raw);
181
+ } catch (error) {
182
+ this.failAll(new Error(`Chrome DevTools returned invalid JSON: ${error.message}`));
183
+ continue;
184
+ }
185
+ if (message.id) {
186
+ const pending = this.pending.get(message.id);
187
+ if (!pending) continue;
188
+ clearTimeout(pending.timer);
189
+ this.pending.delete(message.id);
190
+ if (message.error) pending.reject(new Error(`${pending.method}: ${message.error.message}`));
191
+ else pending.resolve(message.result || {});
192
+ continue;
193
+ }
194
+ for (const waiter of [...this.waiters]) {
195
+ if (waiter.method !== message.method) continue;
196
+ if (waiter.sessionId && waiter.sessionId !== message.sessionId) continue;
197
+ clearTimeout(waiter.timer);
198
+ this.waiters.splice(this.waiters.indexOf(waiter), 1);
199
+ waiter.resolve(message.params || {});
200
+ }
201
+ }
202
+ }
203
+
204
+ send(method, params = {}, sessionId = undefined, timeoutMs = 15000) {
205
+ const id = this.nextId++;
206
+ const message = { id, method, params };
207
+ if (sessionId) message.sessionId = sessionId;
208
+ return new Promise((resolve, reject) => {
209
+ const timer = setTimeout(() => {
210
+ this.pending.delete(id);
211
+ reject(new Error(`${method}: timed out after ${timeoutMs}ms`));
212
+ }, timeoutMs);
213
+ this.pending.set(id, { method, resolve, reject, timer });
214
+ try {
215
+ this.writePipe.write(`${JSON.stringify(message)}\0`, (error) => {
216
+ if (error) this.failAll(this.failure('write pipe', error));
217
+ });
218
+ } catch (error) {
219
+ this.failAll(this.failure('write pipe', error));
220
+ }
221
+ });
222
+ }
223
+
224
+ waitFor(method, sessionId, timeoutMs = 15000) {
225
+ return new Promise((resolve, reject) => {
226
+ const waiter = { method, sessionId, resolve, reject, timer: null };
227
+ waiter.timer = setTimeout(() => {
228
+ this.waiters.splice(this.waiters.indexOf(waiter), 1);
229
+ reject(new Error(`${method}: event timed out after ${timeoutMs}ms`));
230
+ }, timeoutMs);
231
+ this.waiters.push(waiter);
232
+ });
233
+ }
234
+
235
+ failAll(error) {
236
+ for (const pending of this.pending.values()) {
237
+ clearTimeout(pending.timer);
238
+ pending.reject(error);
239
+ }
240
+ for (const waiter of this.waiters) {
241
+ clearTimeout(waiter.timer);
242
+ waiter.reject(error);
243
+ }
244
+ this.pending.clear();
245
+ this.waiters = [];
246
+ }
247
+ }
248
+
249
+ export function chromeVisualBrowserArgs(profileRoot, {
250
+ env = process.env,
251
+ getuid = typeof process.getuid === 'function' ? () => process.getuid() : null,
252
+ } = {}) {
253
+ const args = [
254
+ '--headless=new',
255
+ '--remote-debugging-pipe',
256
+ '--disable-gpu',
257
+ '--hide-scrollbars',
258
+ '--disable-background-networking',
259
+ '--disable-component-update',
260
+ '--disable-default-apps',
261
+ '--disable-sync',
262
+ '--metrics-recording-only',
263
+ '--no-first-run',
264
+ '--no-default-browser-check',
265
+ '--disable-background-timer-throttling',
266
+ '--disable-backgrounding-occluded-windows',
267
+ '--disable-renderer-backgrounding',
268
+ '--force-device-scale-factor=1',
269
+ `--user-data-dir=${profileRoot}`,
270
+ 'about:blank',
271
+ ];
272
+ const rootUser = typeof getuid === 'function' && getuid() === 0;
273
+ const sandboxOptOut = env?.[CHROME_NO_SANDBOX_ENV] === '1';
274
+ if (rootUser || sandboxOptOut) args.unshift('--no-sandbox');
275
+ return args;
276
+ }
277
+
278
+ async function evaluate(cdp, sessionId, expression, awaitPromise = false) {
279
+ const response = await cdp.send('Runtime.evaluate', {
280
+ expression,
281
+ awaitPromise,
282
+ returnByValue: true,
283
+ }, sessionId);
284
+ if (response.exceptionDetails) {
285
+ throw new Error(response.exceptionDetails.exception?.description
286
+ || response.exceptionDetails.text
287
+ || 'Runtime.evaluate failed');
288
+ }
289
+ return response.result?.value;
290
+ }
291
+
292
+ export class ChromeVisualBrowser {
293
+ constructor(chromePath, {
294
+ env = process.env,
295
+ getuid = typeof process.getuid === 'function' ? () => process.getuid() : null,
296
+ spawnImpl = spawn,
297
+ } = {}) {
298
+ this.profileRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-visual-check-profile-'));
299
+ this.stderr = '';
300
+ const args = chromeVisualBrowserArgs(this.profileRoot, { env, getuid });
301
+ this.child = spawnImpl(chromePath, args, { stdio: ['ignore', 'ignore', 'pipe', 'pipe', 'pipe'] });
302
+ this.child.stderr.setEncoding('utf8');
303
+ this.child.stderr.on('data', (chunk) => {
304
+ this.stderr = `${this.stderr}${chunk}`.slice(-8000);
305
+ });
306
+ this.child.stderr.on('error', (error) => {
307
+ this.stderr = `${this.stderr}\nChrome stderr stream failed: ${error.message}`.trim().slice(-8000);
308
+ });
309
+ this.cdp = new PipeCdp(this.child, {
310
+ failureDetails: () => {
311
+ const exit = this.child.signalCode
312
+ ? `signal ${this.child.signalCode}`
313
+ : this.child.exitCode == null ? 'still running' : `exit code ${this.child.exitCode}`;
314
+ const stderr = this.stderr.trim();
315
+ return [
316
+ `Chrome process: ${exit}.`,
317
+ stderr ? `Chrome stderr:\n${stderr}` : '',
318
+ ].filter(Boolean).join('\n');
319
+ },
320
+ });
321
+ this.sessionPromise = this.attach();
322
+ }
323
+
324
+ async attach() {
325
+ const targets = await this.cdp.send('Target.getTargets');
326
+ let target = targets.targetInfos?.find((item) => item.type === 'page');
327
+ if (!target) {
328
+ const created = await this.cdp.send('Target.createTarget', { url: 'about:blank' });
329
+ target = { targetId: created.targetId };
330
+ }
331
+ const attached = await this.cdp.send('Target.attachToTarget', {
332
+ targetId: target.targetId,
333
+ flatten: true,
334
+ });
335
+ await this.cdp.send('Page.enable', {}, attached.sessionId);
336
+ await this.cdp.send('Runtime.enable', {}, attached.sessionId);
337
+ return attached.sessionId;
338
+ }
339
+
340
+ async inspect({ artifactPath, width, height, theme, screenshotPath }) {
341
+ const sessionId = await this.sessionPromise;
342
+ await this.cdp.send('Emulation.setDeviceMetricsOverride', {
343
+ width,
344
+ height,
345
+ deviceScaleFactor: 1,
346
+ mobile: false,
347
+ }, sessionId);
348
+
349
+ const url = new URL(pathToFileURL(artifactPath).href);
350
+ url.searchParams.set('theme', theme);
351
+ const loaded = this.cdp.waitFor('Page.loadEventFired', sessionId);
352
+ const navigation = await this.cdp.send('Page.navigate', { url: url.href }, sessionId);
353
+ if (navigation.errorText) throw new Error(`Chrome navigation failed: ${navigation.errorText}`);
354
+ await loaded;
355
+ await evaluate(this.cdp, sessionId, `(function () {
356
+ document.documentElement.setAttribute('data-motion', 'still');
357
+ var panel = document.querySelector('.diagram-container');
358
+ if (panel) panel.setAttribute('data-detail-level', 'read');
359
+ var fontsReady = document.fonts && document.fonts.ready
360
+ ? document.fonts.ready.catch(function () {})
361
+ : Promise.resolve();
362
+ return fontsReady.then(function () {
363
+ if (window.Archify && Archify.readerLayout && typeof Archify.readerLayout.whenStable === 'function') {
364
+ return Archify.readerLayout.whenStable();
365
+ }
366
+ }).then(function () {
367
+ if (window.Archify && Archify.viewerChromeLayout && typeof Archify.viewerChromeLayout.whenStable === 'function') {
368
+ return Archify.viewerChromeLayout.whenStable();
369
+ }
370
+ }).then(function () {
371
+ if (window.Archify && Archify.readerLayout && typeof Archify.readerLayout.whenStable === 'function') {
372
+ return Archify.readerLayout.whenStable();
373
+ }
374
+ }).then(function () {
375
+ if (window.Archify && Archify.viewerChromeLayout && typeof Archify.viewerChromeLayout.whenStable === 'function') {
376
+ return Archify.viewerChromeLayout.whenStable();
377
+ }
378
+ return new Promise(function (resolve) {
379
+ requestAnimationFrame(function () { requestAnimationFrame(resolve); });
380
+ });
381
+ });
382
+ })()`, true);
383
+
384
+ const metrics = await evaluate(this.cdp, sessionId, `(function () {
385
+ var reader = document.querySelector('.container');
386
+ var diagram = document.querySelector('.diagram-container');
387
+ var svg = diagram && (
388
+ diagram.querySelector(':scope > svg') ||
389
+ diagram.querySelector(':scope > .diagram-stage > svg')
390
+ );
391
+ var stage = diagram && (diagram.querySelector(':scope > .diagram-stage') || svg);
392
+ var legend = svg && svg.querySelector('[data-legend]');
393
+ var navigationDock = diagram && diagram.querySelector('.diagram-nav');
394
+ var viewBox = svg && svg.viewBox && svg.viewBox.baseVal;
395
+ var diagramWidth = svg ? svg.getBoundingClientRect().width : 0;
396
+ var viewBoxWidth = viewBox ? viewBox.width : 0;
397
+ var scale = viewBoxWidth > 0 ? Math.min(1, diagramWidth / viewBoxWidth) : 0;
398
+ var minimum = null;
399
+ if (svg && scale > 0) {
400
+ Array.from(svg.querySelectorAll('text[data-node-label], text[data-boundary-label], text[data-detail="context"]')).forEach(function (text) {
401
+ var detail = text.hasAttribute('data-node-label')
402
+ ? 'primary'
403
+ : text.hasAttribute('data-boundary-label') ? 'boundary' : 'context';
404
+ if (detail === 'context' && !text.closest('[data-node-id]')) return;
405
+ var sourceFontPx = parseFloat(text.getAttribute('font-size') || '');
406
+ if (!Number.isFinite(sourceFontPx)) return;
407
+ var projectedFontPx = sourceFontPx * scale;
408
+ if (!minimum || projectedFontPx < minimum.projectedFontPx) {
409
+ minimum = {
410
+ text: (text.textContent || '').trim(),
411
+ detail: detail,
412
+ sourceFontPx: sourceFontPx,
413
+ projectedFontPx: projectedFontPx
414
+ };
415
+ }
416
+ });
417
+ }
418
+ function intersectionArea(a, b) {
419
+ if (!a || !b || !a.width || !a.height || !b.width || !b.height) return 0;
420
+ var width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
421
+ var height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
422
+ return width * height;
423
+ }
424
+ var legendRect = legend ? legend.getBoundingClientRect() : null;
425
+ var stageRect = window.Archify && Archify.viewerChromeLayout
426
+ && typeof Archify.viewerChromeLayout.stageRect === 'function'
427
+ ? Archify.viewerChromeLayout.stageRect()
428
+ : (stage ? stage.getBoundingClientRect() : null);
429
+ var navigationDockRect = navigationDock ? navigationDock.getBoundingClientRect() : null;
430
+ var stageDockIntersectionArea = intersectionArea(stageRect, navigationDockRect);
431
+ var viewerChromeReceipt = window.Archify && Archify.viewerChromeLayout
432
+ && typeof Archify.viewerChromeLayout.receipt === 'function'
433
+ ? Archify.viewerChromeLayout.receipt()
434
+ : null;
435
+ return {
436
+ innerWidth: window.innerWidth,
437
+ innerHeight: window.innerHeight,
438
+ scrollWidth: Math.ceil(document.documentElement.scrollWidth),
439
+ scrollHeight: Math.ceil(document.documentElement.scrollHeight),
440
+ resolvedTheme: document.documentElement.getAttribute('data-theme') || '',
441
+ readerWidth: reader ? reader.getBoundingClientRect().width : 0,
442
+ diagramWidth: diagramWidth,
443
+ viewBoxWidth: viewBoxWidth,
444
+ minimumProjectedNodeTextPx: minimum ? minimum.projectedFontPx : null,
445
+ minimumProjectedNodeText: minimum ? minimum.text : null,
446
+ minimumProjectedNodeTextDetail: minimum ? minimum.detail : null,
447
+ hasLegend: Boolean(legendRect && legendRect.width && legendRect.height),
448
+ hasNavigationDock: Boolean(navigationDockRect && navigationDockRect.width && navigationDockRect.height),
449
+ legendDockIntersectionArea: stageDockIntersectionArea > 0
450
+ ? intersectionArea(legendRect, navigationDockRect)
451
+ : 0,
452
+ dockStageIntersectionArea: stageDockIntersectionArea,
453
+ dockStageGap: stageRect && navigationDockRect ? navigationDockRect.top - stageRect.bottom : null,
454
+ viewerChromeRequiredGap: viewerChromeReceipt ? viewerChromeReceipt.gap : null,
455
+ viewerChromeReserve: viewerChromeReceipt ? viewerChromeReceipt.reserve : 0,
456
+ viewerChromeActive: viewerChromeReceipt ? viewerChromeReceipt.active : false
457
+ };
458
+ })()`);
459
+ if (!metrics || !Number.isFinite(metrics.scrollWidth) || !Number.isFinite(metrics.scrollHeight)) {
460
+ throw new Error('Chrome returned incomplete containment metrics.');
461
+ }
462
+
463
+ if (screenshotPath) {
464
+ const capture = await this.cdp.send('Page.captureScreenshot', {
465
+ format: 'png',
466
+ fromSurface: true,
467
+ captureBeyondViewport: false,
468
+ }, sessionId, 20000);
469
+ if (!capture.data) throw new Error('Chrome returned an empty screenshot.');
470
+ fs.writeFileSync(screenshotPath, Buffer.from(capture.data, 'base64'));
471
+ }
472
+ return metrics;
473
+ }
474
+
475
+ async close() {
476
+ this.cdp.failAll(new Error('visual-check finished'));
477
+ if (this.child.exitCode === null && this.child.signalCode === null) {
478
+ this.child.kill('SIGTERM');
479
+ await new Promise((resolve) => {
480
+ const timer = setTimeout(() => {
481
+ if (this.child.exitCode === null && this.child.signalCode === null) this.child.kill('SIGKILL');
482
+ resolve();
483
+ }, 1500);
484
+ this.child.once('exit', () => {
485
+ clearTimeout(timer);
486
+ resolve();
487
+ });
488
+ });
489
+ }
490
+ try {
491
+ fs.rmSync(this.profileRoot, { recursive: true, force: true });
492
+ } catch {
493
+ // Chrome may briefly retain profile files on Windows; evidence is done.
494
+ }
495
+ }
496
+ }
497
+
498
+ function observation({ width, height, theme, metrics }) {
499
+ const innerWidth = Number(metrics.innerWidth);
500
+ const innerHeight = Number(metrics.innerHeight);
501
+ const scrollWidth = Number(metrics.scrollWidth);
502
+ const scrollHeight = Number(metrics.scrollHeight);
503
+ const overflowX = scrollWidth > innerWidth;
504
+ const overflowY = scrollHeight > innerHeight;
505
+ const minimumProjectedNodeTextPx = metrics.minimumProjectedNodeTextPx == null
506
+ ? null
507
+ : Number(metrics.minimumProjectedNodeTextPx);
508
+ const readabilityOk = minimumProjectedNodeTextPx == null
509
+ || minimumProjectedNodeTextPx >= MIN_PROJECTED_NODE_TEXT_PX;
510
+ const legendDockIntersectionArea = Number(metrics.legendDockIntersectionArea) || 0;
511
+ const dockStageIntersectionArea = Number(metrics.dockStageIntersectionArea) || 0;
512
+ const dockStageGap = metrics.dockStageGap == null ? null : Number(metrics.dockStageGap);
513
+ const receiptDockStageGap = metrics.viewerChromeRequiredGap == null
514
+ ? null
515
+ : Number(metrics.viewerChromeRequiredGap);
516
+ const requiredDockStageGap = Number.isFinite(receiptDockStageGap) ? receiptDockStageGap : 0;
517
+ const viewerChromeStageOk = !metrics.hasNavigationDock || (
518
+ Number.isFinite(dockStageGap)
519
+ && dockStageIntersectionArea <= 0.5
520
+ && dockStageGap >= requiredDockStageGap - 1
521
+ );
522
+ const viewerChromeOk = legendDockIntersectionArea <= 0.5 && viewerChromeStageOk;
523
+ return {
524
+ width,
525
+ height,
526
+ theme,
527
+ innerWidth,
528
+ innerHeight,
529
+ scrollWidth,
530
+ scrollHeight,
531
+ overflowX,
532
+ overflowY,
533
+ ok: !overflowX && !overflowY,
534
+ readerWidth: Number(metrics.readerWidth) || null,
535
+ diagramWidth: Number(metrics.diagramWidth) || null,
536
+ viewBoxWidth: Number(metrics.viewBoxWidth) || null,
537
+ minimumProjectedNodeTextPx,
538
+ minimumProjectedNodeText: metrics.minimumProjectedNodeText || null,
539
+ minimumProjectedNodeTextDetail: metrics.minimumProjectedNodeTextDetail || null,
540
+ minimumRequiredNodeTextPx: MIN_PROJECTED_NODE_TEXT_PX,
541
+ readabilityOk,
542
+ hasLegend: Boolean(metrics.hasLegend),
543
+ hasNavigationDock: Boolean(metrics.hasNavigationDock),
544
+ legendDockIntersectionArea,
545
+ dockStageIntersectionArea,
546
+ dockStageGap,
547
+ requiredDockStageGap,
548
+ viewerChromeStageOk,
549
+ viewerChromeReserve: Number(metrics.viewerChromeReserve) || 0,
550
+ viewerChromeActive: Boolean(metrics.viewerChromeActive),
551
+ viewerChromeOk,
552
+ resolvedTheme: metrics.resolvedTheme || theme,
553
+ };
554
+ }
555
+
556
+ function contactSheetHtml({ artifactPath, receipt, screenshots }) {
557
+ const cards = screenshots.map((entry) => `
558
+ <figure>
559
+ <img src="${htmlEscape(entry.file)}" alt="${htmlEscape(`${entry.theme} ${entry.width} by ${entry.height}`)}">
560
+ <figcaption><strong>${htmlEscape(entry.theme.toUpperCase())}</strong> · ${entry.width}×${entry.height} · containment ${entry.ok ? 'pass' : 'fail'}</figcaption>
561
+ </figure>`).join('');
562
+ return `<!doctype html>
563
+ <html lang="en">
564
+ <head>
565
+ <meta charset="utf-8">
566
+ <meta name="viewport" content="width=device-width,initial-scale=1">
567
+ <title>Archify automated browser evidence · ${htmlEscape(path.basename(artifactPath))}</title>
568
+ <style>
569
+ *{box-sizing:border-box}body{margin:0;padding:24px;background:#e9eef5;color:#172033;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}header{max-width:1500px;margin:0 auto 18px}h1{margin:0 0 6px;font-size:20px}p{margin:0;color:#526176}.grid{max-width:1500px;margin:auto;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}figure{margin:0;padding:10px;background:white;border:1px solid #c9d4e3;border-radius:12px;box-shadow:0 10px 30px rgba(15,23,42,.08)}img{display:block;width:100%;height:auto;border:1px solid #e2e8f0}figcaption{padding:9px 4px 2px;color:#526176}@media(max-width:900px){.grid{grid-template-columns:1fr}}
570
+ </style>
571
+ </head>
572
+ <body>
573
+ <header><h1>Automated browser evidence</h1><p>${htmlEscape(path.basename(artifactPath))} · visual-check containment ${htmlEscape(receipt.containment.status)} · perceptual visual review pending</p></header>
574
+ <main class="grid">${cards}
575
+ </main>
576
+ </body>
577
+ </html>
578
+ `;
579
+ }
580
+
581
+ function viewportSubject(artifact, entry) {
582
+ return {
583
+ artifact,
584
+ viewport: { width: entry.width, height: entry.height, theme: entry.theme },
585
+ };
586
+ }
587
+
588
+ function failureDiagnostic({ code, message, subject, evidence, supportedFixes, severity = 'error' }) {
589
+ return { code, severity, message, subject, evidence, supportedFixes };
590
+ }
591
+
592
+ function observationDiagnostics({ artifact, allObservations, readabilityObservations }) {
593
+ const diagnostics = [];
594
+ for (const entry of allObservations) {
595
+ if (!entry.ok) {
596
+ diagnostics.push(failureDiagnostic({
597
+ code: 'viewer/viewport-overflow',
598
+ message: `The rendered artifact overflows the ${entry.width}x${entry.height} ${entry.theme} viewport.`,
599
+ subject: viewportSubject(artifact, entry),
600
+ evidence: {
601
+ innerWidth: entry.innerWidth,
602
+ innerHeight: entry.innerHeight,
603
+ scrollWidth: entry.scrollWidth,
604
+ scrollHeight: entry.scrollHeight,
605
+ overflowX: entry.overflowX,
606
+ overflowY: entry.overflowY,
607
+ },
608
+ supportedFixes: [
609
+ `contain the rendered layout within ${entry.width}x${entry.height}, then rerun visual-check`,
610
+ ],
611
+ }));
612
+ }
613
+ if (entry.legendDockIntersectionArea > 0.5) {
614
+ diagnostics.push(failureDiagnostic({
615
+ code: 'viewer/chrome-legend-clearance',
616
+ message: `The navigation Dock obscures the SVG Legend at ${entry.width}x${entry.height} (${entry.theme}).`,
617
+ subject: viewportSubject(artifact, entry),
618
+ evidence: { legendDockIntersectionArea: entry.legendDockIntersectionArea },
619
+ supportedFixes: [
620
+ 'move the SVG Legend or Viewer Dock until legendDockIntersectionArea is 0, then rerun visual-check',
621
+ ],
622
+ }));
623
+ }
624
+ if (!entry.viewerChromeStageOk) {
625
+ const stageOverlapsDock = entry.dockStageIntersectionArea > 0.5;
626
+ diagnostics.push(failureDiagnostic({
627
+ code: 'viewer/chrome-stage-clearance',
628
+ message: stageOverlapsDock
629
+ ? `Navigation Dock enters the protected SVG stage at ${entry.width}x${entry.height} (${entry.theme}).`
630
+ : `Navigation Dock clearance from the protected SVG stage is below the required gap at ${entry.width}x${entry.height} (${entry.theme}).`,
631
+ subject: viewportSubject(artifact, entry),
632
+ evidence: {
633
+ dockStageIntersectionArea: entry.dockStageIntersectionArea,
634
+ dockStageGap: entry.dockStageGap,
635
+ requiredDockStageGap: entry.requiredDockStageGap,
636
+ },
637
+ supportedFixes: [
638
+ `adjust Viewer stage reservation or clipping until dockStageGap is at least ${entry.requiredDockStageGap} and dockStageIntersectionArea is 0, then rerun visual-check`,
639
+ ],
640
+ }));
641
+ }
642
+ }
643
+ for (const entry of readabilityObservations) {
644
+ if (entry.readabilityOk) continue;
645
+ diagnostics.push(failureDiagnostic({
646
+ code: 'viewer/projected-text-readability',
647
+ message: `Projected ${entry.minimumProjectedNodeTextDetail || 'node'} text is below the readability floor at ${entry.width}x${entry.height}.`,
648
+ subject: viewportSubject(artifact, entry),
649
+ evidence: {
650
+ text: entry.minimumProjectedNodeText,
651
+ detail: entry.minimumProjectedNodeTextDetail,
652
+ minimumProjectedNodeTextPx: entry.minimumProjectedNodeTextPx,
653
+ minimumRequiredNodeTextPx: entry.minimumRequiredNodeTextPx,
654
+ },
655
+ supportedFixes: [
656
+ `increase projected node text to at least ${entry.minimumRequiredNodeTextPx}px at ${entry.width}x${entry.height}, then rerun visual-check`,
657
+ ],
658
+ }));
659
+ }
660
+ return diagnostics;
661
+ }
662
+
663
+ function baseReceipt({ artifactPath, artifact, outputs, chrome }) {
664
+ return {
665
+ schemaVersion: 1,
666
+ ok: false,
667
+ command: 'visual-check',
668
+ evidenceKind: 'automated-browser',
669
+ status: 'fail',
670
+ visualReview: 'pending',
671
+ artifact: {
672
+ path: artifactPath,
673
+ sha256: sha256(artifact),
674
+ bytes: artifact.byteLength,
675
+ },
676
+ state: { detail: 'read', motion: 'still' },
677
+ chrome,
678
+ diagnostics: [],
679
+ containment: { status: 'fail', viewports: [] },
680
+ readability: { status: 'fail', minimumProjectedNodeTextPx: MIN_PROJECTED_NODE_TEXT_PX, viewports: [] },
681
+ viewerChrome: { status: 'fail', viewports: [] },
682
+ captures: { status: 'fail', screenshots: [], contactSheet: null },
683
+ sidecars: {
684
+ receipt: path.basename(outputs.receipt),
685
+ contactSheet: path.basename(outputs.contactSheet),
686
+ },
687
+ };
688
+ }
689
+
690
+ function persistReceipt(outputs, receipt) {
691
+ writeAtomic(outputs.receipt, `${JSON.stringify(receipt, null, 2)}\n`);
692
+ }
693
+
694
+ export async function runVisualCheck({
695
+ artifactPath,
696
+ chromePath,
697
+ resolveChrome = findChrome,
698
+ browserFactory = async (resolvedChrome) => new ChromeVisualBrowser(resolvedChrome),
699
+ } = {}) {
700
+ if (!artifactPath) throw new Error('visual-check requires one delivered HTML artifact.');
701
+ const artifact = path.resolve(artifactPath);
702
+ if (!/\.html?$/i.test(artifact)) throw new Error('visual-check requires an .html artifact.');
703
+ const artifactBytes = fs.readFileSync(artifact);
704
+ const outputs = sidecarPaths(artifact);
705
+ cleanupCaptureSidecars(outputs);
706
+ safeUnlink(outputs.receipt);
707
+
708
+ const resolvedChrome = chromePath || resolveChrome();
709
+ const receipt = baseReceipt({
710
+ artifactPath: artifact,
711
+ artifact: artifactBytes,
712
+ outputs,
713
+ chrome: resolvedChrome
714
+ ? { status: 'available', executable: resolvedChrome }
715
+ : { status: 'unavailable', executable: null },
716
+ });
717
+
718
+ if (!resolvedChrome) {
719
+ receipt.status = 'skipped';
720
+ receipt.containment.status = 'skipped';
721
+ receipt.readability.status = 'skipped';
722
+ receipt.viewerChrome.status = 'skipped';
723
+ receipt.captures.status = 'skipped';
724
+ receipt.error = 'Chrome or Chromium is unavailable. Set ARCHIFY_CHROME to its executable path.';
725
+ receipt.diagnostics = [failureDiagnostic({
726
+ code: 'viewer/chrome-unavailable',
727
+ severity: 'warning',
728
+ message: receipt.error,
729
+ subject: { artifact },
730
+ evidence: { executable: null },
731
+ supportedFixes: ['set ARCHIFY_CHROME to a Chrome or Chromium executable and rerun visual-check'],
732
+ })];
733
+ persistReceipt(outputs, receipt);
734
+ return { exitCode: EXIT.skipped, receipt };
735
+ }
736
+
737
+ let browser;
738
+ try {
739
+ browser = await browserFactory(resolvedChrome);
740
+ const observations = new Map();
741
+ const screenshotsByKey = new Map(outputs.screenshots.map((entry) => [
742
+ screenshotKey(entry.width, entry.height, entry.theme),
743
+ entry,
744
+ ]));
745
+
746
+ for (const viewport of VISUAL_CHECK_VIEWPORTS) {
747
+ const key = screenshotKey(viewport.width, viewport.height, 'light');
748
+ const screenshot = screenshotsByKey.get(key);
749
+ const metrics = await browser.inspect({
750
+ artifactPath: artifact,
751
+ ...viewport,
752
+ theme: 'light',
753
+ ...(screenshot ? { screenshotPath: screenshot.path } : {}),
754
+ });
755
+ observations.set(key, observation({ ...viewport, theme: 'light', metrics }));
756
+ }
757
+ for (const viewport of CAPTURE_VIEWPORTS) {
758
+ const key = screenshotKey(viewport.width, viewport.height, 'dark');
759
+ const screenshot = screenshotsByKey.get(key);
760
+ const metrics = await browser.inspect({
761
+ artifactPath: artifact,
762
+ ...viewport,
763
+ theme: 'dark',
764
+ screenshotPath: screenshot.path,
765
+ });
766
+ observations.set(key, observation({ ...viewport, theme: 'dark', metrics }));
767
+ }
768
+
769
+ const afterBytes = fs.readFileSync(artifact);
770
+ if (sha256(afterBytes) !== receipt.artifact.sha256 || afterBytes.byteLength !== receipt.artifact.bytes) {
771
+ throw new Error('The delivered artifact changed while visual-check was running.');
772
+ }
773
+
774
+ receipt.containment.viewports = VISUAL_CHECK_VIEWPORTS.map(({ width, height }) => (
775
+ observations.get(screenshotKey(width, height, 'light'))
776
+ ));
777
+ receipt.readability.viewports = receipt.containment.viewports.map((entry) => ({ ...entry }));
778
+ receipt.viewerChrome.viewports = receipt.containment.viewports.map((entry) => ({ ...entry }));
779
+ receipt.captures.screenshots = outputs.screenshots.map((entry) => ({
780
+ ...observations.get(screenshotKey(entry.width, entry.height, entry.theme)),
781
+ file: path.basename(entry.path),
782
+ }));
783
+ const allObservations = [...observations.values()];
784
+ const containmentPass = allObservations.every((entry) => entry.ok);
785
+ const readabilityPass = receipt.readability.viewports.every((entry) => entry.readabilityOk);
786
+ const viewerChromePass = allObservations.every((entry) => entry.viewerChromeOk);
787
+ receipt.diagnostics = observationDiagnostics({
788
+ artifact,
789
+ allObservations,
790
+ readabilityObservations: receipt.readability.viewports,
791
+ });
792
+ receipt.containment.status = containmentPass ? 'pass' : 'fail';
793
+ receipt.readability.status = readabilityPass ? 'pass' : 'fail';
794
+ receipt.viewerChrome.status = viewerChromePass ? 'pass' : 'fail';
795
+ receipt.captures.status = 'pass';
796
+ receipt.captures.contactSheet = path.basename(outputs.contactSheet);
797
+ receipt.status = containmentPass && readabilityPass && viewerChromePass ? 'pass' : 'fail';
798
+ receipt.ok = containmentPass && readabilityPass && viewerChromePass;
799
+ writeAtomic(outputs.contactSheet, contactSheetHtml({
800
+ artifactPath: artifact,
801
+ receipt,
802
+ screenshots: receipt.captures.screenshots,
803
+ }));
804
+ persistReceipt(outputs, receipt);
805
+ return { exitCode: receipt.ok ? EXIT.pass : EXIT.fail, receipt };
806
+ } catch (error) {
807
+ cleanupCaptureSidecars(outputs);
808
+ receipt.status = 'fail';
809
+ receipt.ok = false;
810
+ receipt.error = error.message;
811
+ receipt.containment.status = 'fail';
812
+ receipt.readability.status = 'fail';
813
+ receipt.viewerChrome.status = 'fail';
814
+ receipt.captures.status = 'fail';
815
+ receipt.captures.screenshots = [];
816
+ receipt.captures.contactSheet = null;
817
+ receipt.diagnostics = [failureDiagnostic({
818
+ code: 'viewer/visual-check-runtime',
819
+ message: 'visual-check could not complete its Chrome inspection.',
820
+ subject: { artifact },
821
+ evidence: { reason: error.message },
822
+ supportedFixes: ['resolve the reported Chrome inspection error, then rerun visual-check'],
823
+ })];
824
+ persistReceipt(outputs, receipt);
825
+ return { exitCode: EXIT.fail, receipt };
826
+ } finally {
827
+ if (browser?.close) await browser.close();
828
+ }
829
+ }