@hybridlabor-api/aos 4.6.6 → 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 (484) 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-uninstall.mjs +55 -1
  15. package/docs/handover/agenttrail-for-ao.md +58 -0
  16. package/docs/handover/deja-for-ao.md +51 -0
  17. package/installer.js +141 -37
  18. package/mcp_config.json +4 -0
  19. package/mcps/mcsc/README.md +24 -0
  20. package/mcps/mcsc/package-lock.json +1234 -0
  21. package/mcps/mcsc/package.json +14 -0
  22. package/mcps/mcsc/packages/core/bin/mcsc.js +162 -0
  23. package/mcps/mcsc/packages/core/rulebook.default.yaml +49 -0
  24. package/mcps/mcsc/packages/core/schema/rulebook.schema.json +56 -0
  25. package/mcps/mcsc/packages/core/src/adapters/agy.js +107 -0
  26. package/mcps/mcsc/packages/core/src/adapters/codex.js +56 -0
  27. package/mcps/mcsc/packages/core/src/adapters/native.js +14 -0
  28. package/mcps/mcsc/packages/core/src/adapters/opencode.js +183 -0
  29. package/mcps/mcsc/packages/core/src/adapters/types.js +21 -0
  30. package/mcps/mcsc/packages/core/src/capabilities/buildOfferTable.js +34 -0
  31. package/mcps/mcsc/packages/core/src/capabilities/registry.js +99 -0
  32. package/mcps/mcsc/packages/core/src/capabilities/types.js +24 -0
  33. package/mcps/mcsc/packages/core/src/detect/cache.js +47 -0
  34. package/mcps/mcsc/packages/core/src/detect/inventory.js +73 -0
  35. package/mcps/mcsc/packages/core/src/detect/probes.js +158 -0
  36. package/mcps/mcsc/packages/core/src/detect/types.js +26 -0
  37. package/mcps/mcsc/packages/core/src/rulebook/load.js +53 -0
  38. package/mcps/mcsc/packages/core/src/rulebook/resolve.js +227 -0
  39. package/mcps/mcsc/packages/core/src/rulebook/rulebook.types.js +89 -0
  40. package/mcps/mcsc/packages/core/src/trail.js +17 -0
  41. package/mcps/mcsc/packages/mcp/server.js +302 -0
  42. package/mcps/mcsc/server.js +2 -0
  43. package/package.json +3 -1
  44. package/skills/basic/bdbmediastorm/SKILL.md +1 -1
  45. package/skills/basic/godmode-ui-ux/SKILL.md +2 -2
  46. package/skills/basic/godmode-ui-ux/references/anti-slop-rules.md +44 -40
  47. package/skills/basic/godmode-ui-ux/references/taste-archetypes.md +49 -0
  48. package/skills/basic/startcycle/SKILL.md +7 -0
  49. package/skills/basic/startcycle-graph/SKILL.md +12 -0
  50. package/skills/basic/startcycle-graph-user/SKILL.md +3 -0
  51. package/skills/basic/teamwork-preview/SKILL.md +2 -0
  52. package/skills/bdbrainstorm/SKILL.md +1 -0
  53. package/skills/global_config/agenttrail/LICENSE +21 -0
  54. package/skills/global_config/agenttrail/SKILL.md +48 -0
  55. package/skills/global_config/agenttrail/bin/agenttrail.mjs +1002 -0
  56. package/skills/global_config/agenttrail/public/index.html +661 -0
  57. package/skills/global_config/aos-project-init/SKILL.md +25 -5
  58. package/skills/global_config/aos-setup/SKILL.md +34 -1
  59. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +115 -1
  60. package/skills/global_config/archify/LICENSE +22 -0
  61. package/skills/global_config/archify/SKILL.md +140 -0
  62. package/skills/global_config/archify/THIRD_PARTY_NOTICES.md +69 -0
  63. package/skills/global_config/archify/assets/JetBrainsMono-OFL.txt +93 -0
  64. package/skills/global_config/archify/assets/template.html +14937 -0
  65. package/skills/global_config/archify/bin/archify.mjs +2139 -0
  66. package/skills/global_config/archify/bin/open-artifact.mjs +86 -0
  67. package/skills/global_config/archify/bin/preview.mjs +653 -0
  68. package/skills/global_config/archify/bin/visual-check.mjs +829 -0
  69. package/skills/global_config/archify/brand-marks/README.md +31 -0
  70. package/skills/global_config/archify/brand-marks/catalog.json +131 -0
  71. package/skills/global_config/archify/delta/architecture-delta.mjs +1232 -0
  72. package/skills/global_config/archify/examples/agent-run.lifecycle.json +60 -0
  73. package/skills/global_config/archify/examples/agent-tool-call.workflow.json +94 -0
  74. package/skills/global_config/archify/examples/async-job-roundtrip.sequence.json +61 -0
  75. package/skills/global_config/archify/examples/brand-aware-delivery.architecture.json +47 -0
  76. package/skills/global_config/archify/examples/cache-miss-request.sequence.json +82 -0
  77. package/skills/global_config/archify/examples/checkout-platform.base.architecture.json +31 -0
  78. package/skills/global_config/archify/examples/checkout-platform.head.architecture.json +31 -0
  79. package/skills/global_config/archify/examples/deployment-release.lifecycle.json +49 -0
  80. package/skills/global_config/archify/examples/event-stream.dataflow.json +57 -0
  81. package/skills/global_config/archify/examples/incident-response.workflow.json +64 -0
  82. package/skills/global_config/archify/examples/product-analytics.dataflow.json +76 -0
  83. package/skills/global_config/archify/examples/production-deployment.architecture.json +71 -0
  84. package/skills/global_config/archify/examples/release-delivery.workflow.json +62 -0
  85. package/skills/global_config/archify/examples/web-app.architecture.json +46 -0
  86. package/skills/global_config/archify/migrations/workflow-v2.mjs +279 -0
  87. package/skills/global_config/archify/package.json +39 -0
  88. package/skills/global_config/archify/recipes/scenarios.mjs +391 -0
  89. package/skills/global_config/archify/references/authoring-contract.md +247 -0
  90. package/skills/global_config/archify/references/brand-marks.md +65 -0
  91. package/skills/global_config/archify/references/delivery-contract.md +138 -0
  92. package/skills/global_config/archify/references/viewer-runtime.md +45 -0
  93. package/skills/global_config/archify/renderers/architecture/grid.mjs +62 -0
  94. package/skills/global_config/archify/renderers/architecture/render-architecture.mjs +1078 -0
  95. package/skills/global_config/archify/renderers/dataflow/README.md +104 -0
  96. package/skills/global_config/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  97. package/skills/global_config/archify/renderers/lifecycle/README.md +115 -0
  98. package/skills/global_config/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  99. package/skills/global_config/archify/renderers/sequence/README.md +114 -0
  100. package/skills/global_config/archify/renderers/sequence/render-sequence.mjs +464 -0
  101. package/skills/global_config/archify/renderers/shared/brand-marks.mjs +563 -0
  102. package/skills/global_config/archify/renderers/shared/cli.mjs +218 -0
  103. package/skills/global_config/archify/renderers/shared/desktop-readability.mjs +26 -0
  104. package/skills/global_config/archify/renderers/shared/diagnostics.mjs +160 -0
  105. package/skills/global_config/archify/renderers/shared/engineering-profiles.mjs +157 -0
  106. package/skills/global_config/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  107. package/skills/global_config/archify/renderers/shared/generated-validators.mjs +13 -0
  108. package/skills/global_config/archify/renderers/shared/geometry.mjs +1423 -0
  109. package/skills/global_config/archify/renderers/shared/i18n.mjs +595 -0
  110. package/skills/global_config/archify/renderers/shared/layout-report.mjs +40 -0
  111. package/skills/global_config/archify/renderers/shared/legend.mjs +217 -0
  112. package/skills/global_config/archify/renderers/shared/output-path.mjs +340 -0
  113. package/skills/global_config/archify/renderers/shared/repository-evidence.mjs +239 -0
  114. package/skills/global_config/archify/renderers/shared/repository-location.mjs +58 -0
  115. package/skills/global_config/archify/renderers/shared/text-fit.mjs +49 -0
  116. package/skills/global_config/archify/renderers/shared/utils.mjs +232 -0
  117. package/skills/global_config/archify/renderers/shared/validator.mjs +86 -0
  118. package/skills/global_config/archify/renderers/workflow/README.md +223 -0
  119. package/skills/global_config/archify/renderers/workflow/render-workflow.mjs +35 -0
  120. package/skills/global_config/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  121. package/skills/global_config/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  122. package/skills/global_config/archify/schemas/README.md +211 -0
  123. package/skills/global_config/archify/schemas/architecture.schema.json +178 -0
  124. package/skills/global_config/archify/schemas/common.schema.json +115 -0
  125. package/skills/global_config/archify/schemas/dataflow.schema.json +243 -0
  126. package/skills/global_config/archify/schemas/lifecycle.schema.json +266 -0
  127. package/skills/global_config/archify/schemas/sequence.schema.json +223 -0
  128. package/skills/global_config/archify/schemas/workflow.schema.json +428 -0
  129. package/skills/global_config/archify/scripts/check-render-output.mjs +916 -0
  130. package/skills/global_config/archify/scripts/generate-brand-marks.mjs +141 -0
  131. package/skills/global_config/archify/scripts/generate-validators.mjs +66 -0
  132. package/skills/global_config/archify/scripts/render-examples.mjs +26 -0
  133. package/skills/global_config/archify/skill-release.json +10 -0
  134. package/skills/global_config/bdb-security-audit/SKILL.md +109 -0
  135. package/skills/global_config/bdb-security-audit/references/audit-checklists.md +172 -0
  136. package/skills/global_config/bdb-security-audit/references/skylos-guide.md +139 -0
  137. package/skills/global_config/bdb-security-audit/references/xalgorix-bridge.md +110 -0
  138. package/skills/global_config/bdbdesignpro/SKILL.md +4 -4
  139. package/skills/global_config/bdbdesignpro/references/bdb-motion-tokens.md +52 -31
  140. package/skills/global_config/bdbresilience/SKILL.md +10 -0
  141. package/skills/global_config/brag/SKILL.md +179 -0
  142. package/skills/global_config/brag/assets/music/README.md +20 -0
  143. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.json +3846 -0
  144. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.md +35 -0
  145. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.json +1826 -0
  146. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.md +35 -0
  147. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.json +2266 -0
  148. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.md +35 -0
  149. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.json +2806 -0
  150. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.md +35 -0
  151. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.json +2796 -0
  152. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.md +35 -0
  153. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-1-by-ende-dot-app.mp3 +0 -0
  154. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-10-by-ende-dot-app.mp3 +0 -0
  155. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-11-by-ende-dot-app.mp3 +0 -0
  156. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-12-by-ende-dot-app.mp3 +0 -0
  157. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-9-by-ende-dot-app.mp3 +0 -0
  158. package/skills/global_config/brag/assets/sfx/casino/card-fan-1.ogg +0 -0
  159. package/skills/global_config/brag/assets/sfx/casino/card-fan-2.ogg +0 -0
  160. package/skills/global_config/brag/assets/sfx/casino/card-place-1.ogg +0 -0
  161. package/skills/global_config/brag/assets/sfx/casino/card-place-2.ogg +0 -0
  162. package/skills/global_config/brag/assets/sfx/casino/card-place-3.ogg +0 -0
  163. package/skills/global_config/brag/assets/sfx/casino/card-place-4.ogg +0 -0
  164. package/skills/global_config/brag/assets/sfx/casino/card-shove-1.ogg +0 -0
  165. package/skills/global_config/brag/assets/sfx/casino/card-shove-2.ogg +0 -0
  166. package/skills/global_config/brag/assets/sfx/casino/card-shove-3.ogg +0 -0
  167. package/skills/global_config/brag/assets/sfx/casino/card-shove-4.ogg +0 -0
  168. package/skills/global_config/brag/assets/sfx/casino/card-shuffle.ogg +0 -0
  169. package/skills/global_config/brag/assets/sfx/casino/card-slide-1.ogg +0 -0
  170. package/skills/global_config/brag/assets/sfx/casino/card-slide-2.ogg +0 -0
  171. package/skills/global_config/brag/assets/sfx/casino/card-slide-3.ogg +0 -0
  172. package/skills/global_config/brag/assets/sfx/casino/card-slide-4.ogg +0 -0
  173. package/skills/global_config/brag/assets/sfx/casino/card-slide-5.ogg +0 -0
  174. package/skills/global_config/brag/assets/sfx/casino/card-slide-6.ogg +0 -0
  175. package/skills/global_config/brag/assets/sfx/casino/card-slide-7.ogg +0 -0
  176. package/skills/global_config/brag/assets/sfx/casino/card-slide-8.ogg +0 -0
  177. package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-1.ogg +0 -0
  178. package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-2.ogg +0 -0
  179. package/skills/global_config/brag/assets/sfx/casino/chip-lay-1.ogg +0 -0
  180. package/skills/global_config/brag/assets/sfx/casino/chip-lay-2.ogg +0 -0
  181. package/skills/global_config/brag/assets/sfx/casino/chip-lay-3.ogg +0 -0
  182. package/skills/global_config/brag/assets/sfx/casino/chips-collide-1.ogg +0 -0
  183. package/skills/global_config/brag/assets/sfx/casino/chips-collide-2.ogg +0 -0
  184. package/skills/global_config/brag/assets/sfx/casino/chips-collide-3.ogg +0 -0
  185. package/skills/global_config/brag/assets/sfx/casino/chips-collide-4.ogg +0 -0
  186. package/skills/global_config/brag/assets/sfx/casino/chips-handle-1.ogg +0 -0
  187. package/skills/global_config/brag/assets/sfx/casino/chips-handle-2.ogg +0 -0
  188. package/skills/global_config/brag/assets/sfx/casino/chips-handle-3.ogg +0 -0
  189. package/skills/global_config/brag/assets/sfx/casino/chips-handle-4.ogg +0 -0
  190. package/skills/global_config/brag/assets/sfx/casino/chips-handle-6.ogg +0 -0
  191. package/skills/global_config/brag/assets/sfx/casino/chips-stack-1.ogg +0 -0
  192. package/skills/global_config/brag/assets/sfx/casino/chips-stack-2.ogg +0 -0
  193. package/skills/global_config/brag/assets/sfx/casino/chips-stack-3.ogg +0 -0
  194. package/skills/global_config/brag/assets/sfx/casino/chips-stack-4.ogg +0 -0
  195. package/skills/global_config/brag/assets/sfx/casino/chips-stack-5.ogg +0 -0
  196. package/skills/global_config/brag/assets/sfx/casino/chips-stack-6.ogg +0 -0
  197. package/skills/global_config/brag/assets/sfx/casino/dice-grab-1.ogg +0 -0
  198. package/skills/global_config/brag/assets/sfx/casino/dice-grab-2.ogg +0 -0
  199. package/skills/global_config/brag/assets/sfx/casino/dice-shake-1.ogg +0 -0
  200. package/skills/global_config/brag/assets/sfx/casino/dice-shake-2.ogg +0 -0
  201. package/skills/global_config/brag/assets/sfx/casino/dice-shake-3.ogg +0 -0
  202. package/skills/global_config/brag/assets/sfx/casino/dice-throw-1.ogg +0 -0
  203. package/skills/global_config/brag/assets/sfx/casino/dice-throw-2.ogg +0 -0
  204. package/skills/global_config/brag/assets/sfx/casino/dice-throw-3.ogg +0 -0
  205. package/skills/global_config/brag/assets/sfx/casino/die-throw-1.ogg +0 -0
  206. package/skills/global_config/brag/assets/sfx/casino/die-throw-2.ogg +0 -0
  207. package/skills/global_config/brag/assets/sfx/casino/die-throw-3.ogg +0 -0
  208. package/skills/global_config/brag/assets/sfx/casino/die-throw-4.ogg +0 -0
  209. package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_000.ogg +0 -0
  210. package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_003.ogg +0 -0
  211. package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_004.ogg +0 -0
  212. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_000.ogg +0 -0
  213. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_001.ogg +0 -0
  214. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_002.ogg +0 -0
  215. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_003.ogg +0 -0
  216. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_004.ogg +0 -0
  217. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_000.ogg +0 -0
  218. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_001.ogg +0 -0
  219. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_002.ogg +0 -0
  220. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_003.ogg +0 -0
  221. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_004.ogg +0 -0
  222. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_000.ogg +0 -0
  223. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_001.ogg +0 -0
  224. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_002.ogg +0 -0
  225. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_003.ogg +0 -0
  226. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_004.ogg +0 -0
  227. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_000.ogg +0 -0
  228. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_001.ogg +0 -0
  229. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_002.ogg +0 -0
  230. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_003.ogg +0 -0
  231. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_004.ogg +0 -0
  232. package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_000.ogg +0 -0
  233. package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_003.ogg +0 -0
  234. package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_004.ogg +0 -0
  235. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_000.ogg +0 -0
  236. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_001.ogg +0 -0
  237. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_002.ogg +0 -0
  238. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_003.ogg +0 -0
  239. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_004.ogg +0 -0
  240. package/skills/global_config/brag/assets/sfx/impact/impactGlass_heavy_002.ogg +0 -0
  241. package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_001.ogg +0 -0
  242. package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_002.ogg +0 -0
  243. package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_003.ogg +0 -0
  244. package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_000.ogg +0 -0
  245. package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_002.ogg +0 -0
  246. package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_004.ogg +0 -0
  247. package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_000.ogg +0 -0
  248. package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_002.ogg +0 -0
  249. package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_004.ogg +0 -0
  250. package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_002.ogg +0 -0
  251. package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_003.ogg +0 -0
  252. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_000.ogg +0 -0
  253. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_001.ogg +0 -0
  254. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_002.ogg +0 -0
  255. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_003.ogg +0 -0
  256. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_004.ogg +0 -0
  257. package/skills/global_config/brag/assets/sfx/impact/impactMining_001.ogg +0 -0
  258. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_000.ogg +0 -0
  259. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_001.ogg +0 -0
  260. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_002.ogg +0 -0
  261. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_003.ogg +0 -0
  262. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_004.ogg +0 -0
  263. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_000.ogg +0 -0
  264. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_001.ogg +0 -0
  265. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_002.ogg +0 -0
  266. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_003.ogg +0 -0
  267. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_004.ogg +0 -0
  268. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_000.ogg +0 -0
  269. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_001.ogg +0 -0
  270. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_002.ogg +0 -0
  271. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_003.ogg +0 -0
  272. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_004.ogg +0 -0
  273. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_000.ogg +0 -0
  274. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_001.ogg +0 -0
  275. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_002.ogg +0 -0
  276. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_003.ogg +0 -0
  277. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_004.ogg +0 -0
  278. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_000.ogg +0 -0
  279. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_001.ogg +0 -0
  280. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_002.ogg +0 -0
  281. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_003.ogg +0 -0
  282. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_004.ogg +0 -0
  283. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_000.ogg +0 -0
  284. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_001.ogg +0 -0
  285. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_002.ogg +0 -0
  286. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_003.ogg +0 -0
  287. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_004.ogg +0 -0
  288. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_000.ogg +0 -0
  289. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_001.ogg +0 -0
  290. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_002.ogg +0 -0
  291. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_003.ogg +0 -0
  292. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_004.ogg +0 -0
  293. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_000.ogg +0 -0
  294. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_001.ogg +0 -0
  295. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_002.ogg +0 -0
  296. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_003.ogg +0 -0
  297. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_004.ogg +0 -0
  298. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_000.ogg +0 -0
  299. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_001.ogg +0 -0
  300. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_002.ogg +0 -0
  301. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_003.ogg +0 -0
  302. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_004.ogg +0 -0
  303. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_000.ogg +0 -0
  304. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_001.ogg +0 -0
  305. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_002.ogg +0 -0
  306. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_003.ogg +0 -0
  307. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_004.ogg +0 -0
  308. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_000.ogg +0 -0
  309. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_001.ogg +0 -0
  310. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_002.ogg +0 -0
  311. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_003.ogg +0 -0
  312. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_004.ogg +0 -0
  313. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_000.ogg +0 -0
  314. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_001.ogg +0 -0
  315. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_002.ogg +0 -0
  316. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_003.ogg +0 -0
  317. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_004.ogg +0 -0
  318. package/skills/global_config/brag/assets/sfx/interface/bong_001.ogg +0 -0
  319. package/skills/global_config/brag/assets/sfx/interface/click_001.ogg +0 -0
  320. package/skills/global_config/brag/assets/sfx/interface/click_002.ogg +0 -0
  321. package/skills/global_config/brag/assets/sfx/interface/click_003.ogg +0 -0
  322. package/skills/global_config/brag/assets/sfx/interface/click_004.ogg +0 -0
  323. package/skills/global_config/brag/assets/sfx/interface/click_005.ogg +0 -0
  324. package/skills/global_config/brag/assets/sfx/interface/drop_001.ogg +0 -0
  325. package/skills/global_config/brag/assets/sfx/interface/drop_002.ogg +0 -0
  326. package/skills/global_config/brag/assets/sfx/interface/drop_003.ogg +0 -0
  327. package/skills/global_config/brag/assets/sfx/interface/error_005.ogg +0 -0
  328. package/skills/global_config/brag/assets/sfx/interface/error_006.ogg +0 -0
  329. package/skills/global_config/brag/assets/sfx/interface/glitch_002.ogg +0 -0
  330. package/skills/global_config/brag/assets/sfx/interface/glitch_004.ogg +0 -0
  331. package/skills/global_config/brag/assets/sfx/interface/select_008.ogg +0 -0
  332. package/skills/global_config/brag/assets/sfx/interface/switch_001.ogg +0 -0
  333. package/skills/global_config/brag/assets/sfx/interface/switch_002.ogg +0 -0
  334. package/skills/global_config/brag/assets/sfx/interface/switch_004.ogg +0 -0
  335. package/skills/global_config/brag/assets/sfx/interface/switch_005.ogg +0 -0
  336. package/skills/global_config/brag/assets/sfx/interface/switch_006.ogg +0 -0
  337. package/skills/global_config/brag/assets/sfx/interface/switch_007.ogg +0 -0
  338. package/skills/global_config/brag/assets/sfx/keyboard/keypress-001.wav +0 -0
  339. package/skills/global_config/brag/assets/sfx/keyboard/keypress-002.wav +0 -0
  340. package/skills/global_config/brag/assets/sfx/keyboard/keypress-003.wav +0 -0
  341. package/skills/global_config/brag/assets/sfx/keyboard/keypress-004.wav +0 -0
  342. package/skills/global_config/brag/assets/sfx/keyboard/keypress-005.wav +0 -0
  343. package/skills/global_config/brag/assets/sfx/keyboard/keypress-006.wav +0 -0
  344. package/skills/global_config/brag/assets/sfx/keyboard/keypress-007.wav +0 -0
  345. package/skills/global_config/brag/assets/sfx/keyboard/keypress-008.wav +0 -0
  346. package/skills/global_config/brag/assets/sfx/keyboard/keypress-009.wav +0 -0
  347. package/skills/global_config/brag/assets/sfx/keyboard/keypress-010.wav +0 -0
  348. package/skills/global_config/brag/assets/sfx/keyboard/keypress-011.wav +0 -0
  349. package/skills/global_config/brag/assets/sfx/keyboard/keypress-012.wav +0 -0
  350. package/skills/global_config/brag/assets/sfx/keyboard/keypress-013.wav +0 -0
  351. package/skills/global_config/brag/assets/sfx/keyboard/keypress-014.wav +0 -0
  352. package/skills/global_config/brag/assets/sfx/keyboard/keypress-015.wav +0 -0
  353. package/skills/global_config/brag/assets/sfx/keyboard/keypress-016.wav +0 -0
  354. package/skills/global_config/brag/assets/sfx/keyboard/keypress-017.wav +0 -0
  355. package/skills/global_config/brag/assets/sfx/keyboard/keypress-018.wav +0 -0
  356. package/skills/global_config/brag/assets/sfx/keyboard/keypress-019.wav +0 -0
  357. package/skills/global_config/brag/assets/sfx/keyboard/keypress-020.wav +0 -0
  358. package/skills/global_config/brag/assets/sfx/keyboard/keypress-021.wav +0 -0
  359. package/skills/global_config/brag/assets/sfx/keyboard/keypress-022.wav +0 -0
  360. package/skills/global_config/brag/assets/sfx/keyboard/keypress-023.wav +0 -0
  361. package/skills/global_config/brag/assets/sfx/keyboard/keypress-024.wav +0 -0
  362. package/skills/global_config/brag/assets/sfx/keyboard/keypress-025.wav +0 -0
  363. package/skills/global_config/brag/assets/sfx/keyboard/keypress-026.wav +0 -0
  364. package/skills/global_config/brag/assets/sfx/keyboard/keypress-027.wav +0 -0
  365. package/skills/global_config/brag/assets/sfx/keyboard/keypress-028.wav +0 -0
  366. package/skills/global_config/brag/assets/sfx/keyboard/keypress-029.wav +0 -0
  367. package/skills/global_config/brag/assets/sfx/keyboard/keypress-030.wav +0 -0
  368. package/skills/global_config/brag/assets/sfx/keyboard/keypress-031.wav +0 -0
  369. package/skills/global_config/brag/assets/sfx/keyboard/keypress-032.wav +0 -0
  370. package/skills/global_config/brag/assets/sfx/sfx-analysis.json +15584 -0
  371. package/skills/global_config/brag/assets/sfx/sfx-analysis.md +75 -0
  372. package/skills/global_config/brag/assets/sfx/ui/click1.ogg +0 -0
  373. package/skills/global_config/brag/assets/sfx/ui/click2.ogg +0 -0
  374. package/skills/global_config/brag/assets/sfx/ui/click3.ogg +0 -0
  375. package/skills/global_config/brag/assets/sfx/ui/click4.ogg +0 -0
  376. package/skills/global_config/brag/assets/sfx/ui/click5.ogg +0 -0
  377. package/skills/global_config/brag/assets/sfx/ui/mouseclick1.ogg +0 -0
  378. package/skills/global_config/brag/assets/sfx/ui/rollover1.ogg +0 -0
  379. package/skills/global_config/brag/assets/sfx/ui/rollover2.ogg +0 -0
  380. package/skills/global_config/brag/assets/sfx/ui/rollover4.ogg +0 -0
  381. package/skills/global_config/brag/assets/sfx/ui/rollover5.ogg +0 -0
  382. package/skills/global_config/brag/assets/sfx/ui/switch1.ogg +0 -0
  383. package/skills/global_config/brag/assets/sfx/ui/switch10.ogg +0 -0
  384. package/skills/global_config/brag/assets/sfx/ui/switch11.ogg +0 -0
  385. package/skills/global_config/brag/assets/sfx/ui/switch12.ogg +0 -0
  386. package/skills/global_config/brag/assets/sfx/ui/switch13.ogg +0 -0
  387. package/skills/global_config/brag/assets/sfx/ui/switch14.ogg +0 -0
  388. package/skills/global_config/brag/assets/sfx/ui/switch15.ogg +0 -0
  389. package/skills/global_config/brag/assets/sfx/ui/switch16.ogg +0 -0
  390. package/skills/global_config/brag/assets/sfx/ui/switch17.ogg +0 -0
  391. package/skills/global_config/brag/assets/sfx/ui/switch18.ogg +0 -0
  392. package/skills/global_config/brag/assets/sfx/ui/switch19.ogg +0 -0
  393. package/skills/global_config/brag/assets/sfx/ui/switch2.ogg +0 -0
  394. package/skills/global_config/brag/assets/sfx/ui/switch20.ogg +0 -0
  395. package/skills/global_config/brag/assets/sfx/ui/switch21.ogg +0 -0
  396. package/skills/global_config/brag/assets/sfx/ui/switch22.ogg +0 -0
  397. package/skills/global_config/brag/assets/sfx/ui/switch23.ogg +0 -0
  398. package/skills/global_config/brag/assets/sfx/ui/switch24.ogg +0 -0
  399. package/skills/global_config/brag/assets/sfx/ui/switch25.ogg +0 -0
  400. package/skills/global_config/brag/assets/sfx/ui/switch26.ogg +0 -0
  401. package/skills/global_config/brag/assets/sfx/ui/switch27.ogg +0 -0
  402. package/skills/global_config/brag/assets/sfx/ui/switch28.ogg +0 -0
  403. package/skills/global_config/brag/assets/sfx/ui/switch29.ogg +0 -0
  404. package/skills/global_config/brag/assets/sfx/ui/switch3.ogg +0 -0
  405. package/skills/global_config/brag/assets/sfx/ui/switch30.ogg +0 -0
  406. package/skills/global_config/brag/assets/sfx/ui/switch31.ogg +0 -0
  407. package/skills/global_config/brag/assets/sfx/ui/switch32.ogg +0 -0
  408. package/skills/global_config/brag/assets/sfx/ui/switch33.ogg +0 -0
  409. package/skills/global_config/brag/assets/sfx/ui/switch34.ogg +0 -0
  410. package/skills/global_config/brag/assets/sfx/ui/switch35.ogg +0 -0
  411. package/skills/global_config/brag/assets/sfx/ui/switch36.ogg +0 -0
  412. package/skills/global_config/brag/assets/sfx/ui/switch37.ogg +0 -0
  413. package/skills/global_config/brag/assets/sfx/ui/switch38.ogg +0 -0
  414. package/skills/global_config/brag/assets/sfx/ui/switch4.ogg +0 -0
  415. package/skills/global_config/brag/assets/sfx/ui/switch5.ogg +0 -0
  416. package/skills/global_config/brag/assets/sfx/ui/switch6.ogg +0 -0
  417. package/skills/global_config/brag/assets/sfx/ui/switch7.ogg +0 -0
  418. package/skills/global_config/brag/assets/sfx/ui/switch8.ogg +0 -0
  419. package/skills/global_config/brag/assets/sfx/ui/switch9.ogg +0 -0
  420. package/skills/global_config/brag/references/audio.md +307 -0
  421. package/skills/global_config/brag/references/step-1-inspect.md +133 -0
  422. package/skills/global_config/brag/references/step-2-plan.md +234 -0
  423. package/skills/global_config/brag/references/step-3-compose.md +210 -0
  424. package/skills/global_config/brag/references/step-4-deliver.md +168 -0
  425. package/skills/global_config/brag/references/tones.md +231 -0
  426. package/skills/global_config/brag/scripts/analyze_music_cues.py +309 -0
  427. package/skills/global_config/brag/scripts/pyproject.toml +14 -0
  428. package/skills/global_config/brag/scripts/uv.lock +819 -0
  429. package/skills/global_config/deja-memory/SKILL.md +42 -0
  430. package/skills/global_config/editable-design/LICENSE +202 -0
  431. package/skills/global_config/editable-design/README.md +34 -0
  432. package/skills/global_config/editable-design/SKILL.md +630 -0
  433. package/skills/global_config/editable-design/THIRD_PARTY_NOTICES.md +10 -0
  434. package/skills/global_config/editable-design/agents/openai.yaml +4 -0
  435. package/skills/global_config/editable-design/assets/editor/layer-editor.css +262 -0
  436. package/skills/global_config/editable-design/assets/editor/layer-editor.js +1929 -0
  437. package/skills/global_config/editable-design/assets/poster-starter/README.md +92 -0
  438. package/skills/global_config/editable-design/assets/poster-starter/assets/.gitkeep +0 -0
  439. package/skills/global_config/editable-design/assets/poster-starter/index.html +91 -0
  440. package/skills/global_config/editable-design/assets/poster-starter/poster.json +6 -0
  441. package/skills/global_config/editable-design/assets/poster-starter/reference/.gitkeep +1 -0
  442. package/skills/global_config/editable-design/assets/replay/icons/assets.png +0 -0
  443. package/skills/global_config/editable-design/assets/replay/icons/html.png +0 -0
  444. package/skills/global_config/editable-design/assets/replay/icons/input.png +0 -0
  445. package/skills/global_config/editable-design/assets/replay/icons/layers.png +0 -0
  446. package/skills/global_config/editable-design/assets/replay/icons/plan.png +0 -0
  447. package/skills/global_config/editable-design/assets/replay/icons/reference.png +0 -0
  448. package/skills/global_config/editable-design/assets/replay/icons/review.png +0 -0
  449. package/skills/global_config/editable-design/assets/replay/index.html +59 -0
  450. package/skills/global_config/editable-design/assets/replay/replay-motion.js +410 -0
  451. package/skills/global_config/editable-design/assets/replay/replay.css +380 -0
  452. package/skills/global_config/editable-design/assets/replay/replay.js +374 -0
  453. package/skills/global_config/editable-design/font-kit/package-lock.json +115 -0
  454. package/skills/global_config/editable-design/font-kit/package.json +19 -0
  455. package/skills/global_config/editable-design/pack.sh +20 -0
  456. package/skills/global_config/editable-design/references/asset-architecture.md +72 -0
  457. package/skills/global_config/editable-design/references/editor-pitfalls.md +70 -0
  458. package/skills/global_config/editable-design/references/editor-runtime.md +131 -0
  459. package/skills/global_config/editable-design/references/font-system.md +89 -0
  460. package/skills/global_config/editable-design/references/imagery.md +145 -0
  461. package/skills/global_config/editable-design/references/layout-typography.md +116 -0
  462. package/skills/global_config/editable-design/references/replay-contract.md +169 -0
  463. package/skills/global_config/editable-design/scripts/_browser.mjs +247 -0
  464. package/skills/global_config/editable-design/scripts/bake.mjs +87 -0
  465. package/skills/global_config/editable-design/scripts/build-replay.mjs +316 -0
  466. package/skills/global_config/editable-design/scripts/check-contract.mjs +293 -0
  467. package/skills/global_config/editable-design/scripts/check-fonts.sh +252 -0
  468. package/skills/global_config/editable-design/scripts/check-poster.sh +70 -0
  469. package/skills/global_config/editable-design/scripts/doctor.sh +78 -0
  470. package/skills/global_config/editable-design/scripts/explode.mjs +115 -0
  471. package/skills/global_config/editable-design/scripts/font-kit.mjs +187 -0
  472. package/skills/global_config/editable-design/scripts/import-asset.sh +23 -0
  473. package/skills/global_config/editable-design/scripts/init-poster.sh +64 -0
  474. package/skills/global_config/editable-design/scripts/install-font-kit.sh +13 -0
  475. package/skills/global_config/editable-design/scripts/package-lock.json +334 -0
  476. package/skills/global_config/editable-design/scripts/package.json +14 -0
  477. package/skills/global_config/editable-design/scripts/remove-chroma-key.py +201 -0
  478. package/skills/global_config/editable-design/scripts/render-poster.sh +202 -0
  479. package/skills/global_config/editable-design/scripts/trace.sh +159 -0
  480. package/skills/global_config/editable-design/scripts/verify-replay.mjs +97 -0
  481. package/skills/global_config/editable-design/scripts/verify.mjs +782 -0
  482. package/skills/global_config/editable-design/scripts/wire-editor.mjs +104 -0
  483. package/skills/global_config/editable-design/tests/fixtures/small-explode/index.html +86 -0
  484. package/skills/global_config/plan-canvas/SKILL.md +8 -0
@@ -0,0 +1,782 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * verify.mjs —— 编辑器功能验证
4
+ *
5
+ * node verify.mjs <海报 index.html> [--timings]
6
+ *
7
+ * A 类断言(转换类):固化视觉零偏差。
8
+ * B 类断言(编辑类):改完导出、重新加载后 computed style 与几何一致。
9
+ * —— 编辑功能是「故意改变视觉」的,A 类断言对它们不适用。
10
+ * B 类要防的是「改得动但导不出去」,尤其字体静默回落成系统字体。
11
+ *
12
+ * 退出码 0 = 全绿,1 = 有失败。
13
+ */
14
+ import { writeFileSync, existsSync, unlinkSync } from 'node:fs';
15
+ import { dirname, join, resolve, basename } from 'node:path';
16
+ import { launch, open, probe, diff, pixelDiff, wireTemp } from './_browser.mjs';
17
+
18
+ const file = process.argv[2];
19
+ if (!file || !existsSync(resolve(file))) {
20
+ console.error('用法: node verify.mjs <海报 index.html>');
21
+ process.exit(2);
22
+ }
23
+ const src = resolve(file);
24
+ const dir = dirname(src);
25
+ const layersOutput = join(dir, 'layers.html');
26
+ const showTimings = process.argv.includes('--timings');
27
+ const timingStartedAt = Date.now();
28
+ let timingMarkedAt = timingStartedAt;
29
+ let timingPhase = '启动浏览器';
30
+ const timingRows = [];
31
+
32
+ const markTiming = (nextPhase) => {
33
+ const now = Date.now();
34
+ if (showTimings) timingRows.push({ phase: timingPhase, ms: now - timingMarkedAt });
35
+ timingPhase = nextPhase;
36
+ timingMarkedAt = now;
37
+ };
38
+
39
+ let fails = 0;
40
+ let verifiedLayersHTML = '';
41
+ const check = (name, ok, extra = '') => {
42
+ if (!ok) fails++;
43
+ console.log(` ${ok ? '✅' : '❌'} ${name}${extra ? ' ' + extra : ''}`);
44
+ };
45
+ const group = (t) => {
46
+ markTiming(t);
47
+ console.log(`\n【${t}】`);
48
+ };
49
+
50
+ /** 采集每个图层的 computed style 与几何,用于 B 类往返比对。
51
+ * 不依赖编辑器注入的 class,因此导出物上同样可用。 */
52
+ const COLLECT = () => {
53
+ const cv = document.querySelector('[data-canvas-width]') || document.querySelector('.poster-canvas');
54
+ const c = cv.getBoundingClientRect();
55
+ const scale = c.width / (+cv.dataset.canvasWidth || cv.offsetWidth);
56
+ const out = {};
57
+ for (const el of cv.querySelectorAll('[data-layer-id]')) {
58
+ const nodes = [el, ...el.querySelectorAll('*')].filter((n) => !(n instanceof SVGElement));
59
+ const r = el.getBoundingClientRect();
60
+ out[el.dataset.layerId] = {
61
+ left: +((r.left - c.left) / scale).toFixed(1),
62
+ top: +((r.top - c.top) / scale).toFixed(1),
63
+ width: +(r.width / scale).toFixed(1),
64
+ height: +(r.height / scale).toFixed(1),
65
+ fonts: nodes.map((n) => {
66
+ const cs = getComputedStyle(n);
67
+ return `${cs.fontSize}|${cs.fontFamily}`;
68
+ }),
69
+ widths: nodes.map((n) => +(n.getBoundingClientRect().width / scale).toFixed(1)),
70
+ text: el.textContent.replace(/\s+/g, ' ').trim(),
71
+ };
72
+ }
73
+ return out;
74
+ };
75
+
76
+ function compare(a, b) {
77
+ const issues = [];
78
+ for (const id of Object.keys(a)) {
79
+ if (!b[id]) { issues.push(`${id} 在导出物中缺失`); continue; }
80
+ const x = a[id], y = b[id];
81
+ if (Math.abs(x.left - y.left) > 1 || Math.abs(x.top - y.top) > 1) {
82
+ issues.push(`${id} 坐标漂移 ${x.left},${x.top} → ${y.left},${y.top}`);
83
+ }
84
+ if (Math.abs(x.width - y.width) > 1 || Math.abs(x.height - y.height) > 1) {
85
+ issues.push(`${id} 尺寸漂移 ${x.width}×${x.height} → ${y.width}×${y.height}`);
86
+ }
87
+ if (x.text !== y.text) issues.push(`${id} 文本不一致`);
88
+ if (x.fonts.join('#') !== y.fonts.join('#')) {
89
+ const i = x.fonts.findIndex((v, k) => v !== y.fonts[k]);
90
+ issues.push(`${id} 字体/字号不一致: "${x.fonts[i]}" → "${y.fonts[i] ?? '(缺)'}"`);
91
+ }
92
+ // 字体若静默回落成系统字体,computed family 可能不变但渲染宽度会变
93
+ const wBad = x.widths.findIndex((v, k) => Math.abs(v - (y.widths[k] ?? 0)) > 1);
94
+ if (wBad >= 0) issues.push(`${id} 渲染宽度不一致 ${x.widths[wBad]} → ${y.widths[wBad] ?? '(缺)'}(字体可能回落)`);
95
+ }
96
+ for (const id of Object.keys(b)) if (!a[id]) issues.push(`${id} 多出于导出物`);
97
+ return issues;
98
+ }
99
+
100
+ const wired = wireTemp(src, '_hf_verify.html');
101
+ const browser = await launch();
102
+ try {
103
+ /* ---------- A 类:固化零偏差 ---------- */
104
+ group('A 类 · 固化视觉零偏差');
105
+ const { page: p0 } = await open(browser, src);
106
+ const before = await probe(p0);
107
+ const { page, errors } = await open(browser, wired.file, { waitEditor: true });
108
+ const afterBake = await probe(page);
109
+ const dA = diff(before, afterBake, 1);
110
+ check(`${dA.rows.length} 个图层坐标不变`, dA.ok, `最大偏差 ${dA.worst}px`);
111
+
112
+ // 几何一致不等于视觉一致:上下文样式、背景、伪元素和绘制顺序
113
+ // 发生变化时,图层框仍可能完全不动。所以固化类验收必须再过一道像素门。
114
+ // 编辑器里的画布放在固定、可滚动的舞台中,直接截取超出视口的
115
+ // DOM 可能被父层 overflow 裁掉。先导出一份未修改的 baked HTML,
116
+ // 再在同尺寸视口中与原稿比像素,比对的仍然是 bake 结果。
117
+ const bakedFile = join(dir, '_hf_verify_baked.html');
118
+ writeFileSync(bakedFile, await page.evaluate(() => window.__layerEditor.fullHTML()));
119
+ const canvasSize = await p0.evaluate(() => {
120
+ const cv = document.querySelector('[data-canvas-width]') || document.querySelector('.poster-canvas');
121
+ return {
122
+ width: Math.ceil(+cv.dataset.canvasWidth || cv.offsetWidth),
123
+ height: Math.ceil(+cv.dataset.canvasHeight || cv.offsetHeight),
124
+ };
125
+ });
126
+ const pixelViewport = {
127
+ width: Math.max(1600, Math.min(8192, canvasSize.width + 20)),
128
+ height: Math.max(1200, Math.min(8192, canvasSize.height + 20)),
129
+ deviceScaleFactor: 1,
130
+ };
131
+ await p0.setViewport(pixelViewport);
132
+ const { page: pBaked, errors: eBaked } = await open(browser, bakedFile, pixelViewport);
133
+ const pixels = await pixelDiff(
134
+ p0, '[data-canvas-width],.poster-canvas',
135
+ pBaked, '[data-canvas-width],.poster-canvas',
136
+ );
137
+ // 少量 mix-blend-mode / 阴影在被提升进新的 stacking context 后会产生
138
+ // 亚像素级合成差异;同时约束“变化像素比例”和“平均通道差”,可容纳
139
+ // 这种不可见噪声,又会拦住丢背景、错层、去旋转等真实视觉漂移。
140
+ const pixelOK = eBaked.length === 0 && pixels.ok
141
+ && pixels.changedRatio <= 0.005 && pixels.meanAbs <= 0.05;
142
+ check('画布像素级无可见漂移', pixelOK, pixels.reason
143
+ || `变化 ${(pixels.changedRatio * 100).toFixed(3)}%,平均通道差 ${pixels.meanAbs.toFixed(4)}`);
144
+ await pBaked.close();
145
+ try { unlinkSync(bakedFile); } catch {}
146
+
147
+ const scale = await page.evaluate(() => window.__layerEditor.state.scale);
148
+ const at = (sel) => page.evaluate((s) => {
149
+ const r = document.querySelector(s).getBoundingClientRect();
150
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
151
+ }, sel);
152
+ // puppeteer 的 clickCount:2 只改参数、不真的按两次,detail 恒为 1,
153
+ // 浏览器不会合成 dblclick。必须手动发两轮 down/up。
154
+ const dbl = async (sel) => {
155
+ const { x, y } = await at(sel);
156
+ await page.mouse.move(x, y);
157
+ await page.mouse.down(); await page.mouse.up();
158
+ await page.mouse.down({ clickCount: 2 }); await page.mouse.up({ clickCount: 2 });
159
+ await new Promise((r) => setTimeout(r, 180));
160
+ };
161
+ const layerIds = await page.evaluate(() => window.__layerEditor.layers().map((l) => l.id));
162
+ const groupInfo = await page.evaluate(() => [...window.__layerEditor.state.groups.values()].map((group) => ({
163
+ id: group.id,
164
+ label: group.label,
165
+ members: group.memberIds,
166
+ })));
167
+ const explodeExpectation = await page.evaluate(() => {
168
+ const layers = window.__layerEditor.layers();
169
+ const groups = [...window.__layerEditor.state.groups.values()];
170
+ const grouped = new Set(groups.flatMap((group) => group.memberIds));
171
+ const primary = groups.length + layers.filter((layer) => !grouped.has(layer.id)).length;
172
+ const textSupplements = primary < 5 ? layers.filter(({ id, el }) => {
173
+ if (!grouped.has(id)) return false;
174
+ const role = (el.dataset.explodeRole || 'copy').trim();
175
+ if (['surface', 'image', 'icon', 'decoration'].includes(role)) return false;
176
+ return Boolean((el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim());
177
+ }).length : 0;
178
+ return { primary, textSupplements, units: primary + textSupplements + 2 };
179
+ });
180
+ const expectedExplodedUnits = explodeExpectation.units;
181
+ const first = await page.evaluate(() => {
182
+ const layers = window.__layerEditor.layers();
183
+ return layers.find(({ el }) =>
184
+ el.classList.contains('hf-slot') || el.querySelector('.hf-slot'))?.id
185
+ || layers[0]?.id;
186
+ });
187
+ const sel1 = `[data-layer-id="${first}"]`;
188
+ const geomOf = (id) => page.evaluate((i) => {
189
+ const el = document.querySelector(`[data-layer-id="${i}"]`);
190
+ return el ? { left: parseFloat(el.style.left), top: parseFloat(el.style.top) } : null;
191
+ }, id);
192
+ const boxOf = (id) => page.evaluate((i) => {
193
+ const el = document.querySelector(`[data-layer-id="${i}"]`);
194
+ if (!el) return null;
195
+ const r = el.getBoundingClientRect();
196
+ const scale = window.__layerEditor.state.scale;
197
+ return {
198
+ left: parseFloat(el.style.left), top: parseFloat(el.style.top),
199
+ width: r.width / scale, height: r.height / scale,
200
+ widthLocked: el.dataset.hfWidthLocked === '1',
201
+ heightLocked: el.dataset.hfHeightLocked === '1',
202
+ };
203
+ }, id);
204
+
205
+ /* ---------- 拖拽 ---------- */
206
+ group('拖拽');
207
+ const g0 = await geomOf(first);
208
+ const pt = await at(sel1);
209
+ await page.mouse.move(pt.x, pt.y);
210
+ await page.mouse.down();
211
+ await page.mouse.move(pt.x + 60, pt.y + 40, { steps: 12 });
212
+ await page.mouse.up();
213
+ const g1 = await geomOf(first);
214
+ const moved = Math.abs(g1.left - g0.left) > 20 && Math.abs(g1.top - g0.top) > 10;
215
+ check('位移按画布缩放换算', moved,
216
+ `屏幕 +60,+40 → 画布 +${(g1.left - g0.left).toFixed(1)},+${(g1.top - g0.top).toFixed(1)}(缩放 ${Math.round(scale * 100)}%,含吸附修正)`);
217
+ check('拖拽未误选文字', await page.evaluate(() => getSelection().toString() === ''));
218
+ await page.evaluate(() => window.__layerEditor.undo());
219
+ const g2 = await geomOf(first);
220
+ check('撤销精确复位', Math.abs(g2.left - g0.left) < 0.5 && Math.abs(g2.top - g0.top) < 0.5);
221
+ const release0 = await geomOf(first);
222
+ await page.evaluate((id) => {
223
+ const el = window.__layerEditor.layers().find((item) => item.id === id).el;
224
+ const rect = el.getBoundingClientRect();
225
+ const init = { bubbles: true, pointerId: 97, pointerType: 'mouse', isPrimary: true };
226
+ el.dispatchEvent(new PointerEvent('pointerdown', {
227
+ ...init, button: 0, buttons: 1, clientX: rect.left + 4, clientY: rect.top + 4,
228
+ }));
229
+ el.dispatchEvent(new PointerEvent('pointermove', {
230
+ ...init, button: -1, buttons: 0, clientX: rect.left + 90, clientY: rect.top + 60,
231
+ }));
232
+ window.dispatchEvent(new PointerEvent('pointerup', {
233
+ ...init, button: 0, buttons: 0, clientX: rect.left + 90, clientY: rect.top + 60,
234
+ }));
235
+ }, first);
236
+ const release1 = await geomOf(first);
237
+ check('鼠标已松开时不会残留拖拽', Math.abs(release1.left - release0.left) < 0.5
238
+ && Math.abs(release1.top - release0.top) < 0.5);
239
+ const escape0 = await geomOf(first);
240
+ const escapeAt = await at(`[data-layer-id="${first}"]`);
241
+ await page.mouse.move(escapeAt.x, escapeAt.y);
242
+ await page.mouse.down();
243
+ await page.mouse.move(escapeAt.x + 42, escapeAt.y + 24, { steps: 3 });
244
+ const escapeMoved = await geomOf(first);
245
+ await page.keyboard.press('Escape');
246
+ await page.mouse.up();
247
+ const escape1 = await geomOf(first);
248
+ check('Esc 可取消进行中的拖拽', Math.abs(escapeMoved.left - escape0.left) > 5
249
+ && Math.abs(escape1.left - escape0.left) < 0.5
250
+ && Math.abs(escape1.top - escape0.top) < 0.5);
251
+
252
+ /* ---------- 宽高缩放 ---------- */
253
+ group('宽高缩放');
254
+ await page.evaluate((id) => {
255
+ const layer = window.__layerEditor.layers().find((item) => item.id === id);
256
+ window.__layerEditor.select(layer.el);
257
+ }, first);
258
+ check('选中后显示八个缩放手柄', await page.evaluate(() =>
259
+ document.querySelectorAll('.hf-resize-handle').length === 8
260
+ && !document.querySelector('.hf-resize-overlay').hidden));
261
+ check('缩放手柄具有宽松点击热区', await page.evaluate(() =>
262
+ [...document.querySelectorAll('.hf-resize-handle')].every((handle) => {
263
+ const rect = handle.getBoundingClientRect();
264
+ return rect.width >= 28 && rect.height >= 28;
265
+ })));
266
+ const size0 = await boxOf(first);
267
+ const dragHandle = async (selector, dx, dy) => {
268
+ const handle = await page.$(selector);
269
+ const box = await handle?.boundingBox();
270
+ if (!box) return false;
271
+ const x = box.x + box.width / 2;
272
+ const y = box.y + box.height / 2;
273
+ await page.mouse.move(x, y);
274
+ await page.mouse.down();
275
+ await page.mouse.move(x + dx, y + dy, { steps: 10 });
276
+ await page.mouse.up();
277
+ return true;
278
+ };
279
+ const eastReady = await dragHandle('.hf-resize-handle.e', 52, 0);
280
+ const sizeW = await boxOf(first);
281
+ check('左右手柄只改变宽度', eastReady
282
+ && sizeW.width > size0.width + 20
283
+ && Math.abs(sizeW.height - size0.height) < 0.5
284
+ && sizeW.widthLocked && !sizeW.heightLocked,
285
+ `${size0.width.toFixed(1)}×${size0.height.toFixed(1)} → ${sizeW.width.toFixed(1)}×${sizeW.height.toFixed(1)}`);
286
+ const southReady = await dragHandle('.hf-resize-handle.s', 0, 38);
287
+ const sizeWH = await boxOf(first);
288
+ check('上下手柄只改变高度', southReady
289
+ && sizeWH.height > sizeW.height + 15
290
+ && Math.abs(sizeWH.width - sizeW.width) < 0.5
291
+ && sizeWH.widthLocked && sizeWH.heightLocked,
292
+ `${sizeW.width.toFixed(1)}×${sizeW.height.toFixed(1)} → ${sizeWH.width.toFixed(1)}×${sizeWH.height.toFixed(1)}`);
293
+ const resizedSnapshot = await page.evaluate((id) =>
294
+ window.__layerEditor.snapshot().layers.find((layer) => layer.id === id), first);
295
+ check('草稿快照保留手动宽高', resizedSnapshot?.manualWidth === true
296
+ && resizedSnapshot?.manualHeight === true && !!resizedSnapshot?.height);
297
+ const lockedWidth = sizeWH.width;
298
+ await page.evaluate((id) => {
299
+ const layer = window.__layerEditor.layers().find((item) => item.id === id);
300
+ window.__layerEditor.scaleFont(layer.el, 1.08);
301
+ }, first);
302
+ const sizeAfterFont = await boxOf(first);
303
+ check('手动宽度不被文字或字号自动撑回', Math.abs(sizeAfterFont.width - lockedWidth) < 0.5,
304
+ `${lockedWidth.toFixed(1)} → ${sizeAfterFont.width.toFixed(1)}`);
305
+ await page.evaluate(() => {
306
+ window.__layerEditor.undo();
307
+ window.__layerEditor.undo();
308
+ window.__layerEditor.undo();
309
+ });
310
+ const sizeBack = await boxOf(first);
311
+ check('撤销恢复原始宽高与自动适配状态', Math.abs(sizeBack.width - size0.width) < 0.5
312
+ && Math.abs(sizeBack.height - size0.height) < 0.5
313
+ && !sizeBack.widthLocked && !sizeBack.heightLocked,
314
+ `${sizeBack.width.toFixed(1)}×${sizeBack.height.toFixed(1)}`);
315
+ const resizeEscape0 = await boxOf(first);
316
+ const seHandle = await page.$('.hf-resize-handle.se');
317
+ const seBox = await seHandle?.boundingBox();
318
+ if (seBox) {
319
+ const x = seBox.x + seBox.width / 2;
320
+ const y = seBox.y + seBox.height / 2;
321
+ await page.mouse.move(x, y);
322
+ await page.mouse.down();
323
+ await page.mouse.move(x + 44, y + 32, { steps: 3 });
324
+ const resizeEscapeMoved = await boxOf(first);
325
+ await page.keyboard.press('Escape');
326
+ await page.mouse.up();
327
+ const resizeEscape1 = await boxOf(first);
328
+ check('Esc 可取消进行中的缩放', Math.abs(resizeEscapeMoved.width - resizeEscape0.width) > 5
329
+ && Math.abs(resizeEscape1.width - resizeEscape0.width) < 1
330
+ && Math.abs(resizeEscape1.height - resizeEscape0.height) < 1);
331
+ } else {
332
+ check('Esc 可取消进行中的缩放', false, '未找到右下角手柄');
333
+ }
334
+
335
+ /* ---------- 文本编辑 ---------- */
336
+ group('文本编辑');
337
+ const slotSel = await page.evaluate((id) => {
338
+ const layer = document.querySelector(`[data-layer-id="${id}"]`);
339
+ const slot = layer.classList.contains('hf-slot') ? layer : layer.querySelector('.hf-slot');
340
+ if (!slot) return null;
341
+ slot.setAttribute('data-hf-probe', '1');
342
+ return '[data-hf-probe]';
343
+ }, first);
344
+ if (!slotSel) {
345
+ check('图层内找到文本槽', false, `${first} 无文本槽`);
346
+ } else {
347
+ const origText = await page.evaluate((s) => document.querySelector(s).textContent, slotSel);
348
+ await dbl(slotSel);
349
+ check('双击进入编辑态', await page.evaluate(() => !!document.querySelector('.hf-editing')));
350
+ check('已设为 plaintext-only',
351
+ await page.evaluate(() => document.querySelector('.hf-editing')?.getAttribute('contenteditable') === 'plaintext-only'));
352
+ check('内容已全选', await page.evaluate((t) => getSelection().toString().trim() === t.trim(), origText));
353
+
354
+ const posBefore = await geomOf(first);
355
+ await page.keyboard.press('ArrowRight');
356
+ await page.keyboard.press('ArrowDown');
357
+ const posAfter = await geomOf(first);
358
+ check('编辑态方向键不挪图层', posBefore.left === posAfter.left && posBefore.top === posAfter.top);
359
+
360
+ await page.keyboard.press('Escape');
361
+ await new Promise((r) => setTimeout(r, 120));
362
+ check('Esc 回滚原文', await page.evaluate((s, t) => document.querySelector(s).textContent === t, slotSel, origText));
363
+
364
+ await dbl(slotSel);
365
+ await page.evaluate((s) => {
366
+ const r = document.createRange();
367
+ r.selectNodeContents(document.querySelector(s));
368
+ const g = getSelection(); g.removeAllRanges(); g.addRange(r);
369
+ }, slotSel);
370
+ await page.keyboard.type('验证文本ABC');
371
+ await page.keyboard.press('Enter');
372
+ await new Promise((r) => setTimeout(r, 200));
373
+ check('Enter 提交改动', await page.evaluate((s) => document.querySelector(s).textContent === '验证文本ABC', slotSel));
374
+ await page.evaluate(() => window.__layerEditor.undo());
375
+ check('⌘Z 还原文字', await page.evaluate((s, t) => document.querySelector(s).textContent === t, slotSel, origText));
376
+ }
377
+
378
+ /* ---------- 多字号结构安全 ---------- */
379
+ const multi = await page.evaluate(() => {
380
+ for (const l of window.__layerEditor.layers()) {
381
+ const slots = [...l.el.querySelectorAll('.hf-slot')];
382
+ if (slots.length >= 2) {
383
+ const sizes = new Set(slots.map((s) => getComputedStyle(s).fontSize));
384
+ if (sizes.size >= 2) {
385
+ slots[0].parentElement.setAttribute('data-hf-multi', '1');
386
+ return { id: l.id, n: slots.length };
387
+ }
388
+ }
389
+ }
390
+ return null;
391
+ });
392
+ if (multi) {
393
+ group(`结构安全 · ${multi.id}(多字号拼排)`);
394
+ const structBefore = await page.evaluate(() => {
395
+ const box = document.querySelector('[data-hf-multi]');
396
+ return [...box.children].map((c) => `${c.tagName}.${(c.className || '').replace(/hf-\S+/g, '').trim()}`).join(',');
397
+ });
398
+ const target = await page.evaluate(() => {
399
+ const box = document.querySelector('[data-hf-multi]');
400
+ const slot = box.querySelector('.hf-slot');
401
+ slot.setAttribute('data-hf-probe2', '1');
402
+ return { fs: getComputedStyle(slot).fontSize, color: getComputedStyle(slot).color };
403
+ });
404
+ await dbl('[data-hf-probe2]');
405
+ await page.keyboard.type('99');
406
+ await page.keyboard.press('Enter');
407
+ await new Promise((r) => setTimeout(r, 200));
408
+ const structAfter = await page.evaluate(() => {
409
+ const box = document.querySelector('[data-hf-multi]');
410
+ const slot = box.querySelector('[data-hf-probe2]');
411
+ return {
412
+ children: [...box.children].map((c) => `${c.tagName}.${(c.className || '').replace(/hf-\S+/g, '').trim()}`).join(','),
413
+ text: slot.textContent,
414
+ fs: getComputedStyle(slot).fontSize,
415
+ color: getComputedStyle(slot).color,
416
+ };
417
+ });
418
+ check('兄弟 span 结构未被破坏', structBefore === structAfter.children, structAfter.children);
419
+ check('文字已改', structAfter.text === '99');
420
+ check('字号未变', structAfter.fs === target.fs, structAfter.fs);
421
+ check('颜色未变', structAfter.color === target.color);
422
+ await page.evaluate(() => window.__layerEditor.undo());
423
+ }
424
+
425
+ /* ---------- 字号 ---------- */
426
+ group('字号');
427
+ const fsBefore = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontSize, slotSel || sel1);
428
+ await page.evaluate((id) => {
429
+ const l = window.__layerEditor.layers().find((x) => x.id === id);
430
+ window.__layerEditor.select(l.el);
431
+ window.__layerEditor.scaleFont(l.el, 1.08);
432
+ }, first);
433
+ await new Promise((r) => setTimeout(r, 120));
434
+ const fsAfter = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontSize, slotSel || sel1);
435
+ check('A+ 放大字号', parseFloat(fsAfter) > parseFloat(fsBefore), `${fsBefore} → ${fsAfter}`);
436
+ await page.evaluate(() => window.__layerEditor.undo());
437
+ const fsBack = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontSize, slotSel || sel1);
438
+ check('撤销恢复字号', fsBack === fsBefore, fsBack);
439
+
440
+ /* ---------- 字体 ---------- */
441
+ group('字体');
442
+ const opts = await page.evaluate(() => window.__layerEditor.fontOptions());
443
+ check('从 :root 解析出字体族', opts.length > 0, opts.map((o) => o.label).join(' / ') || '(无)');
444
+ let fontChanged = false;
445
+ if (opts.length > 1) {
446
+ const ffBefore = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontFamily, slotSel || sel1);
447
+ // 选一个与当前不同的字体族
448
+ for (const o of opts) {
449
+ await page.evaluate((id, v) => {
450
+ const l = window.__layerEditor.layers().find((x) => x.id === id);
451
+ window.__layerEditor.setFont(l.el, v);
452
+ }, first, o.value);
453
+ await new Promise((r) => setTimeout(r, 120));
454
+ const now = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontFamily, slotSel || sel1);
455
+ if (now !== ffBefore) { fontChanged = true; break; }
456
+ await page.evaluate(() => window.__layerEditor.undo());
457
+ }
458
+ check('切换字体生效', fontChanged);
459
+ } else if (opts.length === 1) {
460
+ check('单一字体栈无需切换', true, opts[0].label);
461
+ }
462
+
463
+ /* ---------- 删除图层 ---------- */
464
+ group('删除图层');
465
+ const victim = layerIds.find((id) => id !== first && id !== 'hero_photo') || layerIds[layerIds.length - 1];
466
+ const othersBefore = await page.evaluate(COLLECT);
467
+ await page.evaluate((id) => {
468
+ const l = window.__layerEditor.layers().find((x) => x.id === id);
469
+ window.__layerEditor.removeLayer(l.el);
470
+ }, victim);
471
+ await new Promise((r) => setTimeout(r, 120));
472
+ check(`${victim} 已从 DOM 移除`, await page.evaluate((id) => !document.querySelector(`[data-layer-id="${id}"]`), victim));
473
+ const othersAfter = await page.evaluate(COLLECT);
474
+ const untouched = Object.keys(othersAfter).every((id) =>
475
+ Math.abs(othersAfter[id].left - othersBefore[id].left) < 0.5
476
+ && Math.abs(othersAfter[id].top - othersBefore[id].top) < 0.5);
477
+ check('其他图层坐标不受影响', untouched);
478
+ await page.evaluate(() => window.__layerEditor.undo());
479
+ await new Promise((r) => setTimeout(r, 120));
480
+ check('撤销恢复图层', await page.evaluate((id) => !!document.querySelector(`[data-layer-id="${id}"]`), victim));
481
+ const restored = await page.evaluate(COLLECT);
482
+ check('恢复后位置正确', Object.keys(othersBefore).every((id) =>
483
+ restored[id] && Math.abs(restored[id].left - othersBefore[id].left) < 0.5));
484
+
485
+ /* ---------- 拆解视图 ---------- */
486
+ group('拆解视图');
487
+ const composedBeforeSpread = await page.evaluate(COLLECT);
488
+ const composedScreenHeight = await page.evaluate(() =>
489
+ document.querySelector('.hf-canvas').getBoundingClientRect().height);
490
+ const enterMotion = await page.evaluate(async () => {
491
+ void window.__layerEditor.setExploded(true);
492
+ await new Promise((r) => setTimeout(r, 180));
493
+ return {
494
+ running: document.getAnimations().filter((a) => a.playState === 'running').length,
495
+ scanning: document.querySelector('.hf-exploded-scan-beam')?.getAnimations()
496
+ .some((a) => a.playState === 'running') || false,
497
+ labelsVisible: [...document.querySelectorAll('.hf-exploded-label')].some((label) =>
498
+ Number.parseFloat(getComputedStyle(label).opacity) > .05),
499
+ reduced: matchMedia('(prefers-reduced-motion: reduce)').matches
500
+ || new URLSearchParams(location.search).get('motion') === '0',
501
+ };
502
+ });
503
+ check('拆解前先执行扫描特效', enterMotion.reduced || enterMotion.scanning,
504
+ enterMotion.reduced ? '已遵循 reduced-motion' : '扫描束运行中');
505
+ check('进入时执行分层散开动画', enterMotion.reduced || enterMotion.running > 0,
506
+ enterMotion.reduced ? '已遵循 reduced-motion' : `${enterMotion.running} 个动画运行中`);
507
+ check('扫描与散开期间不放大模块标签', enterMotion.reduced || !enterMotion.labelsVisible,
508
+ enterMotion.reduced ? '已遵循 reduced-motion' : '运动阶段标签保持隐藏');
509
+ await page.waitForFunction(() => !window.__layerEditor.state.transitioning, { timeout: 20000 });
510
+ await new Promise((r) => setTimeout(r, 220));
511
+ const liveSpread = await page.evaluate(() => ({
512
+ active: document.body.classList.contains('hf-exploded-on'),
513
+ board: !!document.querySelector('.hf-exploded-board'),
514
+ scanHeight: document.querySelector('.hf-exploded-scan')?.getBoundingClientRect().height || 0,
515
+ boardHeight: document.querySelector('.hf-exploded-board')?.getBoundingClientRect().height || 0,
516
+ laneCount: window.__layerEditor.state.spread?.laneCount || 0,
517
+ units: document.querySelectorAll('.hf-exploded-board .hf-exploded-item').length,
518
+ layers: document.querySelectorAll('.hf-exploded-board [data-exploded-layer]').length,
519
+ uniqueLayers: new Set([...document.querySelectorAll('.hf-exploded-board [data-exploded-layer]')]
520
+ .map((node) => node.dataset.explodedLayer)).size,
521
+ primaryContent: Number(document.querySelector('.hf-exploded-board')?.dataset.explodedPrimaryCount || 0),
522
+ textSupplements: document.querySelectorAll('.hf-exploded-item[data-exploded-text-supplement]').length,
523
+ bases: document.querySelectorAll('.hf-exploded-base').length,
524
+ overviews: document.querySelectorAll('.hf-exploded-item[data-exploded-overview]').length,
525
+ backgrounds: document.querySelectorAll('.hf-exploded-item[data-exploded-background]').length,
526
+ groups: document.querySelectorAll('.hf-exploded-item[data-exploded-group]').length,
527
+ gallery: (() => {
528
+ const board = document.querySelector('.hf-exploded-board');
529
+ const width = parseFloat(board?.style.width) || 1;
530
+ const height = parseFloat(board?.style.height) || 1;
531
+ const items = [...document.querySelectorAll('.hf-exploded-item')].map((item) => ({
532
+ left: parseFloat(item.style.left) || 0,
533
+ top: parseFloat(item.style.top) || 0,
534
+ width: parseFloat(item.style.width) || 0,
535
+ height: parseFloat(item.style.height) || 0,
536
+ }));
537
+ const minX = Math.min(...items.map((item) => item.left));
538
+ const maxX = Math.max(...items.map((item) => item.left + item.width));
539
+ const special = [...document.querySelectorAll('[data-exploded-overview],[data-exploded-background]')]
540
+ .map((item) => (parseFloat(item.style.height) || 0) / height);
541
+ return {
542
+ span: (maxX - minX) / width,
543
+ centerOccupied: items.some((item) => item.left < width * .58 && item.left + item.width > width * .42),
544
+ specialMin: Math.min(...special), specialMax: Math.max(...special),
545
+ };
546
+ })(),
547
+ labelsVisible: [...document.querySelectorAll('.hf-exploded-label')].some((label) =>
548
+ Number.parseFloat(getComputedStyle(label).opacity) > .05),
549
+ labelsNamed: [...document.querySelectorAll('.hf-exploded-item[data-exploded-group]')].every((item) =>
550
+ Boolean(item.title?.trim())),
551
+ hasDrilldownAPI: typeof window.__layerEditor.openExplodedGroup === 'function',
552
+ level: document.querySelector('.hf-exploded-board')?.dataset.explodedLevel,
553
+ theme: document.querySelector('.hf-exploded-board')?.dataset.explodedTheme,
554
+ expectedTheme: (() => {
555
+ const cs = getComputedStyle(document.querySelector('.hf-canvas'));
556
+ const parse = (raw) => {
557
+ const rgba = String(raw).match(/rgba?\((\d+(?:\.\d+)?)[, ]+(\d+(?:\.\d+)?)[, ]+(\d+(?:\.\d+)?)(?:\s*[,/]\s*(\d*(?:\.\d+)?))?/i);
558
+ if (rgba) return { rgb: rgba.slice(1, 4).map(Number), alpha: rgba[4] === undefined ? 1 : Number(rgba[4]) };
559
+ const hex = String(raw).match(/#([0-9a-f]{6}|[0-9a-f]{3})(?![0-9a-f])/i);
560
+ if (!hex) return null;
561
+ const v = hex[1].length === 3 ? [...hex[1]].map((c) => c + c).join('') : hex[1];
562
+ return { rgb: [0, 2, 4].map((i) => parseInt(v.slice(i, i + 2), 16)), alpha: 1 };
563
+ };
564
+ let color = parse(cs.backgroundColor);
565
+ if (!color || color.alpha <= .25) {
566
+ const tokens = String(cs.backgroundImage).match(/#[0-9a-f]{3,8}|rgba?\([^)]*\)/ig) || [];
567
+ color = tokens.map(parse).find((entry) => entry && entry.alpha > .65) || null;
568
+ }
569
+ const rgb = color?.rgb || [244, 241, 232];
570
+ const linear = rgb.map((n) => {
571
+ const c = n / 255;
572
+ return c <= .03928 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
573
+ });
574
+ return linear[0] * .2126 + linear[1] * .7152 + linear[2] * .0722 > .52 ? 'light' : 'dark';
575
+ })(),
576
+ sourceVisible: getComputedStyle(document.querySelector('.hf-stage > .hf-canvas')).display !== 'none',
577
+ }));
578
+ check('可切换到横向拆解画布', liveSpread.active && liveSpread.board && !liveSpread.sourceVisible);
579
+ check('全部内容叶子均归入一级拆解', liveSpread.uniqueLayers === layerIds.length,
580
+ `${liveSpread.uniqueLayers}/${layerIds.length} 个唯一叶子,${liveSpread.layers} 次呈现`);
581
+ check('完整总览与背景均作为普通组件', liveSpread.overviews === 1
582
+ && liveSpread.backgrounds === 1 && liveSpread.bases === 0,
583
+ `总览 ${liveSpread.overviews}、背景 ${liveSpread.backgrounds}、固定基底 ${liveSpread.bases}`);
584
+ check('组件画廊数量完整', liveSpread.units === expectedExplodedUnits,
585
+ `${liveSpread.units}/${expectedExplodedUnits}`);
586
+ check('少组件时补入分组文字叶子', liveSpread.primaryContent === explodeExpectation.primary
587
+ && liveSpread.textSupplements === explodeExpectation.textSupplements,
588
+ `一级组件 ${liveSpread.primaryContent},文字补充 ${liveSpread.textSupplements}`);
589
+ check('拆解舞台跟随海报明暗主题', liveSpread.theme === liveSpread.expectedTheme,
590
+ `${liveSpread.theme}/${liveSpread.expectedTheme}`);
591
+ check('一级拆解保持分组兼容', liveSpread.level === '1' && liveSpread.groups === groupInfo.length,
592
+ `${liveSpread.groups}/${groupInfo.length} 个模块`);
593
+ check('拆解视图不提供第二级叶子钻取', !liveSpread.hasDrilldownAPI,
594
+ '仅保留一级内容模块');
595
+ check('静止拆解稿不叠加冗余模块标签', !liveSpread.labelsVisible,
596
+ '名称仅在明确交互时提示');
597
+ check('模块仍保留可发现的语义名称', !groupInfo.length || liveSpread.labelsNamed,
598
+ groupInfo.length ? `${liveSpread.groups} 个模块均有名称` : '无一级模块标签');
599
+ const originRatio = liveSpread.scanHeight / composedScreenHeight;
600
+ check('扫描原点保持完整成稿的可读尺度', originRatio >= 0.68,
601
+ `扫描态为编辑态高度的 ${(originRatio * 100).toFixed(1)}%`);
602
+ check('终态铺满舞台且中央无大块保留区', liveSpread.laneCount >= 2
603
+ && liveSpread.gallery.centerOccupied && liveSpread.gallery.span >= .72,
604
+ `${liveSpread.laneCount} 列,横向覆盖 ${(liveSpread.gallery.span * 100).toFixed(1)}%`);
605
+ check('总览与背景均为小型组件', liveSpread.gallery.specialMin >= .08
606
+ && liveSpread.gallery.specialMax <= .38,
607
+ `高度占舞台 ${(liveSpread.gallery.specialMin * 100).toFixed(1)}%–${(liveSpread.gallery.specialMax * 100).toFixed(1)}%`);
608
+
609
+ if (groupInfo.length) {
610
+ await page.hover('.hf-exploded-item[data-exploded-group]');
611
+ await new Promise((r) => setTimeout(r, 220));
612
+ check('悬停模块时才显示名称提示', await page.evaluate(() =>
613
+ Number.parseFloat(getComputedStyle(document.querySelector('.hf-exploded-item[data-exploded-group] .hf-exploded-label')).opacity) > .9));
614
+ await page.mouse.move(0, 0);
615
+ await page.click('.hf-exploded-item[data-exploded-group]');
616
+ await new Promise((r) => setTimeout(r, 120));
617
+ check('点击内容模块不会进入细粒度第二层', await page.evaluate(() =>
618
+ document.querySelector('.hf-exploded-board')?.dataset.explodedLevel === '1'
619
+ && !document.querySelector('.hf-exploded-nav')));
620
+ }
621
+
622
+ const spreadHTML = await page.evaluate(() => window.__layerEditor.explodedHTML());
623
+ verifiedLayersHTML = spreadHTML;
624
+ check('拆解导出物无编辑器依赖', !/layer-editor|class="hf-(bar|panel|modal|toast)/.test(spreadHTML));
625
+ const spreadFile = join(dir, '_hf_verify_layers.html');
626
+ writeFileSync(spreadFile, spreadHTML);
627
+ const { page: pSpread, errors: eSpread } = await open(browser, spreadFile, { width: 1600, height: 1000 });
628
+ await pSpread.waitForSelector('html[data-hf-exploded-ready]', { timeout: 20000 });
629
+ const spreadAudit = await pSpread.evaluate(() => ({
630
+ units: document.querySelectorAll('.hf-exploded-board .hf-exploded-item').length,
631
+ layers: document.querySelectorAll('[data-exploded-layer]').length,
632
+ uniqueLayers: new Set([...document.querySelectorAll('[data-exploded-layer]')]
633
+ .map((node) => node.dataset.explodedLayer)).size,
634
+ primaryContent: Number(document.querySelector('.hf-exploded-board')?.dataset.explodedPrimaryCount || 0),
635
+ textSupplements: document.querySelectorAll('.hf-exploded-item[data-exploded-text-supplement]').length,
636
+ bases: document.querySelectorAll('.hf-exploded-base').length,
637
+ overviews: document.querySelectorAll('[data-exploded-overview]').length,
638
+ backgrounds: document.querySelectorAll('[data-exploded-background]').length,
639
+ groups: document.querySelectorAll('.hf-exploded-item[data-exploded-group]').length,
640
+ level: document.querySelector('.hf-exploded-board')?.dataset.explodedLevel,
641
+ drilldownUI: !!document.querySelector('.hf-exploded-nav'),
642
+ centerOccupied: (() => {
643
+ const board = document.querySelector('.hf-exploded-board');
644
+ const width = parseFloat(board?.style.width) || 1;
645
+ return [...document.querySelectorAll('.hf-exploded-item')].some((item) => {
646
+ const left = parseFloat(item.style.left) || 0;
647
+ const itemWidth = parseFloat(item.style.width) || 0;
648
+ return left < width * .58 && left + itemWidth > width * .42;
649
+ });
650
+ })(),
651
+ replay: !!document.querySelector('.hf-exploded-replay'),
652
+ motion: document.documentElement.dataset.hfMotion,
653
+ overflow: document.documentElement.scrollWidth > innerWidth + 1
654
+ || document.documentElement.scrollHeight > innerHeight + 1,
655
+ }));
656
+ check('拆解稿可独立渲染无报错', eSpread.length === 0, eSpread.join('; '));
657
+ check('拆解稿自适应且图层齐全', spreadAudit.uniqueLayers === layerIds.length && !spreadAudit.overflow,
658
+ `${spreadAudit.uniqueLayers}/${layerIds.length} 个唯一叶子,${spreadAudit.layers} 次呈现`);
659
+ check('独立拆解稿不保留中央大基底', spreadAudit.bases === 0
660
+ && spreadAudit.centerOccupied, `${spreadAudit.bases} 个固定基底`);
661
+ check('独立稿含完整总览与背景组件', spreadAudit.overviews === 1
662
+ && spreadAudit.backgrounds === 1 && spreadAudit.units === expectedExplodedUnits,
663
+ `${spreadAudit.units}/${expectedExplodedUnits} 个组件`);
664
+ check('独立稿保留少组件文字补充', spreadAudit.primaryContent === explodeExpectation.primary
665
+ && spreadAudit.textSupplements === explodeExpectation.textSupplements,
666
+ `一级组件 ${spreadAudit.primaryContent},文字补充 ${spreadAudit.textSupplements}`);
667
+ check('拆解稿保留一级模块语义', spreadAudit.groups === groupInfo.length,
668
+ `${spreadAudit.groups}/${groupInfo.length} 个模块`);
669
+ check('独立拆解稿只有一级内容层', spreadAudit.level === '1' && !spreadAudit.drilldownUI,
670
+ `level ${spreadAudit.level}`);
671
+ check('拆解稿带自动播放与重播入口', spreadAudit.replay
672
+ && ['played', 'reduced'].includes(spreadAudit.motion), spreadAudit.motion || '未播放');
673
+ const replayMotion = await pSpread.evaluate(async () => {
674
+ if (document.documentElement.dataset.hfMotion === 'reduced') return { any: true, scan: true };
675
+ document.querySelector('.hf-exploded-replay').click();
676
+ await new Promise((r) => setTimeout(r, 160));
677
+ return {
678
+ any: document.getAnimations().some((a) => a.playState === 'running'),
679
+ scan: document.querySelector('.hf-exploded-scan-beam')?.getAnimations()
680
+ .some((a) => a.playState === 'running') || false,
681
+ };
682
+ });
683
+ check('重播按钮可重新触发扫描与拆解', replayMotion.any && replayMotion.scan);
684
+ await pSpread.close();
685
+ try { unlinkSync(spreadFile); } catch {}
686
+
687
+ const exitMotion = await page.evaluate(async () => {
688
+ void window.__layerEditor.setExploded(false);
689
+ await new Promise((r) => setTimeout(r, 150));
690
+ return {
691
+ running: document.getAnimations().filter((a) => a.playState === 'running').length,
692
+ reduced: matchMedia('(prefers-reduced-motion: reduce)').matches,
693
+ };
694
+ });
695
+ check('返回时执行反向合拢动画', exitMotion.reduced || exitMotion.running > 0,
696
+ exitMotion.reduced ? '已遵循 reduced-motion' : `${exitMotion.running} 个动画运行中`);
697
+ await page.waitForFunction(() => !window.__layerEditor.state.transitioning, { timeout: 20000 });
698
+ const composedAfterSpread = await page.evaluate(COLLECT);
699
+ check('返回编辑后原布局不变', compare(composedBeforeSpread, composedAfterSpread).length === 0);
700
+
701
+ /* ---------- B 类:导出往返一致 ---------- */
702
+ group('B 类 · 导出往返一致');
703
+ const stateBefore = await page.evaluate(COLLECT);
704
+ const html = await page.evaluate(() => window.__layerEditor.fullHTML());
705
+ check('导出物无编辑器残留', !/hf-|layer-editor/.test(html));
706
+ const outFile = join(dir, '_hf_verify_out.html');
707
+ writeFileSync(outFile, html);
708
+ const { page: pOut, errors: eOut } = await open(browser, outFile);
709
+ const stateAfter = await pOut.evaluate(COLLECT);
710
+ const issues = compare(stateBefore, stateAfter);
711
+ check('导出物可独立渲染无报错', eOut.length === 0, eOut.join('; '));
712
+ check('坐标 / 字号 / 字体 / 渲染宽度全部一致', issues.length === 0, issues.slice(0, 4).join(' | '));
713
+ try { unlinkSync(outFile); } catch {}
714
+
715
+ /* ---------- 草稿 ---------- */
716
+ group('草稿(localStorage)');
717
+ await page.evaluate((id) => {
718
+ const l = window.__layerEditor.layers().find((x) => x.id === id);
719
+ l.el.style.left = '777px';
720
+ l.el.style.top = '333px';
721
+ l.el.style.width = `${parseFloat(l.el.style.width) + 33}px`;
722
+ l.el.style.height = `${parseFloat(l.el.style.height) + 17}px`;
723
+ l.el.dataset.hfWidthLocked = '1';
724
+ l.el.dataset.hfHeightLocked = '1';
725
+ window.__layerEditor.state.removed.length = 0;
726
+ }, first);
727
+ await page.evaluate(() => window.__layerEditor.snapshot());
728
+ await page.evaluate((id) => {
729
+ // 走真实路径:模拟一次方向键触发 touch() 的草稿保存
730
+ const l = window.__layerEditor.layers().find((x) => x.id === id);
731
+ window.__layerEditor.select(l.el);
732
+ }, first);
733
+ await page.keyboard.press('ArrowRight');
734
+ await new Promise((r) => setTimeout(r, 800));
735
+ const draftRaw = await page.evaluate(() => {
736
+ const prefix = 'hf-draft:' + location.pathname;
737
+ const key = Object.keys(localStorage).find((candidate) =>
738
+ candidate === prefix || candidate.startsWith(prefix + ':'));
739
+ return key ? localStorage.getItem(key) : null;
740
+ });
741
+ check('草稿已写入 localStorage', !!draftRaw, draftRaw ? `${(draftRaw.length / 1024).toFixed(1)}KB` : '');
742
+
743
+ const expect = await boxOf(first);
744
+ await page.reload({ waitUntil: 'networkidle0' });
745
+ await page.waitForSelector('html[data-hf-ready]', { timeout: 20000 });
746
+ await new Promise((r) => setTimeout(r, 400));
747
+ const afterReload = await boxOf(first);
748
+ check('刷新后自动恢复草稿', afterReload && Math.abs(afterReload.left - expect.left) < 0.5,
749
+ `期望 ${expect.left},${expect.top} 实得 ${afterReload?.left},${afterReload?.top}`);
750
+ check('刷新后恢复手动宽高状态', afterReload
751
+ && Math.abs(afterReload.width - expect.width) < 0.5
752
+ && Math.abs(afterReload.height - expect.height) < 0.5
753
+ && afterReload.widthLocked && afterReload.heightLocked,
754
+ `期望 ${expect.width.toFixed(1)}×${expect.height.toFixed(1)} 实得 ${afterReload?.width.toFixed(1)}×${afterReload?.height.toFixed(1)}`);
755
+ check('草稿提示条可见', await page.evaluate(() => !document.getElementById('hf-draft').hidden));
756
+
757
+ if (!fails && verifiedLayersHTML) {
758
+ try {
759
+ writeFileSync(layersOutput, verifiedLayersHTML);
760
+ check('已生成默认 layers.html', true, layersOutput);
761
+ } catch (error) {
762
+ check('已生成默认 layers.html', false, String(error));
763
+ }
764
+ }
765
+
766
+ if (errors.length) console.log('\n编辑器页面报错:', errors.join('; '));
767
+ } finally {
768
+ wired.cleanup();
769
+ await browser.close();
770
+ }
771
+
772
+ markTiming('完成');
773
+ if (showTimings) {
774
+ console.log('\n【分阶段计时】');
775
+ for (const row of timingRows) {
776
+ console.log(` TIMING\t${row.phase}\t${(row.ms / 1000).toFixed(3)}s`);
777
+ }
778
+ console.log(` TIMING\t总计\t${((Date.now() - timingStartedAt) / 1000).toFixed(3)}s`);
779
+ }
780
+
781
+ console.log(fails === 0 ? '\n全部通过 ✅\n' : `\n${fails} 项失败 ❌\n`);
782
+ process.exit(fails ? 1 : 0);