quiver-cli 1.3.1 → 2.0.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 (407) hide show
  1. package/README.md +197 -239
  2. package/dist/cli.js +3163 -2040
  3. package/package.json +10 -11
  4. package/template/.agents/AGENTS.md +0 -26
  5. package/template/.agents/commands/cp.md +0 -108
  6. package/template/.agents/commands/next-setup.md +0 -1064
  7. package/template/.agents/commands/review.md +0 -68
  8. package/template/.agents/commands/terraform-readme.md +0 -38
  9. package/template/.agents/config.json +0 -80
  10. package/template/.agents/plugins/opencode/rtk.ts +0 -34
  11. package/template/.agents/skills/agent-browser/SKILL.md +0 -52
  12. package/template/.agents/skills/apps/skybridge/SKILL.md +0 -52
  13. package/template/.agents/skills/code/cleanup/SKILL.md +0 -26
  14. package/template/.agents/skills/code/improve/SKILL.md +0 -122
  15. package/template/.agents/skills/code/improve/references/audit-playbook.md +0 -130
  16. package/template/.agents/skills/code/improve/references/closing-the-loop.md +0 -96
  17. package/template/.agents/skills/code/improve/references/plan-template.md +0 -197
  18. package/template/.agents/skills/code/vercel-react-best-practices/AGENTS.md +0 -3810
  19. package/template/.agents/skills/code/vercel-react-best-practices/README.md +0 -123
  20. package/template/.agents/skills/code/vercel-react-best-practices/SKILL.md +0 -149
  21. package/template/.agents/skills/code/vercel-react-best-practices/metadata.json +0 -15
  22. package/template/.agents/skills/code/vercel-react-best-practices/rules/_sections.md +0 -46
  23. package/template/.agents/skills/code/vercel-react-best-practices/rules/_template.md +0 -28
  24. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-effect-event-deps.md +0 -56
  25. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  26. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-init-once.md +0 -42
  27. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-use-latest.md +0 -39
  28. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-api-routes.md +0 -38
  29. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +0 -37
  30. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-defer-await.md +0 -82
  31. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-dependencies.md +0 -51
  32. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-parallel.md +0 -28
  33. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -99
  34. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-analyzable-paths.md +0 -63
  35. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -60
  36. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-conditional.md +0 -31
  37. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -49
  38. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  39. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-preload.md +0 -50
  40. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-event-listeners.md +0 -74
  41. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -71
  42. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -48
  43. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-swr-dedup.md +0 -56
  44. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -107
  45. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-cache-function-results.md +0 -80
  46. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-cache-property-access.md +0 -28
  47. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-cache-storage.md +0 -70
  48. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-combine-iterations.md +0 -32
  49. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-early-exit.md +0 -50
  50. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-flatmap-filter.md +0 -60
  51. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -45
  52. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-index-maps.md +0 -37
  53. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-length-check-first.md +0 -49
  54. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-min-max-loop.md +0 -82
  55. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-request-idle-callback.md +0 -105
  56. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -24
  57. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -57
  58. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-activity.md +0 -26
  59. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  60. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -40
  61. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -38
  62. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  63. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  64. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +0 -30
  65. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-resource-hints.md +0 -85
  66. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-script-defer-async.md +0 -68
  67. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -28
  68. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-usetransition-loading.md +0 -75
  69. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -39
  70. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-dependencies.md +0 -45
  71. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +0 -40
  72. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-derived-state.md +0 -29
  73. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -74
  74. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  75. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +0 -38
  76. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-memo.md +0 -44
  77. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +0 -45
  78. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-no-inline-components.md +0 -82
  79. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +0 -35
  80. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +0 -64
  81. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-transitions.md +0 -40
  82. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-use-deferred-value.md +0 -59
  83. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +0 -73
  84. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -73
  85. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-auth-actions.md +0 -96
  86. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-cache-lru.md +0 -41
  87. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-cache-react.md +0 -76
  88. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-dedup-props.md +0 -65
  89. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-hoist-static-io.md +0 -149
  90. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-no-shared-module-state.md +0 -50
  91. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -83
  92. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +0 -34
  93. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-serialization.md +0 -38
  94. package/template/.agents/skills/data/prisma-cli/SKILL.md +0 -265
  95. package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +0 -27
  96. package/template/.agents/skills/data/prisma-cli/references/complete.md +0 -22
  97. package/template/.agents/skills/data/prisma-cli/references/db-execute.md +0 -78
  98. package/template/.agents/skills/data/prisma-cli/references/db-pull.md +0 -185
  99. package/template/.agents/skills/data/prisma-cli/references/db-push.md +0 -150
  100. package/template/.agents/skills/data/prisma-cli/references/db-seed.md +0 -188
  101. package/template/.agents/skills/data/prisma-cli/references/debug.md +0 -46
  102. package/template/.agents/skills/data/prisma-cli/references/dev.md +0 -157
  103. package/template/.agents/skills/data/prisma-cli/references/format.md +0 -48
  104. package/template/.agents/skills/data/prisma-cli/references/generate.md +0 -173
  105. package/template/.agents/skills/data/prisma-cli/references/init.md +0 -139
  106. package/template/.agents/skills/data/prisma-cli/references/mcp.md +0 -39
  107. package/template/.agents/skills/data/prisma-cli/references/migrate-deploy.md +0 -127
  108. package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +0 -145
  109. package/template/.agents/skills/data/prisma-cli/references/migrate-diff.md +0 -89
  110. package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +0 -80
  111. package/template/.agents/skills/data/prisma-cli/references/migrate-resolve.md +0 -57
  112. package/template/.agents/skills/data/prisma-cli/references/migrate-status.md +0 -65
  113. package/template/.agents/skills/data/prisma-cli/references/studio.md +0 -137
  114. package/template/.agents/skills/data/prisma-cli/references/validate.md +0 -53
  115. package/template/.agents/skills/data/prisma-client-api/SKILL.md +0 -216
  116. package/template/.agents/skills/data/prisma-client-api/references/client-methods.md +0 -223
  117. package/template/.agents/skills/data/prisma-client-api/references/constructor.md +0 -221
  118. package/template/.agents/skills/data/prisma-client-api/references/filters.md +0 -256
  119. package/template/.agents/skills/data/prisma-client-api/references/model-queries.md +0 -281
  120. package/template/.agents/skills/data/prisma-client-api/references/query-options.md +0 -276
  121. package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +0 -198
  122. package/template/.agents/skills/data/prisma-client-api/references/relations.md +0 -308
  123. package/template/.agents/skills/data/prisma-client-api/references/transactions.md +0 -184
  124. package/template/.agents/skills/design/impeccable/SKILL.md +0 -84
  125. package/template/.agents/skills/design/impeccable/reference/adapt.md +0 -312
  126. package/template/.agents/skills/design/impeccable/reference/adapt.native.md +0 -58
  127. package/template/.agents/skills/design/impeccable/reference/android.md +0 -46
  128. package/template/.agents/skills/design/impeccable/reference/animate.md +0 -89
  129. package/template/.agents/skills/design/impeccable/reference/audit.md +0 -136
  130. package/template/.agents/skills/design/impeccable/reference/audit.native.md +0 -139
  131. package/template/.agents/skills/design/impeccable/reference/bolder.md +0 -33
  132. package/template/.agents/skills/design/impeccable/reference/clarify.md +0 -94
  133. package/template/.agents/skills/design/impeccable/reference/colorize.md +0 -86
  134. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +0 -44
  135. package/template/.agents/skills/design/impeccable/reference/craft.md +0 -5
  136. package/template/.agents/skills/design/impeccable/reference/critique.md +0 -806
  137. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +0 -37
  138. package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +0 -24
  139. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +0 -38
  140. package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +0 -92
  141. package/template/.agents/skills/design/impeccable/reference/delight.md +0 -70
  142. package/template/.agents/skills/design/impeccable/reference/distill.md +0 -111
  143. package/template/.agents/skills/design/impeccable/reference/doctor.md +0 -54
  144. package/template/.agents/skills/design/impeccable/reference/document.md +0 -416
  145. package/template/.agents/skills/design/impeccable/reference/extract.md +0 -69
  146. package/template/.agents/skills/design/impeccable/reference/harden.md +0 -336
  147. package/template/.agents/skills/design/impeccable/reference/hooks.md +0 -111
  148. package/template/.agents/skills/design/impeccable/reference/init.md +0 -131
  149. package/template/.agents/skills/design/impeccable/reference/ios.md +0 -51
  150. package/template/.agents/skills/design/impeccable/reference/layout.md +0 -84
  151. package/template/.agents/skills/design/impeccable/reference/live-setup.md +0 -102
  152. package/template/.agents/skills/design/impeccable/reference/live.md +0 -323
  153. package/template/.agents/skills/design/impeccable/reference/new-work.md +0 -145
  154. package/template/.agents/skills/design/impeccable/reference/onboard.md +0 -234
  155. package/template/.agents/skills/design/impeccable/reference/operate.md +0 -61
  156. package/template/.agents/skills/design/impeccable/reference/optimize.md +0 -258
  157. package/template/.agents/skills/design/impeccable/reference/overdrive.md +0 -127
  158. package/template/.agents/skills/design/impeccable/reference/polish.md +0 -105
  159. package/template/.agents/skills/design/impeccable/reference/quieter.md +0 -99
  160. package/template/.agents/skills/design/impeccable/reference/routing.md +0 -18
  161. package/template/.agents/skills/design/impeccable/reference/shape.md +0 -59
  162. package/template/.agents/skills/design/impeccable/reference/typeset.md +0 -80
  163. package/template/.agents/skills/design/impeccable/reference/visualize.md +0 -46
  164. package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +0 -1022
  165. package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +0 -94
  166. package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +0 -391
  167. package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +0 -513
  168. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +0 -814
  169. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +0 -325
  170. package/template/.agents/skills/design/impeccable/scripts/context.mjs +0 -1565
  171. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +0 -473
  172. package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +0 -121
  173. package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +0 -1
  174. package/template/.agents/skills/design/impeccable/scripts/detect-csp.mjs +0 -198
  175. package/template/.agents/skills/design/impeccable/scripts/detect.mjs +0 -30
  176. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +0 -2204
  177. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +0 -432
  178. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +0 -1311
  179. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +0 -9043
  180. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +0 -51
  181. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +0 -434
  182. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +0 -1319
  183. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +0 -1241
  184. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +0 -288
  185. package/template/.agents/skills/design/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +0 -189
  186. package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +0 -18
  187. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +0 -213
  188. package/template/.agents/skills/design/impeccable/scripts/detector/profile/profiler.mjs +0 -166
  189. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +0 -635
  190. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +0 -5681
  191. package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +0 -596
  192. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +0 -127
  193. package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +0 -30
  194. package/template/.agents/skills/design/impeccable/scripts/detector/shared/inline-ignores.mjs +0 -148
  195. package/template/.agents/skills/design/impeccable/scripts/detector/shared/page.mjs +0 -7
  196. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +0 -329
  197. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +0 -166
  198. package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +0 -457
  199. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +0 -447
  200. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +0 -819
  201. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +0 -538
  202. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +0 -2490
  203. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +0 -79
  204. package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +0 -93
  205. package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +0 -200
  206. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +0 -396
  207. package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +0 -880
  208. package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +0 -564
  209. package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +0 -130
  210. package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +0 -246
  211. package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +0 -306
  212. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +0 -640
  213. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +0 -137
  214. package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +0 -72
  215. package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +0 -37
  216. package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +0 -26
  217. package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +0 -281
  218. package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +0 -5
  219. package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +0 -194
  220. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +0 -369
  221. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +0 -485
  222. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +0 -169
  223. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +0 -533
  224. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +0 -149
  225. package/template/.agents/skills/design/impeccable/scripts/lib/target-args.mjs +0 -42
  226. package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +0 -33
  227. package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +0 -146
  228. package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +0 -617
  229. package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +0 -60
  230. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +0 -84
  231. package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +0 -28
  232. package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +0 -199
  233. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +0 -47
  234. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +0 -73
  235. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +0 -143
  236. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +0 -197
  237. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +0 -49
  238. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +0 -161
  239. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +0 -17
  240. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +0 -26
  241. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +0 -71
  242. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +0 -247
  243. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +0 -70
  244. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +0 -42
  245. package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +0 -149
  246. package/template/.agents/skills/design/impeccable/scripts/live/insert-ui.mjs +0 -458
  247. package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +0 -142
  248. package/template/.agents/skills/design/impeccable/scripts/live/manual-apply.mjs +0 -939
  249. package/template/.agents/skills/design/impeccable/scripts/live/manual-edit-routes.mjs +0 -357
  250. package/template/.agents/skills/design/impeccable/scripts/live/manual-edits-buffer.mjs +0 -152
  251. package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +0 -14
  252. package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +0 -139
  253. package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +0 -508
  254. package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +0 -563
  255. package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +0 -105
  256. package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +0 -105
  257. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +0 -969
  258. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +0 -1366
  259. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +0 -304
  260. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +0 -259
  261. package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +0 -75
  262. package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +0 -171
  263. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +0 -938
  264. package/template/.agents/skills/design/impeccable/scripts/live-browser-dom.js +0 -146
  265. package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +0 -242
  266. package/template/.agents/skills/design/impeccable/scripts/live-browser-session.js +0 -123
  267. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +0 -12551
  268. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +0 -1200
  269. package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +0 -107
  270. package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +0 -800
  271. package/template/.agents/skills/design/impeccable/scripts/live-discard-manual-edits.mjs +0 -51
  272. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +0 -463
  273. package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +0 -292
  274. package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +0 -368
  275. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +0 -430
  276. package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +0 -123
  277. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +0 -1698
  278. package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +0 -71
  279. package/template/.agents/skills/design/impeccable/scripts/live-target.mjs +0 -30
  280. package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +0 -927
  281. package/template/.agents/skills/design/impeccable/scripts/live.mjs +0 -334
  282. package/template/.agents/skills/design/impeccable/scripts/modern-screenshot.umd.js +0 -14
  283. package/template/.agents/skills/design/impeccable/scripts/palette.mjs +0 -628
  284. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +0 -224
  285. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +0 -1783
  286. package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +0 -74
  287. package/template/.agents/skills/design/shadcn/SKILL.md +0 -277
  288. package/template/.agents/skills/design/shadcn/agents/openai.yml +0 -5
  289. package/template/.agents/skills/design/shadcn/assets/shadcn-small.png +0 -0
  290. package/template/.agents/skills/design/shadcn/assets/shadcn.png +0 -0
  291. package/template/.agents/skills/design/shadcn/cli.md +0 -290
  292. package/template/.agents/skills/design/shadcn/customization.md +0 -209
  293. package/template/.agents/skills/design/shadcn/evals/evals.json +0 -77
  294. package/template/.agents/skills/design/shadcn/mcp.md +0 -105
  295. package/template/.agents/skills/design/shadcn/registry.md +0 -277
  296. package/template/.agents/skills/design/shadcn/rules/base-vs-radix.md +0 -306
  297. package/template/.agents/skills/design/shadcn/rules/chat.md +0 -250
  298. package/template/.agents/skills/design/shadcn/rules/composition.md +0 -213
  299. package/template/.agents/skills/design/shadcn/rules/forms.md +0 -192
  300. package/template/.agents/skills/design/shadcn/rules/icons.md +0 -101
  301. package/template/.agents/skills/design/shadcn/rules/styling.md +0 -185
  302. package/template/.agents/skills/find-skills/SKILL.md +0 -141
  303. package/template/.agents/skills/hono/SKILL.md +0 -593
  304. package/template/.agents/skills/integrations/langfuse/SKILL.md +0 -150
  305. package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +0 -41
  306. package/template/.agents/skills/integrations/langfuse/references/cli.md +0 -60
  307. package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +0 -35
  308. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +0 -87
  309. package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +0 -121
  310. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +0 -293
  311. package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +0 -35
  312. package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +0 -79
  313. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -34
  314. package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +0 -65
  315. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +0 -35
  316. package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +0 -76
  317. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +0 -66
  318. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +0 -75
  319. package/template/.agents/skills/integrations/posthog/SKILL.md +0 -102
  320. package/template/.agents/skills/integrations/posthog/references/error-tracking-alerts.md +0 -63
  321. package/template/.agents/skills/integrations/posthog/references/error-tracking-assigning-issues.md +0 -77
  322. package/template/.agents/skills/integrations/posthog/references/error-tracking-fingerprints.md +0 -57
  323. package/template/.agents/skills/integrations/posthog/references/error-tracking-monitoring.md +0 -140
  324. package/template/.agents/skills/integrations/posthog/references/error-tracking-nextjs.md +0 -490
  325. package/template/.agents/skills/integrations/posthog/references/error-tracking-source-maps.md +0 -45
  326. package/template/.agents/skills/integrations/posthog/references/feature-flags-best-practices.md +0 -139
  327. package/template/.agents/skills/integrations/posthog/references/feature-flags-react.md +0 -302
  328. package/template/.agents/skills/integrations/posthog/references/identify-users.md +0 -202
  329. package/template/.agents/skills/integrations/posthog/references/integration-example.md +0 -706
  330. package/template/.agents/skills/integrations/posthog/references/integration-nextjs.md +0 -385
  331. package/template/.agents/skills/integrations/posthog/references/integration-step-1-begin.md +0 -43
  332. package/template/.agents/skills/integrations/posthog/references/integration-step-2-edit.md +0 -37
  333. package/template/.agents/skills/integrations/posthog/references/integration-step-3-revise.md +0 -22
  334. package/template/.agents/skills/integrations/posthog/references/integration-step-4-conclude.md +0 -38
  335. package/template/.agents/skills/integrations/posthog/references/llm-analytics-anthropic.md +0 -200
  336. package/template/.agents/skills/integrations/posthog/references/llm-analytics-basics.md +0 -62
  337. package/template/.agents/skills/integrations/posthog/references/llm-analytics-costs.md +0 -197
  338. package/template/.agents/skills/integrations/posthog/references/llm-analytics-manual-capture.md +0 -397
  339. package/template/.agents/skills/integrations/posthog/references/llm-analytics-traces.md +0 -98
  340. package/template/.agents/skills/integrations/posthog/references/llm-analytics-vercel-ai.md +0 -120
  341. package/template/.agents/skills/repo/repo-ci/SKILL.md +0 -265
  342. package/template/.agents/skills/repo/repo-init-next-js/SKILL.md +0 -129
  343. package/template/.agents/skills/repo/repo-init-next-js/references/file-contents.md +0 -800
  344. package/template/.agents/skills/repo/repo-init-next-js/scripts/setup.sh +0 -47
  345. package/template/.agents/skills/repo/repo-init-node/SKILL.md +0 -196
  346. package/template/.agents/skills/skill-creator/LICENSE.txt +0 -202
  347. package/template/.agents/skills/skill-creator/SKILL.md +0 -485
  348. package/template/.agents/skills/skill-creator/agents/analyzer.md +0 -274
  349. package/template/.agents/skills/skill-creator/agents/comparator.md +0 -202
  350. package/template/.agents/skills/skill-creator/agents/grader.md +0 -223
  351. package/template/.agents/skills/skill-creator/assets/eval_review.html +0 -146
  352. package/template/.agents/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  353. package/template/.agents/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  354. package/template/.agents/skills/skill-creator/references/schemas.md +0 -430
  355. package/template/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  356. package/template/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  357. package/template/.agents/skills/skill-creator/scripts/generate_report.py +0 -326
  358. package/template/.agents/skills/skill-creator/scripts/improve_description.py +0 -247
  359. package/template/.agents/skills/skill-creator/scripts/package_skill.py +0 -136
  360. package/template/.agents/skills/skill-creator/scripts/quick_validate.py +0 -103
  361. package/template/.agents/skills/skill-creator/scripts/run_eval.py +0 -310
  362. package/template/.agents/skills/skill-creator/scripts/run_loop.py +0 -328
  363. package/template/.agents/skills/skill-creator/scripts/utils.py +0 -47
  364. package/template/.agents/skills/supabase/CHANGELOG.md +0 -78
  365. package/template/.agents/skills/supabase/SKILL.md +0 -149
  366. package/template/.agents/skills/supabase/assets/feedback-issue-template.md +0 -17
  367. package/template/.agents/skills/supabase/references/skill-feedback.md +0 -17
  368. package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +0 -73
  369. package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +0 -64
  370. package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +0 -170
  371. package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +0 -39
  372. package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +0 -34
  373. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +0 -55
  374. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +0 -49
  375. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +0 -46
  376. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +0 -44
  377. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +0 -41
  378. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +0 -46
  379. package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +0 -54
  380. package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +0 -53
  381. package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +0 -50
  382. package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +0 -50
  383. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +0 -56
  384. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +0 -68
  385. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +0 -50
  386. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +0 -54
  387. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +0 -45
  388. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +0 -55
  389. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +0 -55
  390. package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +0 -44
  391. package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +0 -40
  392. package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +0 -48
  393. package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +0 -43
  394. package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +0 -45
  395. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +0 -80
  396. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +0 -46
  397. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +0 -59
  398. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +0 -55
  399. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +0 -55
  400. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +0 -61
  401. package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +0 -54
  402. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +0 -50
  403. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +0 -63
  404. package/template/.agents/skills/writing/humanizer/LICENSE +0 -21
  405. package/template/.agents/skills/writing/humanizer/README.md +0 -209
  406. package/template/.agents/skills/writing/humanizer/SKILL.md +0 -456
  407. package/template/.agents/upstreams.json +0 -115
@@ -1,1022 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * build-phase: the comp-led build as a state machine on disk, so the phases
4
- * new-work.md names are gated by scripts instead of remembered by the model.
5
- *
6
- * State lives at .impeccable/build/state.json. Phases, in order:
7
- *
8
- * comps the comp round: three comps of the chosen direction under
9
- * .impeccable/mocks/ with prompt sidecars, one approved by the
10
- * user (sidecar "approved": true). Skipped when start names an
11
- * approved --comp (a surface round already locked one).
12
- * spec the approved comp is measured (comp-spec.mjs wrote spec.json)
13
- * plates every raster region in the spec has its plate on disk
14
- * hero the first viewport is reproduced: comp-diff of hero-repro.png
15
- * against the comp clears the gate
16
- * sections the rest of the surface is built inside the spec's system
17
- * motion interaction, reveals, motion
18
- * responsive the other viewports
19
- * review the finish reviewer ran; disposition recorded
20
- *
21
- * node build-phase.mjs start --comp <approved.png> [--breakpoint 1440x900] [--artifact index.html]
22
- * node build-phase.mjs start --direction <seed key> # no comp yet: opens the comps phase first
23
- * node build-phase.mjs status # human-readable, plus NEXT line
24
- * node build-phase.mjs status --json
25
- * node build-phase.mjs advance # try to close the current phase; runs its gate
26
- * node build-phase.mjs advance --force --reason "<why>" # skip a gate; recorded, never silent
27
- * node build-phase.mjs record hero --build .impeccable/review/hero-repro.png # run the hero gate explicitly
28
- * node build-phase.mjs note "<text>" # append a note to the current phase
29
- * node build-phase.mjs finish --disposition ship|fix|rebuild|recapture
30
- *
31
- * Gates:
32
- * comps -> >= 3 comp rasters (png/webp/jpg) directly under
33
- * .impeccable/mocks/ (decision/ excluded), each with a .json
34
- * sidecar, and exactly one sidecar carrying "approved": true;
35
- * closing records that file as the state's comp.
36
- * spec -> spec.json exists and has >= 1 region
37
- * plates -> every region with medium raster has its plate file, decodable,
38
- * at least 1.5x the comp region's pixel width (textures
39
- * exempt), and reads as the region against the masked comp
40
- * crop: structure >= PLATE_STRUCTURE_MIN and comp-diff overall
41
- * >= PLATE_MIN (textures: palette + grain only). Structure is
42
- * the floor because it is what a wrong-but-busy plate cannot
43
- * fake: noise, a mirror, a mosaic, another region all keep
44
- * the palette and the energy and lose structure. A missing
45
- * or thin plate names itself.
46
- * hero -> every plate is referenced by a source file (the artifact
47
- * named at start, else a bounded walk of the project), and
48
- * .impeccable/review/hero-repro.png exists and comp-diff overall
49
- * >= HERO_MIN (default 0.72) with no region `missing`. The
50
- * score, the report path, and the attempt count are recorded.
51
- * responsive -> .impeccable/review/desktop.png and mobile.png exist, and
52
- * the desktop capture still scores >= RESPONSIVE_MIN against
53
- * the comp with no region missing: a first viewport that only
54
- * holds at the comp's exact width is not built.
55
- * sections / motion -> no mechanical gate; advancing records the moment,
56
- * and the finish reviewer reads the timeline.
57
- *
58
- * Exit codes: 0 ok / advanced, 2 gate failed (state unchanged, reasons
59
- * printed), 1 usage.
60
- *
61
- * Nothing here needs a browser. Screenshots come from the harness; this
62
- * script only measures them.
63
- */
64
- import fs from 'node:fs';
65
- import path from 'node:path';
66
- import { spawnSync } from 'node:child_process';
67
- import { fileURLToPath } from 'node:url';
68
- import { createRequire } from 'node:module';
69
- import { decodePng, loadRaster } from './lib/png.mjs';
70
- const require = createRequire(import.meta.url);
71
- import { crop, createImage, blit, resize } from './lib/raster.mjs';
72
- import { structureScore } from './lib/image-metrics.mjs';
73
- import { compare, verdictFor, alignBuild, bestShift } from './comp-diff.mjs';
74
- import { textRegionCheck, chromeStripCheck, inventedInk, plateClipCheck, svgIllustrations } from './lib/hero-checks.mjs';
75
- import { SPEC_PATH, BUILD_DIR, loadSpec, plateReference } from './comp-spec.mjs';
76
- import { choiceStamped } from './font-match.mjs';
77
-
78
- const HERE = path.dirname(fileURLToPath(import.meta.url));
79
- export const STATE_PATH = path.join(BUILD_DIR, 'state.json');
80
- export const PHASES = ['comps', 'spec', 'plates', 'hero', 'sections', 'motion', 'responsive', 'review'];
81
- export const MOCKS_DIR = path.join('.impeccable', 'mocks');
82
- export const HERO_MIN = 0.72;
83
- export const RESPONSIVE_MIN = 0.65;
84
- export const PLATE_MIN = 0.4;
85
- export const PLATE_STRUCTURE_MIN = 0.4;
86
- export const HERO_REPRO = path.join('.impeccable', 'review', 'hero-repro.png');
87
-
88
- function arg(name, fallback = null) {
89
- const i = process.argv.indexOf(`--${name}`);
90
- if (i === -1) return fallback;
91
- const v = process.argv[i + 1];
92
- return v && !v.startsWith('--') ? v : fallback;
93
- }
94
- const flag = (name) => process.argv.includes(`--${name}`);
95
- const now = () => new Date().toISOString();
96
-
97
- /** The recorded build path (config.local.json over config.json), or null. */
98
- export function readBuildPath(cwd = process.cwd()) {
99
- let value = null;
100
- for (const name of ['config.json', 'config.local.json']) {
101
- try {
102
- const raw = JSON.parse(fs.readFileSync(path.join(cwd, '.impeccable', name), 'utf8'));
103
- if (raw?.buildPath === 'comp' || raw?.buildPath === 'code') value = raw.buildPath;
104
- } catch { /* absent */ }
105
- }
106
- return value;
107
- }
108
-
109
- /**
110
- * Whether a direction was dealt and the build never started, or started and
111
- * stopped before the hero gate: the condition context.mjs and detect.mjs
112
- * report as COMP_ROUND_OPEN when page code exists. Returns null when the
113
- * build path is code-led (no round owed) or nothing is pending.
114
- */
115
- export function compRoundOpen(cwd = process.cwd()) {
116
- const buildPath = readBuildPath(cwd);
117
- if (buildPath === 'code') return null;
118
- const pending = path.join(cwd, BUILD_DIR, 'pending.json');
119
- const statePath = path.join(cwd, STATE_PATH);
120
- if (fs.existsSync(pending) && !fs.existsSync(statePath)) return { reason: 'a direction was chosen (concept-seed rolled) but build-phase.mjs start never ran', pending };
121
- if (fs.existsSync(statePath)) {
122
- try {
123
- const st = JSON.parse(fs.readFileSync(statePath, 'utf8'));
124
- const idx = PHASES.indexOf(st.phase);
125
- if (idx !== -1 && idx <= PHASES.indexOf('hero') && st.phases?.comps?.status !== 'skipped' && st.phases?.comps?.status !== 'closed') return { reason: `build-phase is at ${st.phase}; the comps phase never closed`, state: statePath };
126
- if (idx !== -1 && idx <= PHASES.indexOf('hero')) return { reason: `build-phase is at ${st.phase}; the hero gate has not passed`, state: statePath };
127
- } catch { /* unreadable: say nothing */ }
128
- }
129
- return null;
130
- }
131
-
132
- export function loadState(statePath = STATE_PATH) {
133
- if (!fs.existsSync(statePath)) return null;
134
- return JSON.parse(fs.readFileSync(statePath, 'utf8'));
135
- }
136
-
137
- export function saveState(state, statePath = STATE_PATH) {
138
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
139
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
140
- }
141
-
142
- export function newState({ comp = null, breakpoint = null, artifact = null, direction = null }) {
143
- const first = comp ? 'spec' : 'comps';
144
- const phases = Object.fromEntries(PHASES.map((p) => [p, { status: p === first ? 'open' : 'pending', openedAt: p === first ? now() : null, closedAt: null, attempts: 0, notes: [], gate: null, forced: null }]));
145
- if (comp) { phases.comps.status = 'skipped'; phases.comps.notes.push({ at: now(), text: 'started with an approved comp; the comp round happened before this state (surface round or manual)' }); }
146
- return {
147
- tool: 'build-phase',
148
- version: 2,
149
- startedAt: now(),
150
- comp,
151
- direction,
152
- breakpoint,
153
- artifact,
154
- phase: first,
155
- phases,
156
- finish: null,
157
- };
158
- }
159
-
160
- // ---- gates -----------------------------------------------------------------
161
-
162
- /** Comp rasters directly under the mocks dir, with their sidecars. */
163
- export function listComps(mocksDir = MOCKS_DIR) {
164
- if (!fs.existsSync(mocksDir)) return [];
165
- const out = [];
166
- for (const name of fs.readdirSync(mocksDir)) {
167
- if (!/\.(png|webp|jpe?g)$/i.test(name)) continue;
168
- const file = path.join(mocksDir, name);
169
- if (!fs.statSync(file).isFile()) continue;
170
- const sidecarPath = `${file}.json`;
171
- let sidecar = null;
172
- if (fs.existsSync(sidecarPath)) { try { sidecar = JSON.parse(fs.readFileSync(sidecarPath, 'utf8')); } catch { sidecar = null; } }
173
- out.push({ file, sidecarPath, sidecar, approved: !!(sidecar && sidecar.approved === true) });
174
- }
175
- return out;
176
- }
177
-
178
- export function gateComps(state, { mocksDir = MOCKS_DIR } = {}) {
179
- const comps = listComps(mocksDir);
180
- const reasons = [];
181
- if (comps.length < 3) reasons.push(`${comps.length} comp${comps.length === 1 ? '' : 's'} under ${mocksDir}; the comp round puts three compositional options of the chosen direction in front of the user (reference/visualize.md). Generate the missing ones (harness image tool or generate-image.mjs), each with a .json sidecar holding its prompt.`);
182
- const noSidecar = comps.filter((c) => !c.sidecar);
183
- if (noSidecar.length) reasons.push(`no prompt sidecar for: ${noSidecar.map((c) => path.basename(c.file)).join(', ')} (write <file>.json with { "prompt": "..." }; generate-image.mjs does this itself)`);
184
- const approved = comps.filter((c) => c.approved);
185
- if (approved.length === 0) reasons.push('no comp is approved: put the three comps in front of the user (decision page via serve-question.mjs, or the structured question tool), then set "approved": true in the chosen comp\'s sidecar. A delegated pick is recorded the same way and disclosed.');
186
- if (approved.length > 1) reasons.push(`${approved.length} comps carry "approved": true; exactly one is the approved comp: ${approved.map((c) => path.basename(c.file)).join(', ')}`);
187
- return { ok: reasons.length === 0, reasons, summary: `${comps.length} comps, ${approved.length} approved`, approved: approved.length === 1 ? approved[0].file : null };
188
- }
189
-
190
- export function gateSpec(state, { specPath = SPEC_PATH } = {}) {
191
- const spec = loadSpec(specPath);
192
- if (!spec) return { ok: false, reasons: [`no spec at ${specPath}: run comp-spec.mjs --comp ${state.comp} --grid, name the regions, then --regions regions.json`] };
193
- if (!spec.regions || spec.regions.length < 1) return { ok: false, reasons: ['spec has no regions'] };
194
- if (spec.comp && state.comp && path.resolve(spec.comp) !== path.resolve(state.comp)) {
195
- return { ok: false, reasons: [`spec measures ${spec.comp}, but this build started on ${state.comp}; re-run comp-spec on the approved comp`] };
196
- }
197
- const plates = spec.regions.filter((r) => r.medium === 'raster').length;
198
- // A plate box that cuts its artwork is a plate the page will crop.
199
- const cut = spec.regions.filter((r) => r.medium === 'raster' && r.clipped && r.clipped.length);
200
- if (cut.length) return { ok: false, reasons: cut.map((r) => `region ${r.id}: the comp's artwork runs off its box on the ${r.clipped.join(' and ')}; widen the region's span so the box holds the whole shape with a margin (or set "bleed": true if the page really crops it there), then re-run comp-spec.mjs --regions`) };
201
- // Type is measured, not guessed: the largest text region must carry a
202
- // font-match measurement and a ranked choice before page code exists.
203
- // Three of six misses a human called on a first-round build were the
204
- // headline face wider and lighter than the comp's, the parts list smaller,
205
- // the footer heavier: ratios font-match reads off pixels.
206
- // The lead text region is the display type: the region whose measured cap
207
- // height is largest (a headline), not the biggest box (a table of rows).
208
- // Unmeasured regions sort by box height as a proxy until measured.
209
- const textRegions = spec.regions.filter((r) => r.kind === 'text').sort((a, b) => {
210
- const ca = a.type && a.type.comp ? a.type.comp.capHeightPx : a.px.h * 0.4;
211
- const cb = b.type && b.type.comp ? b.type.comp.capHeightPx : b.px.h * 0.4;
212
- return cb - ca;
213
- });
214
- const reasons = [];
215
- if (textRegions.length) {
216
- // when nothing is measured yet, ask for all measurements first; the lead
217
- // is only knowable once cap heights exist
218
- const anyMeasured = textRegions.some((r) => r.type);
219
- if (!anyMeasured) {
220
- reasons.push(`measure the type before closing the spec: node ${HERE}/font-match.mjs --measure <id> for each text region (${textRegions.map((r) => r.id).join(', ')}); the region with the largest cap height is the lead and gets --rank.`);
221
- return { ok: false, reasons };
222
- }
223
- const measurable = textRegions.filter((r) => r.type && r.type.comp);
224
- const lead = measurable[0] || textRegions[0];
225
- if (!lead.type) reasons.push(`the lead text region ${lead.id} has no type measurement: run node ${HERE}/font-match.mjs --measure ${lead.id} (and --rank ${lead.id} --text "<its first words>" to choose the face by metrics). Set font-size from the printed cap height; do not pick a face by name.`);
226
- else if (lead.type.comp && !lead.type.chosen) reasons.push(`the lead text region ${lead.id} is measured (${lead.type.widthClass} ${lead.type.weightClass}, cap ${lead.type.comp.capHeightPx}px) but no face is ranked: run node ${HERE}/font-match.mjs --rank ${lead.id} --text "<its first words>" [--candidates "Family:weight,..."] and use the USE line.`);
227
- else if (lead.type.comp && lead.type.chosen && !choiceStamped(lead.id, lead.type.chosen)) reasons.push(`the lead text region ${lead.id} carries a "chosen" face that font-match did not write (${lead.type.chosen.family || '?'}). A face typed into spec.json is the guess this gate exists to refuse; run node ${HERE}/font-match.mjs --rank ${lead.id} --text "<its first words>" and let it record the choice (with no browser it records the catalog's nearest face).`);
228
- const unmeasured = textRegions.slice(1).filter((r) => !r.type).map((r) => r.id);
229
- if (unmeasured.length && !reasons.length) reasons.push(`measure the other text regions too, each sets its own font-size and weight class: node ${HERE}/font-match.mjs --measure <id> for ${unmeasured.join(', ')}`);
230
- }
231
- if (reasons.length) return { ok: false, reasons };
232
- return { ok: true, reasons: [], summary: `${spec.regions.length} regions, ${plates} plates, ${textRegions.length} text regions measured` };
233
- }
234
-
235
- /**
236
- * The one plate rule, shared by the plates gate and generate-image's
237
- * PLATE-WARN so they never disagree. `score` is compare().whole against the
238
- * masked comp crop under cover alignment.
239
- */
240
- export function plateVerdict(region, score) {
241
- const isTexture = region.kind === 'texture';
242
- const reasons = [];
243
- if (isTexture) {
244
- const effective = 0.5 * score.color + 0.5 * Math.min(1, score.detail / 0.6);
245
- if (effective < PLATE_MIN) reasons.push(`scores ${(effective * 100).toFixed(0)}% as the material of region ${region.id} (color ${(score.color * 100).toFixed(0)}%, detail ${(score.detail * 100).toFixed(0)}%); crop a clean patch of the comp region (comp-spec.mjs --crop ${region.id} --raw) and mirror-tile it, generate only when no clean patch exists`);
246
- return { ok: reasons.length === 0, reasons, effective };
247
- }
248
- // Added detail is invented material only when the comp region is calm;
249
- // a paper sleeve is grainy in the comp too, and its plate is allowed the
250
- // same grain. Comp energy travels on the region (comp-spec's detail.energy).
251
- const compCalm = !region.detail || region.detail.energy < 12;
252
- if (compCalm && score.detailAdded > 0.45) reasons.push(`carries detail the comp region ${region.id} does not have (added-detail ${(score.detailAdded * 100).toFixed(0)}% of cells): noise, grain, or a busier subject where the comp is calm; regenerate from the crop reference without adding texture`);
253
- if (score.structure < PLATE_STRUCTURE_MIN) reasons.push(`structure ${(score.structure * 100).toFixed(0)}% against the comp region ${region.id}: the composition of the plate is not the region's (different subject, orientation, or crop); regenerate with comp-spec.mjs --crop ${region.id} as the reference image`);
254
- if (score.overall < PLATE_MIN) reasons.push(`scores ${(score.overall * 100).toFixed(0)}% against the comp region ${region.id} (structure ${(score.structure * 100).toFixed(0)}%, color ${(score.color * 100).toFixed(0)}%, detail ${(score.detail * 100).toFixed(0)}%); regenerate with the crop as --ref and the comp-spec plate prompt`);
255
- return { ok: reasons.length === 0, reasons, effective: score.overall };
256
- }
257
-
258
- export function gatePlates(state, { specPath = SPEC_PATH } = {}) {
259
- const spec = loadSpec(specPath);
260
- if (!spec) return { ok: false, reasons: ['no spec'] };
261
- const rasterRegions = spec.regions.filter((r) => r.medium === 'raster');
262
- if (!rasterRegions.length) return { ok: true, reasons: [], summary: 'no plates owed', plates: [] };
263
- let comp = null;
264
- try { comp = loadRaster(spec.comp).image; } catch { /* scored without the comp crop below */ }
265
- const reasons = [], plates = [];
266
- for (const r of rasterRegions) {
267
- const file = r.plate;
268
- if (!file || !fs.existsSync(file)) { reasons.push(`plate missing for ${r.id}: expected ${file || '(no path)'}; produce it from comp-spec.mjs --crop ${r.id} with generate-image.mjs --plate`); plates.push({ id: r.id, file, status: 'missing' }); continue; }
269
- let img;
270
- try { img = decodePng(fs.readFileSync(file)); } catch (e) { reasons.push(`plate ${file} is not a decodable PNG: ${e.message}`); plates.push({ id: r.id, file, status: 'unreadable' }); continue; }
271
- // A texture tiles, so it owes no size floor and no structural match:
272
- // it is judged on palette and grain only. Every other plate must be at
273
- // least 1.5x the region (capped at 1536px, the largest size the
274
- // generators emit; past that the region is a full-bleed field the page
275
- // scales) and read as the region under object-fit: cover.
276
- const isTexture = r.kind === 'texture';
277
- const minW = Math.min(1536, r.px.w * 1.5);
278
- if (!isTexture && img.width < minW) reasons.push(`plate ${file} is ${img.width}px wide; the comp region is ${r.px.w}px and a shipping plate needs at least ${Math.round(minW)}px. Regenerate at asset size, do not crop the comp.`);
279
- let score = null;
280
- if (comp) {
281
- const ref = plateReference(comp, spec, r);
282
- // a keyed (alpha) plate ships over the page ground: score it composited
283
- // over the region's sampled ground, the way it will show
284
- let build = img;
285
- let transparent = 0; for (let i = 3; i < img.data.length; i += 4) if (img.data[i] < 128) transparent++;
286
- if (transparent > (img.data.length / 4) * 0.05) {
287
- const g = (r.palette && r.palette[0] && r.palette[0].hex) || '#ffffff';
288
- const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(g);
289
- const ground = m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16), 255] : [255, 255, 255, 255];
290
- const over = createImage(img.width, img.height, ground);
291
- blit(over, img, 0, 0);
292
- build = over;
293
- }
294
- const res = compare({ comp: ref, build, align: 'cover', spec: null, kind: r.kind });
295
- score = res.whole;
296
- const v = plateVerdict(r, score);
297
- for (const reason of v.reasons) reasons.push(`plate ${file}: ${reason}`);
298
- // A plate that matches the comp crop almost exactly is the comp crop,
299
- // upscaled past the size floor: the comp's grain, its neighbours'
300
- // edges, and its resolution ship as the artwork. Crops are never
301
- // plates; the crop is the reference the plate is generated from.
302
- // A fake-mode plate (offline pipelines, eval fixtures) IS the crop by
303
- // design and says so in its tEXt; the identity refusal is for models
304
- // shipping the comp's pixels as artwork, not for the deterministic
305
- // stand-in.
306
- const isFake = img.text && img.text['impeccable:fake'] === '1';
307
- if (!isTexture && !isFake) {
308
- const raw = crop(comp, r.px.x, r.px.y, r.px.w, r.px.h);
309
- const same = structureScore(raw, resize(img, raw.width, raw.height));
310
- if (same >= 0.95) reasons.push(`plate ${file} is the comp crop of region ${r.id} (structure ${(same * 100).toFixed(0)}% against the raw region, a resample of the same pixels): a crop of the comp is never a plate; generate the plate from the crop as reference (generate-image.mjs --plate ${r.id})`);
311
- }
312
- }
313
- plates.push({ id: r.id, file, status: 'ok', size: `${img.width}x${img.height}`, score: score ? score.overall : null });
314
- }
315
- return { ok: reasons.length === 0, reasons, summary: `${plates.filter((p) => p.status === 'ok').length}/${rasterRegions.length} plates`, plates };
316
- }
317
-
318
- /** Source files that could reference a plate: bounded walk, skipping deps and build output. */
319
- function sourceFiles(root = '.', limit = 400) {
320
- const out = [];
321
- // `assets` is walked: the extension filter already keeps binaries out, and
322
- // a stylesheet at assets/hero.css may be the one file that references a
323
- // plate (Greptile P1 on #599: the gate refused a valid build for it).
324
- const skip = new Set(['node_modules', '.git', 'dist', 'build', 'out', '.next', '.svelte-kit', '.impeccable', 'coverage']);
325
- const exts = /\.(html?|css|scss|jsx?|tsx?|svelte|vue|astro|mdx?|php|erb|hbs)$/i;
326
- const walk = (dir, depth) => {
327
- if (out.length >= limit || depth > 6) return;
328
- let entries = [];
329
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
330
- for (const e of entries) {
331
- if (out.length >= limit) return;
332
- if (e.isDirectory()) { if (!skip.has(e.name) && !e.name.startsWith('.')) walk(path.join(dir, e.name), depth + 1); }
333
- else if (exts.test(e.name)) out.push(path.join(dir, e.name));
334
- }
335
- };
336
- walk(root, 0);
337
- return out;
338
- }
339
-
340
- /** Plates the artifact never references: a plate on disk that no source names ships nothing. */
341
- export function unreferencedPlates(spec, artifact = null) {
342
- const plates = (spec?.regions || []).filter((r) => r.medium === 'raster' && r.plate);
343
- if (!plates.length) return [];
344
- // The artifact narrows nothing: a plate may be referenced only from a
345
- // stylesheet the artifact links, so the source walk runs either way, the
346
- // artifact keeps a seat in the corpus, and the stylesheets it links are
347
- // added by name (resolved against the artifact's own directory), which
348
- // covers a stylesheet outside the walk's root, depth, or file limit.
349
- const linked = [];
350
- if (artifact && fs.existsSync(artifact)) {
351
- linked.push(artifact);
352
- try {
353
- const html = fs.readFileSync(artifact, 'utf8');
354
- for (const m of html.matchAll(/<link\b[^>]*href=["']([^"']+)["'][^>]*>/gi)) {
355
- const href = m[1];
356
- if (/^(https?:|data:|\/\/)/i.test(href)) continue;
357
- if (!/rel=["']?stylesheet/i.test(m[0]) && !/\.css(\?|$)/i.test(href)) continue;
358
- const clean = href.split('?')[0];
359
- // a root-relative href serves from the project root, not the drive
360
- // root: try the working directory and the artifact's own directory
361
- // (unreadable candidates are skipped by the corpus loop)
362
- if (clean.startsWith('/')) { linked.push(path.join(process.cwd(), clean), path.join(path.dirname(artifact), clean)); }
363
- else linked.push(path.resolve(path.dirname(artifact), clean));
364
- }
365
- } catch { /* the artifact still counts */ }
366
- }
367
- const files = [...new Set([...linked, ...sourceFiles()])];
368
- let corpus = '';
369
- for (const f of files) { try { corpus += fs.readFileSync(f, 'utf8') + '\n'; } catch { /* skip */ } }
370
- const missing = [];
371
- for (const r of plates) {
372
- const base = path.basename(r.plate);
373
- const stem = base.replace(/\.[a-z0-9]+$/i, '');
374
- // a data URI inline copy counts when the region id or file stem is named beside it
375
- if (corpus.includes(base) || (corpus.includes('data:image/') && (corpus.includes(stem) || corpus.includes(r.id)))) continue;
376
- missing.push(r);
377
- }
378
- return missing;
379
- }
380
-
381
- /** Organic clip-path findings whose selector's element the artifact places (by class/id name) on a raster region. Cheap heuristic: the finding's selector or the surrounding rule mentions the region id or its plate stem. */
382
- export function organicClipRegions(artifactFile, spec) {
383
- let scan;
384
- try {
385
- const mod = require(path.join(HERE, '..', '..', 'cli', 'engine', 'rules', 'checks.mjs'));
386
- scan = mod.scanCssTextForOrganicClipPath;
387
- } catch { scan = null; }
388
- if (!scan) return [];
389
- let html = '';
390
- try { html = fs.readFileSync(artifactFile, 'utf8'); } catch { return []; }
391
- const findings = scan(html);
392
- if (!findings.length) return [];
393
- const rasterRegions = (spec.regions || []).filter((r) => r.medium === 'raster');
394
- const out = [];
395
- for (const f of findings) {
396
- const sel = String(f.selector || '').toLowerCase();
397
- for (const r of rasterRegions) {
398
- const stem = path.basename(r.plate || '', path.extname(r.plate || '')).toLowerCase();
399
- if ((sel && (sel.includes(r.id.toLowerCase()) || (stem && sel.includes(stem)))) || rasterRegions.length === 1) { out.push({ id: r.id, snippet: f.snippet }); break; }
400
- }
401
- }
402
- return out;
403
- }
404
-
405
- /**
406
- * The scaffold: the measured layout as CSS custom properties and one
407
- * reference page. Positions in % of the comp so the frame scales; plates at
408
- * their boxes with object-fit: contain (never cover: cover is how the arch
409
- * lost its left side); text slots sized from the measured cap height (cap /
410
- * 0.70 as the em estimate when font-match gave no size) in the ranked face.
411
- * A reference for a builder that cannot lay out to a box, and a check for
412
- * one that can; the gate reads pixels either way.
413
- */
414
- export function writeScaffold(spec, state, { dir = path.join(BUILD_DIR, 'scaffold') } = {}) {
415
- fs.mkdirSync(dir, { recursive: true });
416
- const W = spec.compSize.width, H = spec.compSize.height;
417
- const pct = (v) => `${(v * 100).toFixed(3)}%`;
418
- const vars = [':root {'];
419
- const rules = [];
420
- const bodyParts = [];
421
- const fontLinks = new Set();
422
- for (const r of spec.regions) {
423
- if (r.kind === 'band') continue;
424
- const b = r.box, id = r.id;
425
- vars.push(` --r-${id}-x: ${pct(b.x)}; --r-${id}-y: ${pct(b.y)}; --r-${id}-w: ${pct(b.w)}; --r-${id}-h: ${pct(b.h)};`);
426
- const type = r.type || {};
427
- const cap = type.comp && type.comp.capHeightPx;
428
- const chosen = type.chosen || null;
429
- const fontPx = chosen && chosen.fontSizePx ? chosen.fontSizePx : (cap ? Math.round(cap / 0.7) : null);
430
- if (cap) vars.push(` --r-${id}-cap: ${cap}px;${fontPx ? ` --r-${id}-font: ${fontPx}px;` : ''}${chosen ? ` --r-${id}-family: '${chosen.family}'; --r-${id}-weight: ${chosen.weight};` : ''}`);
431
- if (chosen && chosen.family) fontLinks.add(`${chosen.family}:${chosen.weight}`);
432
- rules.push(`.r-${id} { position: absolute; left: var(--r-${id}-x); top: var(--r-${id}-y); width: var(--r-${id}-w); height: var(--r-${id}-h); }`);
433
- const label = (r.note || id).replace(/</g, '&lt;');
434
- if (r.medium === 'raster' && r.kind !== 'texture') {
435
- const src = r.plate ? path.relative(dir, r.plate) : '';
436
- bodyParts.push(` <figure class="r-${id} region plate" data-region="${id}"><img src="${src}" alt="" style="width:100%;height:100%;object-fit:contain;object-position:${r.kind === 'image' ? 'center' : 'top left'}"></figure>`);
437
- } else if (r.kind === 'texture') {
438
- const src = r.plate ? path.relative(dir, r.plate) : '';
439
- bodyParts.push(` <div class="r-${id} region texture" data-region="${id}" style="background-image:url('${src}');background-repeat:repeat"></div>`);
440
- } else if (r.kind === 'text') {
441
- const style = [fontPx ? `font-size:var(--r-${id}-font)` : '', chosen ? `font-family:var(--r-${id}-family),sans-serif;font-weight:var(--r-${id}-weight)` : '', 'line-height:1.05', 'margin:0'].filter(Boolean).join(';');
442
- // the slot shows the region's own words when the spec has them, else its id
443
- // at the measured size (a slot, not a caption: the note goes in a comment)
444
- bodyParts.push(` <div class="r-${id} region text" data-region="${id}"><!-- ${label} --><p style="${style}">${(r.text || '').replace(/</g, '&lt;') || id}</p></div>`);
445
- } else if (r.kind === 'control') {
446
- bodyParts.push(` <div class="r-${id} region control" data-region="${id}"><!-- ${label}: rebuild the control's chrome from the crop (comp-spec.mjs --crop ${id}); its ink box, border, fill, radius, and label size are the comp's --></div>`);
447
- } else {
448
- bodyParts.push(` <div class="r-${id} region chrome" data-region="${id}"><!-- ${label} --></div>`);
449
- }
450
- }
451
- vars.push('}');
452
- const css = [
453
- '/* Impeccable scaffold: the measured layout of the approved comp as custom properties. Generated by build-phase.mjs scaffold; regenerate after comp-spec.mjs --regions changes. Bind these to your own markup; positions are % of the comp frame so they scale with it. */',
454
- ...vars,
455
- '',
456
- `.comp-frame { position: relative; width: 100%; aspect-ratio: ${W} / ${H}; overflow: hidden; }`,
457
- ...rules,
458
- '',
459
- ].join('\n');
460
- const cssPath = path.join(dir, 'layout.css');
461
- fs.writeFileSync(cssPath, css);
462
- const link = fontLinks.size ? ` <link rel="stylesheet" href="https://fonts.googleapis.com/css2?${[...fontLinks].map((f) => { const [fam, w] = f.split(':'); return `family=${encodeURIComponent(fam).replace(/%20/g, '+')}:wght@${w}`; }).join('&')}&display=swap">\n` : '';
463
- const html = `<!doctype html>\n<html lang="en">\n<head>\n <meta charset="utf-8">\n <title>Scaffold reference: ${path.basename(spec.comp)}</title>\n${link} <link rel="stylesheet" href="layout.css">\n <style>html,body{margin:0}body{background:${(spec.palette && spec.palette[0] && spec.palette[0].hex) || '#fff'}}.region{box-sizing:border-box}.region.text p{white-space:pre-wrap}</style>\n</head>\n<body>\n<!-- Reference only. Every region sits at its measured box inside a comp-aspect frame. Take the boxes (layout.css), keep your own semantic structure. -->\n<main class="comp-frame" style="max-width:${W}px">\n${bodyParts.join('\n')}\n</main>\n</body>\n</html>\n`;
464
- const htmlPath = path.join(dir, 'hero-reference.html');
465
- fs.writeFileSync(htmlPath, html);
466
- return { dir, css: cssPath, html: htmlPath };
467
- }
468
-
469
- /** Fraction of grid cells with invented ink that fails the hero. */
470
- export const INVENTED_MIN = 0.04;
471
-
472
- /**
473
- * Text, chrome and invented-ink readings for the hero: the comp and the
474
- * build aligned the way comp-diff aligns them, then per-region checks from
475
- * lib/hero-checks.mjs. Returns { text: [], chrome: [], invented }.
476
- */
477
- export function heroReadings(state, spec, buildPath) {
478
- if (!spec || !state.comp) return null;
479
- const comp = loadRaster(state.comp).image;
480
- const build = loadRaster(buildPath).image;
481
- let aligned = alignBuild(comp, build, 'top');
482
- const shift = bestShift(comp, aligned);
483
- if (shift.dx || shift.dy) { const shifted = createImage(aligned.width, aligned.height, [255, 255, 255, 255]); blit(shifted, aligned, -shift.dx, -shift.dy); aligned = shifted; }
484
- const text = [], chrome = [], plates = [];
485
- for (const r of spec.regions) {
486
- if (!r.px) continue;
487
- const a = crop(comp, r.px.x, r.px.y, r.px.w, r.px.h), b = crop(aligned, r.px.x, r.px.y, r.px.w, r.px.h);
488
- if (r.kind === 'text') { const t = textRegionCheck(r, a, b); text.push(...t.findings); }
489
- else if (r.kind === 'chrome' || r.kind === 'control') { const c = chromeStripCheck(r, a, b); chrome.push(...c.findings); }
490
- else if (r.kind === 'plate' || r.kind === 'image') {
491
- const c = plateClipCheck(r, a, b);
492
- if (c.sides.length) plates.push(`plate ${r.id} is clipped at the ${c.sides.join(' and ')}: the comp's artwork keeps a margin there (ink box ${c.comp.w}x${c.comp.h} at ${c.comp.x},${c.comp.y} in the region) and the build's runs to the edge (${c.build.w}x${c.build.h} at ${c.build.x},${c.build.y}); size the box to the artwork's aspect and use object-fit: contain, or place the <img> at the artwork's own size, never cover on a narrower box`);
493
- }
494
- }
495
- const invented = inventedInk(comp, aligned);
496
- return { text, chrome, plates, invented };
497
- }
498
-
499
- export function gateHero(state, { buildPath = HERO_REPRO, specPath = SPEC_PATH, min = HERO_MIN, outDir = path.join('.impeccable', 'review', 'diff', 'hero'), artifact = null } = {}) {
500
- if (!fs.existsSync(buildPath)) return { ok: false, reasons: [`no hero capture at ${buildPath}: screenshot the first viewport at the comp's own dimensions (${state.breakpoint || 'comp size'}) into that path`] };
501
- const specForRefs = loadSpec(specPath);
502
- // Resolve the page first: with an artifact in hand, unreferencedPlates
503
- // follows its linked stylesheets exactly (wherever they live), and the
504
- // bounded source walk is only the fallback for a build with no page yet.
505
- let pageFile = artifact || state.artifact || null;
506
- if (!pageFile || !fs.existsSync(pageFile)) {
507
- if (fs.existsSync('index.html')) pageFile = 'index.html';
508
- else { try { const htmls = fs.readdirSync('.').filter((f) => /\.html?$/i.test(f)); if (htmls.length === 1) pageFile = htmls[0]; } catch { /* fall back to the walk */ } }
509
- }
510
- const unreferenced = unreferencedPlates(specForRefs, pageFile && fs.existsSync(pageFile) ? pageFile : null);
511
- if (unreferenced.length) {
512
- return { ok: false, reasons: unreferenced.map((r) => `plate ${r.plate} (region ${r.id}) is not referenced by any source file this scan can see: the page draws that region in code while the produced plate sits unused. Place the plate (an <img>, a background-image, or an inlined data URI named for it) and recapture. If the plate IS referenced from a file the scan missed (your page is not index.html, or the reference lives in a stylesheet outside the project walk), re-run with --artifact <your page>: its linked stylesheets are followed exactly.`) };
513
- }
514
- const script = path.join(HERE, 'comp-diff.mjs');
515
- const args = [script, '--comp', state.comp, '--build', buildPath, '--out-dir', outDir, '--label', 'hero', '--json'];
516
- const spec = loadSpec(specPath);
517
- if (spec) args.push('--spec', specPath);
518
- const res = spawnSync(process.execPath, args, { encoding: 'utf8' });
519
- if (res.status !== 0 && res.status !== 3) return { ok: false, reasons: [`comp-diff failed: ${res.stderr || res.stdout}`] };
520
- let report;
521
- try { report = JSON.parse(res.stdout); } catch { return { ok: false, reasons: ['comp-diff produced no report'] }; }
522
- const reasons = [];
523
- const advisories = [];
524
- // The capture must be the comp's own frame: a 1440-wide capture of a
525
- // 1536x1024 comp is a different composition before anything is compared.
526
- const [cw, ch] = String(report.compSize || '').split('x').map(Number);
527
- const [bw, bh] = String(report.buildSize || '').split('x').map(Number);
528
- if (cw && ch && bw && bh) {
529
- const compAspect = cw / ch, buildAspect = bw / bh;
530
- if (bw < cw * 0.9 || Math.abs(buildAspect - compAspect) / compAspect > 0.08) reasons.push(`hero capture is ${bw}x${bh}; the comp is ${cw}x${ch}. Capture the first viewport at the comp's own dimensions (viewport ${cw}x${ch}, not full page) into ${buildPath}.`);
531
- }
532
- // Above the fidelity bar, the numeric readings advise instead of block
533
- // (Paul's calibration: builds at 68-73+ with colour and spacing nits were
534
- // passes; a 90% build sat open behind three ink colours and a cost cap
535
- // ended two 76-79% runs mid-loop). Hard vetoes stay unconditional: a
536
- // missing region, a contradicted plate or text block, an SVG illustration,
537
- // a clipped plate, invented ink are the wrong page at any score.
538
- const aboveBar = report.overall >= min;
539
- if (!aboveBar) reasons.push(`hero overall ${(report.overall * 100).toFixed(1)}% < ${(min * 100).toFixed(0)}% (structure ${(report.scores.structure * 100).toFixed(0)}%, color ${(report.scores.color * 100).toFixed(0)}%, detail ${(report.scores.detail * 100).toFixed(0)}%)`);
540
- if (report.scores.colorIntersection != null && report.scores.colorIntersection < 0.2) reasons.push(`the palette is not the comp's (color intersection ${(report.scores.colorIntersection * 100).toFixed(0)}%): comp ${(report.palette.comp || []).slice(0, 3).map((c) => c.hex).join(' ')} vs build ${(report.palette.build || []).slice(0, 3).map((c) => c.hex).join(' ')}. Use the spec's sampled palette values, not a rendition of them.`);
541
- // A texture band that shares its box with a text/control region carries
542
- // that region's ink in the comp crop; when the overlapping ink regions are
543
- // present in the build, a low detail score on the texture is the ink
544
- // metric measuring the wrong thing, not missing material.
545
- const specRegions = specForRefs ? specForRefs.regions : [];
546
- const overlaps = (a, b) => a.box.x < b.box.x + b.box.w && b.box.x < a.box.x + a.box.w && a.box.y < b.box.y + b.box.h && b.box.y < a.box.y + a.box.h;
547
- const verdictOf = Object.fromEntries(report.regions.map((r) => [r.id, r.verdict]));
548
- const missing = report.regions.filter((r) => {
549
- if (r.verdict !== 'missing') return false;
550
- if (r.kind !== 'texture') return true;
551
- const me = specRegions.find((x) => x.id === r.id);
552
- if (!me) return true;
553
- const inkOver = specRegions.filter((x) => x.id !== r.id && (x.kind === 'text' || x.kind === 'control' || x.kind === 'chrome') && overlaps(me, x));
554
- const inkPresent = inkOver.length > 0 && inkOver.every((x) => verdictOf[x.id] && verdictOf[x.id] !== 'missing');
555
- if (inkPresent) { r.verdict = 'drift'; return false; }
556
- return true;
557
- });
558
- // A plate that passed the plates gate and is referenced by the page is
559
- // placed material, not missing material: comp-diff at the region box
560
- // re-litigates the plate's content (an exploded diagram of a different
561
- // carburetor scores 'missing' on detail against the comp's), and no CSS
562
- // edit can move that score. What the hero owes for a passed plate is its
563
- // placement: material present in the box, at the box. Say that as a box.
564
- const passedPlate = (id) => state.plates && state.plates[id] && state.plates[id].status === 'ok' && (state.plates[id].score == null || state.plates[id].score >= PLATE_MIN);
565
- const placementNotes = [];
566
- for (const r of report.regions) {
567
- if (!(r.kind === 'plate' || r.kind === 'image' || r.kind === 'texture') || !passedPlate(r.id)) continue;
568
- if (r.verdict !== 'missing' && r.verdict !== 'contradicted') continue;
569
- // a texture's presence is its ground: palette and structure held means
570
- // the material is there (grain reads flatter at capture scale); a plate
571
- // or image needs its own energy in the box
572
- const present = r.kind === 'texture'
573
- ? (r.score.structure >= 0.85 && r.score.color >= 0.6)
574
- : (r.score.detailRaw != null ? r.score.detailRaw >= 0.3 : r.score.detail >= 0.3);
575
- if (!present) continue; // nothing drawn there: still missing
576
- r.verdict = 'drift';
577
- r.placed = true;
578
- if (r.inkBox && r.inkBox.comp && r.inkBox.build) {
579
- const c = r.inkBox.comp, b = r.inkBox.build;
580
- const off = Math.abs(b.w - c.w) > c.w * 0.2 || Math.abs(b.h - c.h) > c.h * 0.2 || Math.abs(b.x - c.x) > c.w * 0.15 || Math.abs(b.y - c.y) > c.h * 0.15;
581
- if (off) placementNotes.push(`plate ${r.id} is placed but not at the comp's box: its ink spans ${c.w}x${c.h}px at (${c.x},${c.y}) in the comp region and ${b.w}x${b.h}px at (${b.x},${b.y}) in the build; size and position the <img> to the spec box (object-fit: cover), not to the surrounding layout`);
582
- }
583
- }
584
- const missingAfter = missing.filter((r) => r.verdict === 'missing');
585
- for (const r of missingAfter) reasons.push(`region ${r.id} is missing (detail ${(r.score.detail * 100).toFixed(0)}%, structure ${(r.score.structure * 100).toFixed(0)}%): the comp shows material the build does not`);
586
- for (const n of placementNotes) (aboveBar ? advisories : reasons).push(aboveBar ? `(advisory, above the ${(min * 100).toFixed(0)}% bar) ${n}` : n);
587
- const contradicted = report.regions.filter((r) => r.verdict === 'contradicted');
588
- // A contradicted plate, image, or text region is the wrong page whatever
589
- // the mean says; chrome and controls get the one-third allowance.
590
- // Controls are held like text: their chrome (border, fill, chevron, arrow,
591
- // the dropdown's shape) is the comp's, and a control that reads as a
592
- // different control is a contradiction of its own. Only icon glyphs carry
593
- // the closest-obtainable concession, and those live inside the region.
594
- const directionContradicted = contradicted.filter((r) => r.kind === 'plate' || r.kind === 'image' || r.kind === 'text' || r.kind === 'control');
595
- for (const r of directionContradicted) reasons.push(`region ${r.id} (${r.kind}) is contradicted (structure ${(r.score.structure * 100).toFixed(0)}%, detail added ${(r.score.detailAdded * 100).toFixed(0)}%): ${r.kind === 'text' ? 'the composition of this text region differs from the comp; re-derive it from the spec box' : r.kind === 'control' ? 'this control does not read as the comp\'s: rebuild its chrome from the crop (border, fill, radius, chevron or arrow, label size) rather than from a component default' : 'the plate here does not read as the comp region; regenerate it with the crop as reference (generate-image.mjs --plate ' + r.id + ') and place it at its box'}`);
596
- // a control that drifts far is a different control too: name it
597
- for (const r of report.regions) {
598
- if (r.kind !== 'control' || r.verdict !== 'drift' || r.score.overall >= 0.65) continue;
599
- reasons.push(`control ${r.id} drifts to ${(r.score.overall * 100).toFixed(0)}% (structure ${(r.score.structure * 100).toFixed(0)}%, color ${(r.score.color * 100).toFixed(0)}%): its chrome differs from the comp's; open ${path.join(outDir, 'regions', `${r.id}.png`)} and match the border, fill, radius, chevron or arrow, and label size`);
600
- }
601
- // Controls: report the ink box in comp vs build so a button in a 63px row
602
- // built into a 41px row is named as numbers, not as a drift score.
603
- for (const r of report.regions) {
604
- // whatever the region's verdict: a small button in a large region scores
605
- // match on the region mean while being half its comp height
606
- if (r.kind !== 'control') continue;
607
- if (r.inkBox && r.inkBox.comp && r.inkBox.build) {
608
- // Only when the comp's ink is a discrete element inside its region (a
609
- // button, a tab), not when it fills the region edge to edge (a control
610
- // drawn over a plate's edge, a full-width bar): then the box says nothing.
611
- // report regions carry normalized x/y/w/h at the top level
612
- const rwN = r.w ?? (r.box && r.box.w) ?? 1, rhN = r.h ?? (r.box && r.box.h) ?? 1;
613
- const rw = rwN * (report.compSize ? parseInt(String(report.compSize).split('x')[0], 10) : 1536);
614
- const rh = rhN * (report.compSize ? parseInt(String(report.compSize).split('x')[1], 10) : 1024);
615
- // A bar that spans the region in either axis is not a discrete
616
- // control: its ink box is the region box clipped, and the build's
617
- // box is whatever the region clips there. Comparing the two told one
618
- // session six times that a 1376x87 strip was 1382x102, and no edit it
619
- // made could move that number.
620
- if (r.inkBox.comp.w >= rw * 0.85 || r.inkBox.comp.h >= rh * 0.85) continue;
621
- const dh = r.inkBox.build.h - r.inkBox.comp.h, dw = r.inkBox.build.w - r.inkBox.comp.w;
622
- // the build's box is only comparable when it is discrete too
623
- if (r.inkBox.build.w >= rw * 0.98 || r.inkBox.build.h >= rh * 0.98) continue;
624
- if (Math.abs(dh) > Math.max(6, r.inkBox.comp.h * 0.15) || Math.abs(dw) > Math.max(12, r.inkBox.comp.w * 0.15)) (aboveBar ? advisories : reasons).push(`${aboveBar ? `(advisory, above the ${(min * 100).toFixed(0)}% bar) ` : ''}region ${r.id}: its ink sits in a ${r.inkBox.comp.w}x${r.inkBox.comp.h}px box in the comp and ${r.inkBox.build.w}x${r.inkBox.build.h}px in the build (padding, row height, or size); match the box, not only the position`);
625
- }
626
- }
627
- const otherContradicted = contradicted.filter((r) => !directionContradicted.includes(r));
628
- if (otherContradicted.length > Math.max(1, Math.floor(report.regions.length / 3))) reasons.push(`${otherContradicted.length} of ${report.regions.length} regions contradicted: ${otherContradicted.map((r) => r.id).join(', ')}`);
629
- // A CSS-drawn organic contour sitting on a raster region's box is the plate
630
- // replaced by code, whatever the pixels score.
631
- // The page resolved above serves the code scans too.
632
- const artifactFile = pageFile;
633
- if (artifactFile && fs.existsSync(artifactFile) && specForRefs) {
634
- const organic = organicClipRegions(artifactFile, specForRefs);
635
- for (const r of organic) reasons.push(`artifact draws an organic clip-path (${r.snippet}) inside raster region ${r.id}'s box; that region ships as its plate, never as a polygon`);
636
- // Inline SVG past an icon's budget is a drawing in code: the single most
637
- // repeated pin of the human review ("terrible svg instead of asset",
638
- // "lines that point nowhere"), on every model. Icons, arrows and
639
- // chevrons pass; diagrams, notation, and leader lines do not; those are
640
- // plates, or part of the plate they annotate.
641
- let svgs = [];
642
- try { svgs = svgIllustrations(fs.readFileSync(artifactFile, 'utf8')); } catch { svgs = []; }
643
- for (const v of svgs.slice(0, 6)) reasons.push(`artifact draws an illustration in inline SVG${v.label ? ` (${v.label})` : ''}: ${v.snippet}. Drawings, diagrams, notation, and leader lines are plates or belong to the plate they annotate; only icon-sized SVG (under 64px, a few paths) is code`);
644
- if (svgs.length > 6) reasons.push(`...and ${svgs.length - 6} more inline SVG illustrations`);
645
- }
646
- // Numbers a designer reads off the side-by-side: type set at the wrong
647
- // size, weight, colour, or place; a nav bar too tall; ink where the comp
648
- // has none (a kicker, a divider, a second row). Each was a pin in the
649
- // first human review of builds the region scores had passed.
650
- let readings = null;
651
- try { readings = heroReadings(state, specForRefs, buildPath); } catch (e) { reasons.push(`hero readings errored (${e.message}); the region scores above stand`); }
652
- if (readings) {
653
- // the numbers are the edit list: keep it short enough to act on in one
654
- // pass (the worst first: size, then lines, then colour and place)
655
- const order = (f) => (/cap height/.test(f) ? 0 : /lines? in the build/.test(f) ? 1 : /heavier|lighter/.test(f) ? 2 : /ink is/.test(f) ? 3 : 4);
656
- // sibling regions (row-1 ... row-8, item-a / item-b) with the same kind
657
- // of finding are one edit: fold them into one line naming the siblings
658
- const folded = new Map();
659
- for (const f of readings.text) {
660
- const m = /^text ([a-z0-9]+(?:-[a-z0-9]+)*?)(?:-(?:\d+|[a-z]))?: (cap height|\d+ lines? in the build|the face renders|ink is|its first line|it starts|line pitch)/i.exec(f);
661
- const key = m ? `${m[1]}|${m[2].replace(/\d+/g, 'N')}` : f;
662
- if (!folded.has(key)) folded.set(key, { first: f, ids: [] });
663
- const idm = /^text ([^:]+):/.exec(f); if (idm) folded.get(key).ids.push(idm[1]);
664
- }
665
- const text = [...folded.values()].map((v) => (v.ids.length > 1 ? `${v.first} (also ${v.ids.slice(1).join(', ')})` : v.first)).sort((a, b) => order(a) - order(b));
666
- // A reading that has come back unchanged three attempts running is one
667
- // the session is not acting on (or cannot: a measured "rule" that is
668
- // really an underline the layout does not have). It stays in the message
669
- // as advisory and stops blocking; the fresh readings still do. One
670
- // session spent 27 attempts on the same three lines.
671
- const seen = state.phases.hero.readingsSeen || (state.phases.hero.readingsSeen = {});
672
- const stale = (f) => { const k = f.replace(/\s+/g, ' ').trim(); seen[k] = (seen[k] || 0) + 1; return seen[k] > 3; };
673
- const all = [...text, ...readings.chrome];
674
- const fresh = all.filter((f) => !stale(f));
675
- const advisory = all.filter((f) => !fresh.includes(f));
676
- const kept = fresh.slice(0, 8);
677
- if (aboveBar) {
678
- if (kept.length) advisories.push(`(advisory, above the ${(min * 100).toFixed(0)}% bar; fix in the polish pass before responsive) ${kept.length} reading${kept.length === 1 ? '' : 's'}:`);
679
- for (const f of kept) advisories.push(` ${f}`);
680
- } else {
681
- if (kept.length) reasons.push(`READINGS, each one CSS edit (${fresh.length > kept.length ? `${kept.length} of ${fresh.length}, the rest after these` : `${kept.length}`}):`);
682
- for (const f of kept) reasons.push(f);
683
- }
684
- if (advisory.length) advisories.push(...advisory.map((f) => `(advisory, unchanged for 3+ attempts) ${f}`));
685
- for (const f of readings.plates || []) reasons.push(f);
686
- // invented ink: enough cells, or a couple of strongly inked ones (a
687
- // legend, a badge, a second control in a calm corner)
688
- const strongCells = (readings.invented ? readings.invented.cells : []).filter((c) => c.build >= 22);
689
- if (readings.invented && (readings.invented.fraction >= INVENTED_MIN || strongCells.length >= 2)) {
690
- const cells = readings.invented.cells.map((c) => c.label);
691
- reasons.push(`the build carries ink in ${cells.length} grid cells where the comp is calm (${cells.slice(0, 12).join(', ')}${cells.length > 12 ? ', ...' : ''}); nothing exists on the page that the comp does not show (a kicker, an extra nav item, a divider, a second row of controls); remove it or name it in a stated decision after the hero passes`);
692
- }
693
- }
694
- const worstRegions = [...report.regions].sort((a, b) => a.score.overall - b.score.overall).slice(0, 3);
695
- const regionDir = path.join(outDir, 'regions');
696
- return {
697
- ok: reasons.length === 0,
698
- reasons,
699
- summary: `hero ${(report.overall * 100).toFixed(0)}% (${report.verdict})`,
700
- score: report.overall,
701
- verdict: report.verdict,
702
- report: path.join(outDir, 'report.json'),
703
- sideBySide: report.files ? report.files.sideBySide : null,
704
- worst: worstRegions.map((r) => `${r.id} ${r.verdict} ${(r.score.overall * 100).toFixed(0)}%`),
705
- worstIds: worstRegions.map((r) => r.id),
706
- worstCrops: worstRegions.map((r) => ({ id: r.id, verdict: r.verdict, score: r.score, file: path.join(regionDir, `${r.id}.png`) })),
707
- advisories,
708
- regionVerdicts: Object.fromEntries(report.regions.map((r) => [r.id, r.verdict])),
709
- };
710
- }
711
-
712
- /**
713
- * The hero attempt loop: after two failed advances where the same region is
714
- * still missing/contradicted and the artifact changed only in CSS values,
715
- * refuse a third of the same kind. Missing material is not a layout
716
- * tolerance problem; the fix is a plate, a placed plate, or a rebuilt region.
717
- */
718
- export function heroLoopVerdict(state, gate, artifactPath) {
719
- const p = state.phases.hero;
720
- const history = p.history || [];
721
- const entry = { at: now(), score: gate.score ?? null, worstIds: gate.worstIds || [], regionVerdicts: gate.regionVerdicts || {}, artifactHash: hashFile(artifactPath) };
722
- history.push(entry);
723
- p.history = history.slice(-6);
724
- if (history.length < 3) return null;
725
- const last3 = history.slice(-3);
726
- const stuck = last3[0].worstIds[0] && last3.every((h) => h.worstIds[0] === last3[0].worstIds[0]);
727
- const scores = last3.map((h) => h.score ?? 0);
728
- const noProgress = Math.max(...scores) - Math.min(...scores) < 0.03;
729
- if (stuck && noProgress) {
730
- return `region ${last3[0].worstIds[0]} has been the worst region for three attempts and the score moved less than 3 points: value edits are not reaching it. Open ${path.join('.impeccable', 'review', 'diff', 'hero', 'regions', `${last3[0].worstIds[0]}.png`)} and rebuild that region from the comp crop (place its plate, or produce one with generate-image.mjs --plate, or re-derive its structure from the spec box), then recapture.`;
731
- }
732
- return null;
733
- }
734
-
735
- function hashFile(file) {
736
- try {
737
- const crypto = require('node:crypto');
738
- return crypto.createHash('sha1').update(fs.readFileSync(file)).digest('hex').slice(0, 12);
739
- } catch { return null; }
740
- }
741
-
742
- /**
743
- * Responsive gate: the desktop capture (whatever common width the build
744
- * used, 1440 typically) must still read as the comp. A first viewport that
745
- * only holds at the comp's exact width and collapses to one column 96px
746
- * narrower passed every earlier gate in the first simulated round.
747
- */
748
- export function gateResponsive(state, { specPath = SPEC_PATH, min = RESPONSIVE_MIN, outDir = path.join('.impeccable', 'review', 'diff', 'desktop') } = {}) {
749
- const desktop = path.join('.impeccable', 'review', 'desktop.png');
750
- const mobile = path.join('.impeccable', 'review', 'mobile.png');
751
- const reasons = [];
752
- if (!fs.existsSync(desktop)) reasons.push(`no ${desktop}: capture the page at a common desktop width (1440 wide, full page) into that path`);
753
- if (!fs.existsSync(mobile)) reasons.push(`no ${mobile}: capture the page at 390 wide, full page, into that path`);
754
- if (reasons.length) return { ok: false, reasons };
755
- const script = path.join(HERE, 'comp-diff.mjs');
756
- const args = [script, '--comp', state.comp, '--build', desktop, '--out-dir', outDir, '--label', 'desktop', '--json'];
757
- if (loadSpec(specPath)) args.push('--spec', specPath);
758
- const res = spawnSync(process.execPath, args, { encoding: 'utf8' });
759
- let report;
760
- try { report = JSON.parse(res.stdout); } catch { return { ok: false, reasons: [`comp-diff failed on ${desktop}: ${res.stderr || res.stdout}`] }; }
761
- // A texture read at 1440 differs from itself at 1536 by resampling alone;
762
- // it passed at the hero and cannot block responsive on its own.
763
- // Likewise a plate or image that passed the plates gate: it read 'missing'
764
- // on detail at 1440 in a run where the hero had just accepted it at 1536,
765
- // structure 94%. Only a region with no energy in its box is missing here.
766
- const missing = report.regions.filter((r) => {
767
- if (r.verdict !== 'missing' || r.kind === 'texture') return false;
768
- const passed = state.plates && state.plates[r.id] && state.plates[r.id].status === 'ok';
769
- if ((r.kind === 'plate' || r.kind === 'image') && passed) {
770
- const present = r.score.detailRaw != null ? r.score.detailRaw >= 0.3 : r.score.detail >= 0.3;
771
- if (present && r.score.structure >= 0.5) return false;
772
- }
773
- return true;
774
- });
775
- // A plate placed and passed at the hero is not re-litigated at 1440: the
776
- // rescale alone drops SSIM on a busy region. Text can still contradict
777
- // (a wrapped headline is a different composition).
778
- const contradictedDirection = report.regions.filter((r) => r.verdict === 'contradicted' && r.kind === 'text');
779
- if (report.overall < min) reasons.push(`the desktop capture (${report.buildSize}; the top ${report.compSize} rows scaled to the comp's width are compared, a full-page capture is fine) scores ${(report.overall * 100).toFixed(0)}% against the comp, under ${(min * 100).toFixed(0)}%: the first viewport does not survive a common desktop width. The hero passed at ${state.breakpoint || 'the comp size'}; the layout must hold from ~1280 up, not only at the comp's exact width (grid columns in fr / minmax, not fixed px that overflow and wrap).`);
780
- for (const r of missing) reasons.push(`at desktop width, region ${r.id} is missing`);
781
- for (const r of contradictedDirection) reasons.push(`at desktop width, region ${r.id} (${r.kind}) is contradicted (structure ${(r.score.structure * 100).toFixed(0)}%)`);
782
- return { ok: reasons.length === 0, reasons, summary: `desktop ${(report.overall * 100).toFixed(0)}% (${report.verdict})`, score: report.overall, sideBySide: report.files ? report.files.sideBySide : null };
783
- }
784
-
785
- const GATES = { comps: gateComps, spec: gateSpec, plates: gatePlates, hero: gateHero, responsive: gateResponsive };
786
-
787
- // ---- transitions -----------------------------------------------------------
788
-
789
- export function runGate(state, phase, opts = {}) {
790
- const gate = GATES[phase];
791
- if (!gate) return { ok: true, reasons: [], summary: 'no mechanical gate' };
792
- // A gate that throws is a bug in the gate, never a verdict on the build:
793
- // return it as a refusal that names itself, so the model sees one line
794
- // and the state stays consistent instead of a stack trace and a half-run.
795
- try { return gate(state, opts); }
796
- catch (e) { return { ok: false, reasons: [`gate ${phase} errored (${e.message}); this is a tool bug, not a finding about the page. Re-run with the same inputs; if it repeats, note it and continue with build-phase.mjs advance --force --reason "user: gate ${phase} errored, proceeding" so the run is not lost.`], error: String(e && e.stack || e) }; }
797
- }
798
-
799
- /** Reasons a gate may be forced past. The user downgrading the comp's authority
800
- * in words is the only one; the parent quotes it. A reason that does not name
801
- * the user is a model talking itself past its own gate, and it is refused. */
802
- export function forceAllowed(reason) {
803
- if (typeof reason !== 'string' || reason.trim().length < 20) return false;
804
- if (/gate \w+ errored/i.test(reason)) return true;
805
- const namesUser = /\buser\b|\bthey (said|asked|told|chose|picked)\b|\bpaul\b/i.test(reason);
806
- // The user must be downgrading the comp itself, not "approving" a
807
- // translation the model proposed. A reason that keeps the comp's
808
- // topology/palette while dropping "pixel-level" is a translation, and
809
- // translation is what the gate measures; it is not a downgrade.
810
- const aboutComp = /\b(comp|mock|mockup|composition|fidelity|plate|region)\b/i.test(reason);
811
- const isTranslationDodge = /truthful|semantic|pixel-level|prioriti[sz]e (facts|semantics|accessibility)/i.test(reason) && !/(drop|skip|remove|without|not needed|don't need|do not need|ignore) (the )?(comp|plate|region|fidelity)/i.test(reason);
812
- // The user's words have to be a downgrade of the comp, said as such: a
813
- // brief line quoted back ("should feel like an extension of her artwork")
814
- // is the direction the comp already serves, not permission to leave it.
815
- // One session forced two gates on that sentence. Ask for a downgrade verb
816
- // in the same reason as the comp noun.
817
- const downgrades = /\b(don't|do not|doesn't|does not|no longer|not) (need|have to|want|care|require|match|follow|hold)|\b(drop|skip|remove|ignore|waive|relax|override|approve|approved|accept|accepted|fine|okay|ok|good enough|ship it|move on|proceed|go ahead|instead of|rather than)\b/i.test(reason);
818
- // and the user's words are quoted or reported, not paraphrased into a
819
- // principle ("the user made the artwork binding" is the model's reading)
820
- const reported = /["'\u201c\u2018].{6,}["'\u201d\u2019]|\b(user|they|paul) (said|says|asked|asks|told|wrote|replied|answered|chose|picked|approved|confirmed)\b/i.test(reason);
821
- const briefQuoteOnly = /\b(should feel|feel like|not a .* page|extension of)\b/i.test(reason) && !/\b(comp|mock|fidelity|gate|plate)\b.*\b(approved|accept|fine|ok|okay|skip|drop|waive|relax|override|move on|proceed)\b/i.test(reason);
822
- return namesUser && aboutComp && downgrades && reported && !isTranslationDodge && !briefQuoteOnly;
823
- }
824
-
825
- export function advance(state, { force = false, reason = null, gateOpts = {} } = {}) {
826
- const phase = state.phase;
827
- const idx = PHASES.indexOf(phase);
828
- if (idx === -1 || phase === 'review') return { ok: false, reasons: [`phase ${phase} cannot advance; use finish`] };
829
- const p = state.phases[phase];
830
- p.attempts += 1;
831
- const gate = runGate(state, phase, gateOpts);
832
- const { plates: _p, ...gateRecord } = gate;
833
- p.gate = { ...gateRecord, at: now() };
834
- // the plates gate's per-plate scores are what the hero gate reads to tell
835
- // a placed plate from a missing one, so they travel on the state
836
- if (phase === 'plates' && Array.isArray(gate.plates)) state.plates = Object.fromEntries(gate.plates.map((pl) => [pl.id, { status: pl.status, score: pl.score, size: pl.size }]));
837
- if (!gate.ok && force && !forceAllowed(reason)) {
838
- p.status = 'open';
839
- return { ok: false, phase, reasons: [...gate.reasons, `--force refused: "${reason || ''}" does not quote the user downgrading the comp. A single-file deliverable, a missing tool, or difficulty is not a reason, and a refused force is not the end of the phase: the readings above are the edits, each one a CSS value; make them, recapture, advance. Ask the user only when a reading contradicts something they said about this comp.`], gate };
840
- }
841
- if (phase === 'hero' && (gate.score != null)) {
842
- const stuck = heroLoopVerdict(state, gate, gateOpts.artifact || state.artifact || 'index.html');
843
- if (stuck && !gate.ok) gate.reasons = [stuck, ...gate.reasons];
844
- }
845
- if (!gate.ok && !force) { p.status = 'open'; return { ok: false, phase, reasons: gate.reasons, gate }; }
846
- if (!gate.ok && force) p.forced = { at: now(), reason, reasons: gate.reasons };
847
- p.status = 'closed'; p.closedAt = now();
848
- if (phase === 'comps' && gate.approved) {
849
- state.comp = gate.approved;
850
- if (!state.breakpoint) { try { const i = loadRaster(gate.approved).image; state.breakpoint = `${i.width}x${i.height}`; } catch { /* non-png comp: breakpoint stays unset */ } }
851
- }
852
- const next = PHASES[idx + 1];
853
- state.phase = next;
854
- state.phases[next].status = 'open'; state.phases[next].openedAt = now();
855
- return { ok: true, phase, next, gate, forced: !!p.forced };
856
- }
857
-
858
- export function nextInstruction(state) {
859
- switch (state.phase) {
860
- case 'comps': return `Comp round for the chosen direction${state.direction ? ` (seed ${state.direction})` : ''}: read reference/visualize.md, generate three compositional comps of the requested surface at its own viewport into ${MOCKS_DIR}/ (each with a prompt sidecar), put them in front of the user, and set "approved": true in the chosen comp's sidecar. Then build-phase.mjs advance. No page code before this closes.`;
861
- case 'spec': return `Measure the comp: node comp-spec.mjs --comp ${state.comp} --grid, open ${path.join(BUILD_DIR, 'comp-grid.png')}, write regions.json (every illustration, photo, texture as its own plate region; every text block its own text region), run comp-spec.mjs --comp ${state.comp} --regions regions.json. Then measure the type: node font-match.mjs --measure <id> for each text region (cap height, width class, weight class) and font-match.mjs --rank <lead text region> --text "<its first words>" to choose the headline face by metrics (the USE line is the CSS; with no browser it records the catalog's nearest face, which is the choice; do not install one, and do not write a chosen face into the spec by hand). Then build-phase.mjs advance.`;
862
- case 'plates': return 'Produce every plate in the spec (comp-spec.mjs --print lists them). Illustrations, photos, figures: node generate-image.mjs --plate <id>, one call per plate. It crops the comp region itself, sends the crop as the edit reference, sizes the plate, keys ink-on-ground to alpha, scores the result against the crop (PLATE-SCORE) and embeds the prompt; nothing else does all of that. Only when it errors (no key, no network) fall back to the harness image tool with comp-spec.mjs --crop <id> as its reference image and comp-spec.mjs --plate-prompt <id> as its prompt, then embed-prompt.mjs; do not post-process a plate with magick or write your own keying. A generation takes 30 to 90 seconds: run it with a long wait (a 90 s yield, or all plates in one command joined with &&) rather than polling an open session turn after turn. A line drawing or figure on flat ground is keyed to alpha automatically (PLATE-CHROMA): place it with a plain <img> over the page\'s own ground, never on a second paper. An opaque plate whose ground differs from the page goes in with mix-blend-mode: multiply. Textures (paper, cloth, grain): do not generate first; crop a clean patch of the comp region (comp-spec.mjs --crop <id> --raw, then cut a patch free of ink), mirror-tile it to the plate size, and save it as the plate; generate only when no clean patch exists. The gate scores a texture against its whole region box, so a texture region should be drawn around clean ground (a sample cell), not around the ink it sits under; the page tiles it wherever the material goes. Then build-phase.mjs advance. Write no page code before this passes.';
863
- case 'hero': return `Run build-phase.mjs scaffold first: it writes the measured layout as CSS custom properties (.impeccable/build/scaffold/layout.css, --r-<id>-x/y/w/h in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page with every region at its box. Bind those numbers to your own markup (an element per region, its box from the properties); the reference is a check, not the page, and overlapping boxes are overlapping boxes. Build only the first viewport at ${state.breakpoint || 'the comp size'}. Copy the comp's words verbatim in this phase (headline, labels, table cells, footer): the user approved that comp with those words, and rewriting is a later, stated decision, never a silent one here. Set every text region's font-size from its measured cap height and its face from the ranking. Plates first: place every plate at its spec box (comp-spec.mjs --print lists boxes as percentages of the viewport) with object-fit: cover before writing a line of text or a control, capture into ${HERO_REPRO}, and run build-phase.mjs record hero (not advance) once so you see the plate regions read as match before text exists; then lay the semantic layer (text, controls, rules) over the plates from the spec's palette and boxes, capture, advance. When it fails, open the region crops it lists first, in order, then fix; do not build past the hero until it passes.`;
864
- case 'sections': return 'Build the remaining sections inside the spec system (same corner language, rules, and palette; nothing the comp does not show). The hero passed with the comp\'s words verbatim; from here, content beyond the comp is yours to author at full fidelity, and any change to words the comp showed is a stated decision in your report, never silent. Then build-phase.mjs advance.';
865
- case 'motion': return 'Add the signature interaction, reveals, and motion. Then build-phase.mjs advance.';
866
- case 'responsive': return 'Build the other viewports (mobile first if the surface is mobile). The first viewport must hold at common desktop widths (1280 to 1600), not only at the comp\'s exact size: fluid columns, no fixed-px grid that wraps 96px narrower. Settle or disable entrance motion before capturing (an element mid-animation reads as missing). Capture desktop.png (1440 wide, full page) and mobile.png (390 wide, full page) into .impeccable/review/; the gate diffs the top of desktop.png (scaled to the comp\'s width) against the comp. Then build-phase.mjs advance.';
867
- case 'review': return 'Spawn the finish reviewer with the state file, the hero diff report, and the captures; record its disposition with build-phase.mjs finish --disposition <word>.';
868
- default: return '';
869
- }
870
- }
871
-
872
- export function renderStatus(state) {
873
- const lines = [`BUILD-PHASE ${state.phase.toUpperCase()} comp ${state.comp || '(pending comp round)'}${state.direction ? ` direction ${state.direction}` : ''}${state.breakpoint ? ` breakpoint ${state.breakpoint}` : ''}`];
874
- for (const p of PHASES) {
875
- const s = state.phases[p];
876
- let line = ` ${p.padEnd(11)} ${s.status.padEnd(8)}`;
877
- if (s.gate && s.gate.summary) line += ` ${s.gate.summary}`;
878
- if (s.attempts > 1) line += ` (${s.attempts} attempts)`;
879
- if (s.forced) line += ` FORCED: ${s.forced.reason}`;
880
- lines.push(line);
881
- }
882
- if (state.finish) lines.push(` finish ${state.finish.disposition} at ${state.finish.at}`);
883
- lines.push(`NEXT ${nextInstruction(state)}`);
884
- return lines.join('\n');
885
- }
886
-
887
- async function main() {
888
- const cmd = process.argv[2];
889
- if (!cmd || flag('help')) {
890
- console.error('usage: build-phase.mjs start --comp <png> [--breakpoint WxH] | status [--json] | advance [--force --reason "..."] | record hero --build <png> | scaffold | note "<text>" | finish --disposition <word>');
891
- process.exit(1);
892
- }
893
- if (cmd === 'start') {
894
- const comp = arg('comp');
895
- const direction = arg('direction');
896
- if (!comp && !direction) { console.error('build-phase: start needs --comp <approved comp png> (comp already approved) or --direction <seed key> (comp round still to run)'); process.exit(1); }
897
- if (comp && !fs.existsSync(comp)) { console.error(`build-phase: comp ${comp} does not exist`); process.exit(1); }
898
- // The direction choice ping rides on start (see concept-seed.mjs): one
899
- // command records the choice and opens the phases. Never fatal.
900
- if (direction && arg('kind')) {
901
- try {
902
- const { pingChosen } = await import('./concept-seed.mjs');
903
- const sent = await pingChosen({ chosenId: arg('chosen') || undefined, key: direction, scope: 'direction', mode: arg('mode') || undefined, kind: arg('kind'), register: arg('register') || undefined });
904
- console.log(sent ? 'choice recorded' : 'choice ping skipped');
905
- } catch { console.log('choice ping skipped'); }
906
- }
907
- // Clear the roll's pending marker: the build has started.
908
- try { fs.rmSync(path.join(BUILD_DIR, 'pending.json'), { force: true }); } catch { /* absent */ }
909
- // Code-led: no phase machine to run; say what comes next and stop.
910
- const buildPath = readBuildPath();
911
- if (direction && !comp && buildPath === 'code') {
912
- console.log('CODE-LED (from .impeccable config): no comp round and no phase gates. Write the direction contract (reference/new-work.md section 5), build, and finish per section 7. The chosen decision comp, if any, rides to the finish review as the critique reference.');
913
- return;
914
- }
915
- let breakpoint = arg('breakpoint');
916
- if (!breakpoint && comp) { try { const i = loadRaster(comp).image; breakpoint = `${i.width}x${i.height}`; } catch { /* leave null */ } }
917
- const existing = loadState();
918
- if (existing && !flag('reset')) {
919
- console.log(`build-phase: state exists (phase ${existing.phase}); pass --reset to start over`);
920
- console.log(renderStatus(existing));
921
- return;
922
- }
923
- const state = newState({ comp, breakpoint, artifact: arg('artifact'), direction });
924
- saveState(state);
925
- console.log(renderStatus(state));
926
- return;
927
- }
928
- const state = loadState();
929
- if (!state) { console.error(`build-phase: no state at ${STATE_PATH}; run build-phase.mjs start --comp <approved comp>`); process.exit(1); }
930
- if (cmd === 'status') {
931
- if (flag('json')) console.log(JSON.stringify(state, null, 2)); else console.log(renderStatus(state));
932
- return;
933
- }
934
- if (cmd === 'scaffold') {
935
- const spec = loadSpec();
936
- if (!spec) { console.error(`build-phase: no spec at ${SPEC_PATH}; run comp-spec.mjs first`); process.exit(1); }
937
- const out = writeScaffold(spec, state);
938
- console.log(`SCAFFOLD ${out.dir}`);
939
- console.log(` ${out.css} one custom property set per region (--r-<id>-x/y/w/h in % of the comp; --r-<id>-cap, --r-<id>-font, --r-<id>-weight where measured); bind these to your own markup`);
940
- console.log(` ${out.html} a reference page: every region positioned at its box inside a ${state.breakpoint || spec.compSize.width + 'x' + spec.compSize.height} frame, plates placed with object-fit: contain, text slots at the measured cap height in the ranked face`);
941
- console.log(' The reference is a check, not the page: keep your own semantic structure and bind the numbers to it (an element per region, its box from the properties). Overlapping boxes are overlapping boxes. What the gate reads is pixels; a page that lands each region at its box passes whatever markup it uses.');
942
- return;
943
- }
944
- if (cmd === 'note') {
945
- const text = process.argv.slice(3).filter((a) => !a.startsWith('--')).join(' ');
946
- state.phases[state.phase].notes.push({ at: now(), text });
947
- saveState(state);
948
- console.log(`noted on ${state.phase}`);
949
- return;
950
- }
951
- if (cmd === 'record') {
952
- const which = process.argv[3];
953
- if (which !== 'hero') { console.error('build-phase: record hero --build <png>'); process.exit(1); }
954
- const gate = gateHero(state, { buildPath: arg('build', HERO_REPRO), min: arg('min') ? parseFloat(arg('min')) : HERO_MIN });
955
- // record is a look, not an attempt: the plates-only capture is expected
956
- // to fail on every text region, and counting it muddied the tally.
957
- state.phases.hero.records = (state.phases.hero.records || 0) + 1;
958
- state.phases.hero.gate = { ...gate, at: now() };
959
- saveState(state);
960
- // record is the look, advance is the gate: on the plates-only capture,
961
- // every text region reads missing by design, so say what the plates did.
962
- const plateRows = Object.entries(gate.regionVerdicts || {}).filter(([id]) => { const spec = loadSpec(); const r = spec && spec.regions.find((x) => x.id === id); return r && r.medium === 'raster'; });
963
- if (plateRows.length) console.log(`PLATES ${plateRows.map(([id, v]) => `${id}:${v}`).join(' ')}`);
964
- console.log(`${gate.ok ? 'PASS' : 'FAIL'} ${gate.summary || ''} (record: nothing advanced)`);
965
- for (const r of gate.reasons) console.log(` - ${r}`);
966
- if (gate.advisories) for (const a of gate.advisories) console.log(` ${a}`);
967
- if (gate.worst) console.log(` worst: ${gate.worst.join('; ')}`);
968
- if (gate.sideBySide) console.log(` open ${gate.sideBySide}`);
969
- process.exit(gate.ok ? 0 : 2);
970
- }
971
- if (cmd === 'advance') {
972
- const gateOpts = {};
973
- if (arg('build')) gateOpts.buildPath = arg('build');
974
- if (arg('min')) gateOpts.min = parseFloat(arg('min'));
975
- if (arg('artifact')) gateOpts.artifact = arg('artifact');
976
- const res = advance(state, { force: flag('force'), reason: arg('reason'), gateOpts });
977
- saveState(state);
978
- if (!res.ok) {
979
- console.log(`GATE ${res.phase ? res.phase.toUpperCase() : ''} FAILED (state unchanged)`);
980
- if (res.gate && res.gate.worstCrops && res.gate.worstCrops.length) {
981
- console.log(' LOOK FIRST, in this order, before editing anything (comp on the left, your build on the right):');
982
- for (const c of res.gate.worstCrops) console.log(` ${c.file} ${c.id}: ${c.verdict} ${(c.score.overall * 100).toFixed(0)}% (structure ${(c.score.structure * 100).toFixed(0)}%, color ${(c.score.color * 100).toFixed(0)}%, detail ${(c.score.detail * 100).toFixed(0)}%)`);
983
- console.log(' A region scored missing needs its material (a plate placed, or produced), not a value change; contradicted needs its structure re-derived from the spec box; drift is where padding and size edits belong. When a thin chrome strip (masthead, breadcrumb, table header) is the worst region, check its box height in the spec against the comp first: a strip one grid row tall in the spec but 53px in the comp compares your build against ground it never had.');
984
- }
985
- for (const r of res.reasons) console.log(` - ${r}`);
986
- if (res.gate && res.gate.advisories && res.gate.advisories.length) for (const a of res.gate.advisories) console.log(` ${a}`);
987
- if (res.gate && res.gate.sideBySide) console.log(` then ${res.gate.sideBySide} for the whole viewport`);
988
- process.exit(2);
989
- }
990
- console.log(`ADVANCED ${res.phase} -> ${res.next}${res.forced ? ' (FORCED; recorded)' : ''}${res.gate.summary ? ` ${res.gate.summary}` : ''}`);
991
- if (res.gate && res.gate.advisories && res.gate.advisories.length) for (const a of res.gate.advisories) console.log(` ${a}`);
992
- console.log(`NEXT ${nextInstruction(state)}`);
993
- return;
994
- }
995
- if (cmd === 'finish') {
996
- const disposition = arg('disposition');
997
- if (!['ship', 'fix', 'rebuild', 'recapture'].includes(disposition)) { console.error('build-phase: finish --disposition ship|fix|rebuild|recapture'); process.exit(1); }
998
- // A ship cannot be recorded over an open phase. The model can still stop
999
- // talking, but it cannot write "ship" into the state with the hero open;
1000
- // sessions did exactly that and summarised the build as complete.
1001
- const openBefore = PHASES.filter((ph) => ph !== 'review' && state.phases[ph] && state.phases[ph].status !== 'closed' && state.phases[ph].status !== 'skipped');
1002
- if (disposition === 'ship' && openBefore.length) {
1003
- console.error(`build-phase: finish --disposition ship refused: ${openBefore.join(', ')} ${openBefore.length === 1 ? 'is' : 'are'} not closed (phase ${state.phase}). Record fix or rebuild, or close the phases first; a page shipped over an open hero is a page shipped against its own gate.`);
1004
- process.exit(2);
1005
- }
1006
- state.finish = { disposition, at: now(), phaseAtFinish: state.phase };
1007
- if (state.phase === 'review') { state.phases.review.status = 'closed'; state.phases.review.closedAt = now(); }
1008
- saveState(state);
1009
- console.log(renderStatus(state));
1010
- return;
1011
- }
1012
- console.error(`build-phase: unknown command ${cmd}`);
1013
- process.exit(1);
1014
- }
1015
-
1016
- // realpath on both sides: a skill mounted through a symlink (Cursor, a
1017
- // worktree, an eval stage) must still run as a CLI.
1018
- const isMain = (() => {
1019
- try { return !!process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); }
1020
- catch { return !!process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname); }
1021
- })();
1022
- if (isMain) main();