@tangle-network/browser-agent-driver 0.33.2 → 0.34.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 (485) hide show
  1. package/README.md +42 -1
  2. package/discovery/llms.txt +27 -0
  3. package/discovery/tangle-agent.json +36 -0
  4. package/dist/brain/action-parse.d.ts +16 -0
  5. package/dist/brain/action-parse.d.ts.map +1 -0
  6. package/dist/brain/action-parse.js +191 -0
  7. package/dist/brain/action-parse.js.map +1 -0
  8. package/dist/brain/decide.d.ts +58 -0
  9. package/dist/brain/decide.d.ts.map +1 -0
  10. package/dist/brain/decide.js +418 -0
  11. package/dist/brain/decide.js.map +1 -0
  12. package/dist/brain/history-compact.d.ts +19 -0
  13. package/dist/brain/history-compact.d.ts.map +1 -0
  14. package/dist/brain/history-compact.js +153 -0
  15. package/dist/brain/history-compact.js.map +1 -0
  16. package/dist/brain/index.d.ts +78 -152
  17. package/dist/brain/index.d.ts.map +1 -1
  18. package/dist/brain/index.js +62 -2268
  19. package/dist/brain/index.js.map +1 -1
  20. package/dist/brain/model-client.d.ts +101 -0
  21. package/dist/brain/model-client.d.ts.map +1 -0
  22. package/dist/brain/model-client.js +391 -0
  23. package/dist/brain/model-client.js.map +1 -0
  24. package/dist/brain/plan.d.ts +57 -0
  25. package/dist/brain/plan.d.ts.map +1 -0
  26. package/dist/brain/plan.js +194 -0
  27. package/dist/brain/plan.js.map +1 -0
  28. package/dist/brain/prompts.d.ts +37 -0
  29. package/dist/brain/prompts.d.ts.map +1 -0
  30. package/dist/brain/prompts.js +455 -0
  31. package/dist/brain/prompts.js.map +1 -0
  32. package/dist/brain/provider-fetch.d.ts +26 -0
  33. package/dist/brain/provider-fetch.d.ts.map +1 -0
  34. package/dist/brain/provider-fetch.js +45 -0
  35. package/dist/brain/provider-fetch.js.map +1 -0
  36. package/dist/brain/snapshot-budget.d.ts +20 -0
  37. package/dist/brain/snapshot-budget.d.ts.map +1 -0
  38. package/dist/brain/snapshot-budget.js +202 -0
  39. package/dist/brain/snapshot-budget.js.map +1 -0
  40. package/dist/brain/system-prompt.d.ts +67 -0
  41. package/dist/brain/system-prompt.d.ts.map +1 -0
  42. package/dist/brain/system-prompt.js +128 -0
  43. package/dist/brain/system-prompt.js.map +1 -0
  44. package/dist/brain/tasks/design-audit.d.ts +33 -0
  45. package/dist/brain/tasks/design-audit.d.ts.map +1 -0
  46. package/dist/brain/tasks/design-audit.js +100 -0
  47. package/dist/brain/tasks/design-audit.js.map +1 -0
  48. package/dist/brain/tasks/evaluate.d.ts +25 -0
  49. package/dist/brain/tasks/evaluate.d.ts.map +1 -0
  50. package/dist/brain/tasks/evaluate.js +55 -0
  51. package/dist/brain/tasks/evaluate.js.map +1 -0
  52. package/dist/brain/tasks/goal-verification.d.ts +32 -0
  53. package/dist/brain/tasks/goal-verification.d.ts.map +1 -0
  54. package/dist/brain/tasks/goal-verification.js +68 -0
  55. package/dist/brain/tasks/goal-verification.js.map +1 -0
  56. package/dist/brain/tasks/knowledge.d.ts +26 -0
  57. package/dist/brain/tasks/knowledge.d.ts.map +1 -0
  58. package/dist/brain/tasks/knowledge.js +56 -0
  59. package/dist/brain/tasks/knowledge.js.map +1 -0
  60. package/dist/brain/tasks/link-scout.d.ts +37 -0
  61. package/dist/brain/tasks/link-scout.d.ts.map +1 -0
  62. package/dist/brain/tasks/link-scout.js +69 -0
  63. package/dist/brain/tasks/link-scout.js.map +1 -0
  64. package/dist/brain/types.d.ts +55 -0
  65. package/dist/brain/types.d.ts.map +1 -0
  66. package/dist/brain/types.js +7 -0
  67. package/dist/brain/types.js.map +1 -0
  68. package/dist/browser/stealth-init-script.d.ts +9 -0
  69. package/dist/browser/stealth-init-script.d.ts.map +1 -0
  70. package/dist/browser/stealth-init-script.js +128 -0
  71. package/dist/browser/stealth-init-script.js.map +1 -0
  72. package/dist/browser/storage-state.d.ts +29 -0
  73. package/dist/browser/storage-state.d.ts.map +1 -0
  74. package/dist/browser/storage-state.js +47 -0
  75. package/dist/browser/storage-state.js.map +1 -0
  76. package/dist/cli/args.d.ts +141 -0
  77. package/dist/cli/args.d.ts.map +1 -0
  78. package/dist/cli/args.js +167 -0
  79. package/dist/cli/args.js.map +1 -0
  80. package/dist/cli/benchmark-sync.d.ts +9 -0
  81. package/dist/cli/benchmark-sync.d.ts.map +1 -0
  82. package/dist/cli/benchmark-sync.js +43 -0
  83. package/dist/cli/benchmark-sync.js.map +1 -0
  84. package/dist/cli/build-config.d.ts +11 -0
  85. package/dist/cli/build-config.d.ts.map +1 -0
  86. package/dist/cli/build-config.js +355 -0
  87. package/dist/cli/build-config.js.map +1 -0
  88. package/dist/cli/commands/auth.d.ts +3 -0
  89. package/dist/cli/commands/auth.d.ts.map +1 -0
  90. package/dist/cli/commands/auth.js +36 -0
  91. package/dist/cli/commands/auth.js.map +1 -0
  92. package/dist/cli/commands/chrome-debug.d.ts +7 -0
  93. package/dist/cli/commands/chrome-debug.d.ts.map +1 -0
  94. package/dist/cli/commands/chrome-debug.js +11 -0
  95. package/dist/cli/commands/chrome-debug.js.map +1 -0
  96. package/dist/cli/commands/design-audit.d.ts +3 -0
  97. package/dist/cli/commands/design-audit.d.ts.map +1 -0
  98. package/dist/cli/commands/design-audit.js +68 -0
  99. package/dist/cli/commands/design-audit.js.map +1 -0
  100. package/dist/cli/commands/preview.d.ts +14 -0
  101. package/dist/cli/commands/preview.d.ts.map +1 -0
  102. package/dist/cli/commands/preview.js +31 -0
  103. package/dist/cli/commands/preview.js.map +1 -0
  104. package/dist/cli/commands/run.d.ts +3 -0
  105. package/dist/cli/commands/run.d.ts.map +1 -0
  106. package/dist/cli/commands/run.js +728 -0
  107. package/dist/cli/commands/run.js.map +1 -0
  108. package/dist/cli/commands/runs.d.ts +8 -0
  109. package/dist/cli/commands/runs.d.ts.map +1 -0
  110. package/dist/cli/commands/runs.js +35 -0
  111. package/dist/cli/commands/runs.js.map +1 -0
  112. package/dist/cli/commands/share.d.ts +10 -0
  113. package/dist/cli/commands/share.d.ts.map +1 -0
  114. package/dist/cli/commands/share.js +32 -0
  115. package/dist/cli/commands/share.js.map +1 -0
  116. package/dist/cli/commands/showcase.d.ts +17 -0
  117. package/dist/cli/commands/showcase.d.ts.map +1 -0
  118. package/dist/cli/commands/showcase.js +20 -0
  119. package/dist/cli/commands/showcase.js.map +1 -0
  120. package/dist/cli/commands/snapshot.d.ts +12 -0
  121. package/dist/cli/commands/snapshot.d.ts.map +1 -0
  122. package/dist/cli/commands/snapshot.js +24 -0
  123. package/dist/cli/commands/snapshot.js.map +1 -0
  124. package/dist/cli/commands/view.d.ts +7 -0
  125. package/dist/cli/commands/view.d.ts.map +1 -0
  126. package/dist/cli/commands/view.js +23 -0
  127. package/dist/cli/commands/view.js.map +1 -0
  128. package/dist/cli/constants.d.ts +6 -0
  129. package/dist/cli/constants.d.ts.map +1 -0
  130. package/dist/cli/constants.js +3 -0
  131. package/dist/cli/constants.js.map +1 -0
  132. package/dist/cli/version.d.ts +7 -0
  133. package/dist/cli/version.d.ts.map +1 -0
  134. package/dist/cli/version.js +16 -0
  135. package/dist/cli/version.js.map +1 -0
  136. package/dist/cli-design-audit.d.ts +44 -36
  137. package/dist/cli-design-audit.d.ts.map +1 -1
  138. package/dist/cli-design-audit.js +137 -1646
  139. package/dist/cli-design-audit.js.map +1 -1
  140. package/dist/cli-jobs.js +2 -1
  141. package/dist/cli-jobs.js.map +1 -1
  142. package/dist/cli.js +66 -1697
  143. package/dist/cli.js.map +1 -1
  144. package/dist/config.d.ts +5 -0
  145. package/dist/config.d.ts.map +1 -1
  146. package/dist/config.js +8 -0
  147. package/dist/config.js.map +1 -1
  148. package/dist/design/audit/evolve/agent.d.ts +23 -0
  149. package/dist/design/audit/evolve/agent.d.ts.map +1 -0
  150. package/dist/design/audit/evolve/agent.js +272 -0
  151. package/dist/design/audit/evolve/agent.js.map +1 -0
  152. package/dist/design/audit/evolve/css.d.ts +10 -0
  153. package/dist/design/audit/evolve/css.d.ts.map +1 -0
  154. package/dist/design/audit/evolve/css.js +206 -0
  155. package/dist/design/audit/evolve/css.js.map +1 -0
  156. package/dist/design/audit/evolve/index.d.ts +13 -0
  157. package/dist/design/audit/evolve/index.d.ts.map +1 -0
  158. package/dist/design/audit/evolve/index.js +12 -0
  159. package/dist/design/audit/evolve/index.js.map +1 -0
  160. package/dist/design/audit/evolve/report.d.ts +3 -0
  161. package/dist/design/audit/evolve/report.d.ts.map +1 -0
  162. package/dist/design/audit/evolve/report.js +45 -0
  163. package/dist/design/audit/evolve/report.js.map +1 -0
  164. package/dist/design/audit/evolve/types.d.ts +14 -0
  165. package/dist/design/audit/evolve/types.d.ts.map +1 -0
  166. package/dist/design/audit/evolve/types.js +2 -0
  167. package/dist/design/audit/evolve/types.js.map +1 -0
  168. package/dist/design/audit/pipeline.d.ts +12 -0
  169. package/dist/design/audit/pipeline.d.ts.map +1 -1
  170. package/dist/design/audit/pipeline.js +43 -27
  171. package/dist/design/audit/pipeline.js.map +1 -1
  172. package/dist/design/audit/reference/artifact/build.d.ts +33 -0
  173. package/dist/design/audit/reference/artifact/build.d.ts.map +1 -0
  174. package/dist/design/audit/reference/artifact/build.js +64 -0
  175. package/dist/design/audit/reference/artifact/build.js.map +1 -0
  176. package/dist/design/audit/reference/artifact/render.d.ts +39 -0
  177. package/dist/design/audit/reference/artifact/render.d.ts.map +1 -0
  178. package/dist/design/audit/reference/artifact/render.js +213 -0
  179. package/dist/design/audit/reference/artifact/render.js.map +1 -0
  180. package/dist/design/audit/reference/artifact/text.d.ts +11 -0
  181. package/dist/design/audit/reference/artifact/text.d.ts.map +1 -0
  182. package/dist/design/audit/reference/artifact/text.js +20 -0
  183. package/dist/design/audit/reference/artifact/text.js.map +1 -0
  184. package/dist/design/audit/reference/artifact/to-findings.d.ts +57 -0
  185. package/dist/design/audit/reference/artifact/to-findings.d.ts.map +1 -0
  186. package/dist/design/audit/reference/artifact/to-findings.js +314 -0
  187. package/dist/design/audit/reference/artifact/to-findings.js.map +1 -0
  188. package/dist/design/audit/reference/config.d.ts +50 -0
  189. package/dist/design/audit/reference/config.d.ts.map +1 -0
  190. package/dist/design/audit/reference/config.js +108 -0
  191. package/dist/design/audit/reference/config.js.map +1 -0
  192. package/dist/design/audit/reference/contracts.d.ts +1030 -0
  193. package/dist/design/audit/reference/contracts.d.ts.map +1 -0
  194. package/dist/design/audit/reference/contracts.js +28 -0
  195. package/dist/design/audit/reference/contracts.js.map +1 -0
  196. package/dist/design/audit/reference/corpus/build.d.ts +115 -0
  197. package/dist/design/audit/reference/corpus/build.d.ts.map +1 -0
  198. package/dist/design/audit/reference/corpus/build.js +152 -0
  199. package/dist/design/audit/reference/corpus/build.js.map +1 -0
  200. package/dist/design/audit/reference/corpus/schema.d.ts +33 -0
  201. package/dist/design/audit/reference/corpus/schema.d.ts.map +1 -0
  202. package/dist/design/audit/reference/corpus/schema.js +249 -0
  203. package/dist/design/audit/reference/corpus/schema.js.map +1 -0
  204. package/dist/design/audit/reference/corpus/store.d.ts +26 -0
  205. package/dist/design/audit/reference/corpus/store.d.ts.map +1 -0
  206. package/dist/design/audit/reference/corpus/store.js +104 -0
  207. package/dist/design/audit/reference/corpus/store.js.map +1 -0
  208. package/dist/design/audit/reference/dna/delta.d.ts +21 -0
  209. package/dist/design/audit/reference/dna/delta.d.ts.map +1 -0
  210. package/dist/design/audit/reference/dna/delta.js +126 -0
  211. package/dist/design/audit/reference/dna/delta.js.map +1 -0
  212. package/dist/design/audit/reference/dna/derive.d.ts +31 -0
  213. package/dist/design/audit/reference/dna/derive.d.ts.map +1 -0
  214. package/dist/design/audit/reference/dna/derive.js +381 -0
  215. package/dist/design/audit/reference/dna/derive.js.map +1 -0
  216. package/dist/design/audit/reference/dna/descriptor.d.ts +36 -0
  217. package/dist/design/audit/reference/dna/descriptor.d.ts.map +1 -0
  218. package/dist/design/audit/reference/dna/descriptor.js +209 -0
  219. package/dist/design/audit/reference/dna/descriptor.js.map +1 -0
  220. package/dist/design/audit/reference/dna/page-adapter.d.ts +17 -0
  221. package/dist/design/audit/reference/dna/page-adapter.d.ts.map +1 -0
  222. package/dist/design/audit/reference/dna/page-adapter.js +35 -0
  223. package/dist/design/audit/reference/dna/page-adapter.js.map +1 -0
  224. package/dist/design/audit/reference/dna/scroll-capture.d.ts +93 -0
  225. package/dist/design/audit/reference/dna/scroll-capture.d.ts.map +1 -0
  226. package/dist/design/audit/reference/dna/scroll-capture.js +374 -0
  227. package/dist/design/audit/reference/dna/scroll-capture.js.map +1 -0
  228. package/dist/design/audit/reference/engine/budget.d.ts +63 -0
  229. package/dist/design/audit/reference/engine/budget.d.ts.map +1 -0
  230. package/dist/design/audit/reference/engine/budget.js +110 -0
  231. package/dist/design/audit/reference/engine/budget.js.map +1 -0
  232. package/dist/design/audit/reference/engine/core.d.ts +32 -0
  233. package/dist/design/audit/reference/engine/core.d.ts.map +1 -0
  234. package/dist/design/audit/reference/engine/core.js +261 -0
  235. package/dist/design/audit/reference/engine/core.js.map +1 -0
  236. package/dist/design/audit/reference/engine/guard.d.ts +43 -0
  237. package/dist/design/audit/reference/engine/guard.d.ts.map +1 -0
  238. package/dist/design/audit/reference/engine/guard.js +43 -0
  239. package/dist/design/audit/reference/engine/guard.js.map +1 -0
  240. package/dist/design/audit/reference/engine/score-core.d.ts +48 -0
  241. package/dist/design/audit/reference/engine/score-core.d.ts.map +1 -0
  242. package/dist/design/audit/reference/engine/score-core.js +134 -0
  243. package/dist/design/audit/reference/engine/score-core.js.map +1 -0
  244. package/dist/design/audit/reference/engine/wiring.d.ts +35 -0
  245. package/dist/design/audit/reference/engine/wiring.d.ts.map +1 -0
  246. package/dist/design/audit/reference/engine/wiring.js +80 -0
  247. package/dist/design/audit/reference/engine/wiring.js.map +1 -0
  248. package/dist/design/audit/reference/eval/taste-core.d.ts +77 -0
  249. package/dist/design/audit/reference/eval/taste-core.d.ts.map +1 -0
  250. package/dist/design/audit/reference/eval/taste-core.js +95 -0
  251. package/dist/design/audit/reference/eval/taste-core.js.map +1 -0
  252. package/dist/design/audit/reference/generate/generator.d.ts +49 -0
  253. package/dist/design/audit/reference/generate/generator.d.ts.map +1 -0
  254. package/dist/design/audit/reference/generate/generator.js +94 -0
  255. package/dist/design/audit/reference/generate/generator.js.map +1 -0
  256. package/dist/design/audit/reference/generate/parse.d.ts +28 -0
  257. package/dist/design/audit/reference/generate/parse.d.ts.map +1 -0
  258. package/dist/design/audit/reference/generate/parse.js +180 -0
  259. package/dist/design/audit/reference/generate/parse.js.map +1 -0
  260. package/dist/design/audit/reference/generate/prompt.d.ts +36 -0
  261. package/dist/design/audit/reference/generate/prompt.d.ts.map +1 -0
  262. package/dist/design/audit/reference/generate/prompt.js +124 -0
  263. package/dist/design/audit/reference/generate/prompt.js.map +1 -0
  264. package/dist/design/audit/reference/index.d.ts +26 -0
  265. package/dist/design/audit/reference/index.d.ts.map +1 -0
  266. package/dist/design/audit/reference/index.js +31 -0
  267. package/dist/design/audit/reference/index.js.map +1 -0
  268. package/dist/design/audit/reference/judge/image-clamp.d.ts +34 -0
  269. package/dist/design/audit/reference/judge/image-clamp.d.ts.map +1 -0
  270. package/dist/design/audit/reference/judge/image-clamp.js +88 -0
  271. package/dist/design/audit/reference/judge/image-clamp.js.map +1 -0
  272. package/dist/design/audit/reference/judge/pairwise.d.ts +34 -0
  273. package/dist/design/audit/reference/judge/pairwise.d.ts.map +1 -0
  274. package/dist/design/audit/reference/judge/pairwise.js +105 -0
  275. package/dist/design/audit/reference/judge/pairwise.js.map +1 -0
  276. package/dist/design/audit/reference/judge/parse.d.ts +31 -0
  277. package/dist/design/audit/reference/judge/parse.d.ts.map +1 -0
  278. package/dist/design/audit/reference/judge/parse.js +113 -0
  279. package/dist/design/audit/reference/judge/parse.js.map +1 -0
  280. package/dist/design/audit/reference/judge/prompt.d.ts +38 -0
  281. package/dist/design/audit/reference/judge/prompt.d.ts.map +1 -0
  282. package/dist/design/audit/reference/judge/prompt.js +111 -0
  283. package/dist/design/audit/reference/judge/prompt.js.map +1 -0
  284. package/dist/design/audit/reference/judge/quality.d.ts +28 -0
  285. package/dist/design/audit/reference/judge/quality.d.ts.map +1 -0
  286. package/dist/design/audit/reference/judge/quality.js +62 -0
  287. package/dist/design/audit/reference/judge/quality.js.map +1 -0
  288. package/dist/design/audit/reference/judge/rank.d.ts +41 -0
  289. package/dist/design/audit/reference/judge/rank.d.ts.map +1 -0
  290. package/dist/design/audit/reference/judge/rank.js +196 -0
  291. package/dist/design/audit/reference/judge/rank.js.map +1 -0
  292. package/dist/design/audit/reference/judge/text-judge.d.ts +31 -0
  293. package/dist/design/audit/reference/judge/text-judge.d.ts.map +1 -0
  294. package/dist/design/audit/reference/judge/text-judge.js +43 -0
  295. package/dist/design/audit/reference/judge/text-judge.js.map +1 -0
  296. package/dist/design/audit/reference/judge/vision-judge.d.ts +59 -0
  297. package/dist/design/audit/reference/judge/vision-judge.d.ts.map +1 -0
  298. package/dist/design/audit/reference/judge/vision-judge.js +150 -0
  299. package/dist/design/audit/reference/judge/vision-judge.js.map +1 -0
  300. package/dist/design/audit/reference/judge/vision-model.d.ts +65 -0
  301. package/dist/design/audit/reference/judge/vision-model.d.ts.map +1 -0
  302. package/dist/design/audit/reference/judge/vision-model.js +110 -0
  303. package/dist/design/audit/reference/judge/vision-model.js.map +1 -0
  304. package/dist/design/audit/reference/pipeline/evaluate-reference.d.ts +68 -0
  305. package/dist/design/audit/reference/pipeline/evaluate-reference.d.ts.map +1 -0
  306. package/dist/design/audit/reference/pipeline/evaluate-reference.js +52 -0
  307. package/dist/design/audit/reference/pipeline/evaluate-reference.js.map +1 -0
  308. package/dist/design/audit/reference/reference-context.d.ts +31 -0
  309. package/dist/design/audit/reference/reference-context.d.ts.map +1 -0
  310. package/dist/design/audit/reference/reference-context.js +73 -0
  311. package/dist/design/audit/reference/reference-context.js.map +1 -0
  312. package/dist/design/audit/reference/retrieval/embedding-hash.d.ts +30 -0
  313. package/dist/design/audit/reference/retrieval/embedding-hash.d.ts.map +1 -0
  314. package/dist/design/audit/reference/retrieval/embedding-hash.js +87 -0
  315. package/dist/design/audit/reference/retrieval/embedding-hash.js.map +1 -0
  316. package/dist/design/audit/reference/retrieval/embedding-openai.d.ts +38 -0
  317. package/dist/design/audit/reference/retrieval/embedding-openai.d.ts.map +1 -0
  318. package/dist/design/audit/reference/retrieval/embedding-openai.js +73 -0
  319. package/dist/design/audit/reference/retrieval/embedding-openai.js.map +1 -0
  320. package/dist/design/audit/reference/retrieval/matcher.d.ts +34 -0
  321. package/dist/design/audit/reference/retrieval/matcher.d.ts.map +1 -0
  322. package/dist/design/audit/reference/retrieval/matcher.js +107 -0
  323. package/dist/design/audit/reference/retrieval/matcher.js.map +1 -0
  324. package/dist/design/audit/reference/run.d.ts +59 -0
  325. package/dist/design/audit/reference/run.d.ts.map +1 -0
  326. package/dist/design/audit/reference/run.js +99 -0
  327. package/dist/design/audit/reference/run.js.map +1 -0
  328. package/dist/design/audit/report.d.ts +35 -0
  329. package/dist/design/audit/report.d.ts.map +1 -0
  330. package/dist/design/audit/report.js +195 -0
  331. package/dist/design/audit/report.js.map +1 -0
  332. package/dist/design/audit/tokens/extract.d.ts +71 -0
  333. package/dist/design/audit/tokens/extract.d.ts.map +1 -0
  334. package/dist/design/audit/tokens/extract.js +1034 -0
  335. package/dist/design/audit/tokens/extract.js.map +1 -0
  336. package/dist/design/audit/types.d.ts +8 -0
  337. package/dist/design/audit/types.d.ts.map +1 -1
  338. package/dist/design/compare.d.ts.map +1 -1
  339. package/dist/design/compare.js +2 -6
  340. package/dist/design/compare.js.map +1 -1
  341. package/dist/design/cookie-consent.d.ts +3 -0
  342. package/dist/design/cookie-consent.d.ts.map +1 -0
  343. package/dist/design/cookie-consent.js +26 -0
  344. package/dist/design/cookie-consent.js.map +1 -0
  345. package/dist/design/viewports.d.ts +18 -0
  346. package/dist/design/viewports.d.ts.map +1 -0
  347. package/dist/design/viewports.js +10 -0
  348. package/dist/design/viewports.js.map +1 -0
  349. package/dist/index.d.ts +2 -2
  350. package/dist/index.d.ts.map +1 -1
  351. package/dist/index.js +1 -1
  352. package/dist/index.js.map +1 -1
  353. package/dist/memory/store.d.ts +19 -1
  354. package/dist/memory/store.d.ts.map +1 -1
  355. package/dist/memory/store.js +47 -0
  356. package/dist/memory/store.js.map +1 -1
  357. package/dist/provider-defaults.d.ts +18 -0
  358. package/dist/provider-defaults.d.ts.map +1 -1
  359. package/dist/provider-defaults.js +33 -0
  360. package/dist/provider-defaults.js.map +1 -1
  361. package/dist/runner/allowed-domains.d.ts +11 -0
  362. package/dist/runner/allowed-domains.d.ts.map +1 -0
  363. package/dist/runner/allowed-domains.js +14 -0
  364. package/dist/runner/allowed-domains.js.map +1 -0
  365. package/dist/runner/batch-fill.d.ts +28 -0
  366. package/dist/runner/batch-fill.d.ts.map +1 -0
  367. package/dist/runner/batch-fill.js +80 -0
  368. package/dist/runner/batch-fill.js.map +1 -0
  369. package/dist/runner/completion-policy.d.ts +26 -0
  370. package/dist/runner/completion-policy.d.ts.map +1 -0
  371. package/dist/runner/completion-policy.js +76 -0
  372. package/dist/runner/completion-policy.js.map +1 -0
  373. package/dist/runner/constants.d.ts +14 -0
  374. package/dist/runner/constants.d.ts.map +1 -0
  375. package/dist/runner/constants.js +23 -0
  376. package/dist/runner/constants.js.map +1 -0
  377. package/dist/runner/decision-screenshot.d.ts +24 -0
  378. package/dist/runner/decision-screenshot.d.ts.map +1 -0
  379. package/dist/runner/decision-screenshot.js +23 -0
  380. package/dist/runner/decision-screenshot.js.map +1 -0
  381. package/dist/runner/domain-boundary.d.ts +36 -0
  382. package/dist/runner/domain-boundary.d.ts.map +1 -0
  383. package/dist/runner/domain-boundary.js +81 -0
  384. package/dist/runner/domain-boundary.js.map +1 -0
  385. package/dist/runner/effect-verify.d.ts +28 -0
  386. package/dist/runner/effect-verify.d.ts.map +1 -0
  387. package/dist/runner/effect-verify.js +42 -0
  388. package/dist/runner/effect-verify.js.map +1 -0
  389. package/dist/runner/execute-plan.d.ts +58 -0
  390. package/dist/runner/execute-plan.d.ts.map +1 -0
  391. package/dist/runner/execute-plan.js +426 -0
  392. package/dist/runner/execute-plan.js.map +1 -0
  393. package/dist/runner/max-turns-extension.d.ts +19 -0
  394. package/dist/runner/max-turns-extension.d.ts.map +1 -0
  395. package/dist/runner/max-turns-extension.js +25 -0
  396. package/dist/runner/max-turns-extension.js.map +1 -0
  397. package/dist/runner/micro-plan.d.ts +11 -0
  398. package/dist/runner/micro-plan.d.ts.map +1 -0
  399. package/dist/runner/micro-plan.js +29 -0
  400. package/dist/runner/micro-plan.js.map +1 -0
  401. package/dist/runner/prompt-snippets.d.ts +5 -0
  402. package/dist/runner/prompt-snippets.d.ts.map +1 -0
  403. package/dist/runner/prompt-snippets.js +13 -0
  404. package/dist/runner/prompt-snippets.js.map +1 -0
  405. package/dist/runner/replay/contracts.d.ts +300 -0
  406. package/dist/runner/replay/contracts.d.ts.map +1 -0
  407. package/dist/runner/replay/contracts.js +42 -0
  408. package/dist/runner/replay/contracts.js.map +1 -0
  409. package/dist/runner/replay/controller.d.ts +27 -0
  410. package/dist/runner/replay/controller.d.ts.map +1 -0
  411. package/dist/runner/replay/controller.js +197 -0
  412. package/dist/runner/replay/controller.js.map +1 -0
  413. package/dist/runner/replay/guard.d.ts +28 -0
  414. package/dist/runner/replay/guard.d.ts.map +1 -0
  415. package/dist/runner/replay/guard.js +100 -0
  416. package/dist/runner/replay/guard.js.map +1 -0
  417. package/dist/runner/runner.d.ts +44 -51
  418. package/dist/runner/runner.d.ts.map +1 -1
  419. package/dist/runner/runner.js +155 -851
  420. package/dist/runner/runner.js.map +1 -1
  421. package/dist/runner/scout-feedback.d.ts +51 -0
  422. package/dist/runner/scout-feedback.d.ts.map +1 -0
  423. package/dist/runner/scout-feedback.js +149 -0
  424. package/dist/runner/scout-feedback.js.map +1 -0
  425. package/dist/skills/macro-loader.d.ts +1 -1
  426. package/dist/test-runner.d.ts.map +1 -1
  427. package/dist/test-runner.js +25 -0
  428. package/dist/test-runner.js.map +1 -1
  429. package/dist/types/actions.d.ts +241 -0
  430. package/dist/types/actions.d.ts.map +1 -0
  431. package/dist/types/actions.js +5 -0
  432. package/dist/types/actions.js.map +1 -0
  433. package/dist/types/config.d.ts +211 -0
  434. package/dist/types/config.d.ts.map +1 -0
  435. package/dist/types/config.js +2 -0
  436. package/dist/types/config.js.map +1 -0
  437. package/dist/types/design-audit.d.ts +92 -0
  438. package/dist/types/design-audit.d.ts.map +1 -0
  439. package/dist/types/design-audit.js +5 -0
  440. package/dist/types/design-audit.js.map +1 -0
  441. package/dist/types/design-tokens.d.ts +138 -0
  442. package/dist/types/design-tokens.d.ts.map +1 -0
  443. package/dist/types/design-tokens.js +5 -0
  444. package/dist/types/design-tokens.js.map +1 -0
  445. package/dist/types/page.d.ts +16 -0
  446. package/dist/types/page.d.ts.map +1 -0
  447. package/dist/types/page.js +2 -0
  448. package/dist/types/page.js.map +1 -0
  449. package/dist/types/plan.d.ts +50 -0
  450. package/dist/types/plan.d.ts.map +1 -0
  451. package/dist/types/plan.js +2 -0
  452. package/dist/types/plan.js.map +1 -0
  453. package/dist/types/preview.d.ts +9 -0
  454. package/dist/types/preview.d.ts.map +1 -0
  455. package/dist/types/preview.js +5 -0
  456. package/dist/types/preview.js.map +1 -0
  457. package/dist/types/result.d.ts +62 -0
  458. package/dist/types/result.d.ts.map +1 -0
  459. package/dist/types/result.js +2 -0
  460. package/dist/types/result.js.map +1 -0
  461. package/dist/types/scenario.d.ts +20 -0
  462. package/dist/types/scenario.d.ts.map +1 -0
  463. package/dist/types/scenario.js +5 -0
  464. package/dist/types/scenario.js.map +1 -0
  465. package/dist/types/test-runner.d.ts +145 -0
  466. package/dist/types/test-runner.d.ts.map +1 -0
  467. package/dist/types/test-runner.js +10 -0
  468. package/dist/types/test-runner.js.map +1 -0
  469. package/dist/types/trajectory.d.ts +38 -0
  470. package/dist/types/trajectory.d.ts.map +1 -0
  471. package/dist/types/trajectory.js +2 -0
  472. package/dist/types/trajectory.js.map +1 -0
  473. package/dist/types/turn.d.ts +50 -0
  474. package/dist/types/turn.d.ts.map +1 -0
  475. package/dist/types/turn.js +2 -0
  476. package/dist/types/turn.js.map +1 -0
  477. package/dist/types.d.ts +16 -1017
  478. package/dist/types.d.ts.map +1 -1
  479. package/dist/types.js +15 -8
  480. package/dist/types.js.map +1 -1
  481. package/dist/wallet/rpc-interception.d.ts +15 -0
  482. package/dist/wallet/rpc-interception.d.ts.map +1 -0
  483. package/dist/wallet/rpc-interception.js +95 -0
  484. package/dist/wallet/rpc-interception.js.map +1 -0
  485. package/package.json +7 -3
@@ -6,71 +6,17 @@
6
6
  */
7
7
  import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
- import { execSync } from 'node:child_process';
10
9
  import chalk from 'chalk';
11
10
  import { chromium } from 'playwright';
12
11
  import { Brain } from './brain/index.js';
13
12
  import { PlaywrightDriver } from './drivers/playwright.js';
14
- import { resolveProviderApiKey, resolveProviderModelName } from './provider-defaults.js';
13
+ import { resolveDefaultProvider, resolveProviderApiKey, resolveProviderModelName, isSupportedProvider, SUPPORTED_PROVIDERS } from './provider-defaults.js';
15
14
  import { loadLocalEnvFiles } from './env-loader.js';
16
15
  import { cliError } from './cli-ui.js';
17
16
  import { auditOnePage } from './design/audit/pipeline.js';
18
- /** Split "a, b , c" → ['a','b','c']. Returns undefined for empty input so the
19
- * layered predicates can distinguish "operator did not say" from "[]". */
20
- function parseTagList(input) {
21
- if (!input)
22
- return undefined;
23
- const tags = input.split(',').map(s => s.trim()).filter(Boolean);
24
- return tags.length > 0 ? tags : undefined;
25
- }
26
- /** Pretty-print the ethics-violation report for a set of pages. Prints
27
- * nothing when no page tripped a rule. Each rule is shown with severity,
28
- * remediation, and citation so the operator can act without re-running. */
29
- function printEthicsViolations(pages) {
30
- const offenders = pages.filter(p => (p.ethicsViolations?.length ?? 0) > 0);
31
- if (offenders.length === 0)
32
- return;
33
- console.log('');
34
- console.log(` ${chalk.bgRed.white.bold(' ETHICS VIOLATIONS ')}`);
35
- for (const page of offenders) {
36
- console.log(` ${chalk.dim('Page:')} ${page.url}`);
37
- for (const v of page.ethicsViolations ?? []) {
38
- const sevColor = v.severity === 'critical-floor' ? chalk.red : chalk.yellow;
39
- console.log(` ${sevColor('•')} ${chalk.bold(v.ruleId)} ${chalk.dim('—')} ${sevColor(v.severity)} ${chalk.dim(`(rollup capped at ${v.rollupCap})`)}`);
40
- console.log(` ${chalk.dim('fix:')} ${v.remediation}`);
41
- if (v.citation)
42
- console.log(` ${chalk.dim('cite:')} ${v.citation}`);
43
- }
44
- }
45
- }
46
- /** Lowest rollup cap across all violated pages, or undefined if none fired. */
47
- function lowestRollupCap(pages) {
48
- const caps = pages.flatMap(p => p.ethicsViolations ?? []).map(v => v.rollupCap);
49
- return caps.length === 0 ? undefined : Math.min(...caps);
50
- }
51
- /**
52
- * Layer 1 — print the per-dimension breakdown for one page when an
53
- * `auditResult` is attached. Five dim lines + one rollup line; each shows
54
- * score, range, and confidence so an agent can reason about uncertainty.
55
- */
56
- function printScoreBreakdown(page) {
57
- const result = page.auditResult;
58
- if (!result || !result.scores || !result.rollup)
59
- return;
60
- const dimOrder = ['product_intent', 'visual_craft', 'trust_clarity', 'workflow', 'content_ia'];
61
- for (const dim of dimOrder) {
62
- const s = result.scores[dim];
63
- if (!s)
64
- continue;
65
- const sevColor = s.score >= 8 ? chalk.green : s.score >= 5 ? chalk.yellow : chalk.red;
66
- const confColor = s.confidence === 'high' ? chalk.green : s.confidence === 'medium' ? chalk.yellow : chalk.dim;
67
- console.log(` ${chalk.dim(dim.padEnd(15))} ${sevColor(`${s.score}/10`)} ${chalk.dim(`[${s.range[0]}-${s.range[1]}]`)} ${confColor(s.confidence)}`);
68
- }
69
- const r = result.rollup;
70
- const rColor = r.score >= 8 ? chalk.green : r.score >= 5 ? chalk.yellow : chalk.red;
71
- const confColor = r.confidence === 'high' ? chalk.green : r.confidence === 'medium' ? chalk.yellow : chalk.dim;
72
- console.log(` ${chalk.dim('rollup'.padEnd(15))} ${rColor(`${r.score.toFixed(1)}/10`)} ${chalk.dim(`[${r.range[0].toFixed(1)}-${r.range[1].toFixed(1)}]`)} ${confColor(r.confidence)} ${chalk.dim(r.rule)}`);
73
- }
17
+ import { extractDesignTokens, createZipBundle } from './design/audit/tokens/extract.js';
18
+ import { runEvolveLoop, runAgentEvolveLoop, generateEvolveReport, buildApplyPrompt } from './design/audit/evolve/index.js';
19
+ import { parseTagList, printEthicsViolations, lowestRollupCap, printScoreBreakdown, generateDesignReport, } from './design/audit/report.js';
74
20
  import { resolveAuditPasses } from './design/audit/evaluate.js';
75
21
  import { detectSystemicFindings, topByRoi } from './design/audit/roi.js';
76
22
  import { getTelemetry, setInvocation } from './telemetry/index.js';
@@ -122,122 +68,87 @@ async function discoverPages(page, startUrl, maxPages) {
122
68
  }
123
69
  return discovered;
124
70
  }
125
- // ---------------------------------------------------------------------------
126
- // Report generation
127
- // ---------------------------------------------------------------------------
128
- function generateReport(results, profile, topFixes = []) {
129
- const lines = [];
130
- const avgScore = results.length > 0
131
- ? results.reduce((sum, r) => sum + r.score, 0) / results.length
132
- : 0;
133
- const allFindings = results.flatMap(r => r.findings);
134
- const critical = allFindings.filter(f => f.severity === 'critical').length;
135
- const major = allFindings.filter(f => f.severity === 'major').length;
136
- const minor = allFindings.filter(f => f.severity === 'minor').length;
137
- const totalTokens = results.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0);
138
- lines.push('# Design Audit Report');
139
- lines.push('');
140
- if (profile) {
141
- lines.push(`**Profile:** ${profile}`);
71
+ /**
72
+ * Parse a `--judge-models` list (`"prov:model,prov:model"` or bare `"model"`)
73
+ * into `ModelRef[]`. Splits on `,` then the FIRST `:` (model names never contain
74
+ * one); a colon-less entry leaves `provider` unset so the wiring fills the
75
+ * ambient default. Blank entries are skipped; an empty/absent string ⇒ `[]`.
76
+ */
77
+ export function parseModelRefs(spec) {
78
+ if (!spec)
79
+ return [];
80
+ const refs = [];
81
+ for (const raw of spec.split(',')) {
82
+ const entry = raw.trim();
83
+ if (!entry)
84
+ continue;
85
+ const colon = entry.indexOf(':');
86
+ if (colon < 0) {
87
+ refs.push({ model: entry });
88
+ continue;
89
+ }
90
+ const provider = entry.slice(0, colon).trim();
91
+ const model = entry.slice(colon + 1).trim();
92
+ if (!model)
93
+ continue;
94
+ refs.push(provider ? { provider: provider, model } : { model });
142
95
  }
143
- // Surface per-page classification when present.
144
- const classifications = results
145
- .filter(r => r.classification)
146
- .map(r => `${r.url}: ${r.classification.type}/${r.classification.domain} (${r.classification.maturity})`);
147
- if (classifications.length > 0) {
148
- lines.push(`**Auto-classified:**`);
149
- for (const c of classifications)
150
- lines.push(`- ${c}`);
96
+ return refs;
97
+ }
98
+ /**
99
+ * Default location for the reference corpus — mirrors
100
+ * `reference/config.ts` `DEFAULT_REFERENCE_CONFIG.corpusDir`. Checked with a
101
+ * plain fs read (no engine import) so reference-grounded can be the default
102
+ * without forcing the heavy engine to load on the v1 path.
103
+ */
104
+ const DEFAULT_REFERENCE_CORPUS_DIR = 'bench/design/reference-corpus';
105
+ /** True iff the default corpus dir holds at least one exemplar (`.json` other
106
+ * than the tracked `sources.example.json`). Drives the corpus-aware default. */
107
+ function hasPopulatedCorpus() {
108
+ try {
109
+ return fs
110
+ .readdirSync(path.resolve(DEFAULT_REFERENCE_CORPUS_DIR))
111
+ .some((f) => f.endsWith('.json') && f !== 'sources.example.json');
151
112
  }
152
- lines.push(`**Pages audited:** ${results.length}`);
153
- lines.push(`**Overall score:** ${avgScore.toFixed(1)}/10`);
154
- lines.push(`**Findings:** ${allFindings.length} (${critical} critical, ${major} major, ${minor} minor)`);
155
- if (totalTokens > 0)
156
- lines.push(`**Tokens used:** ${totalTokens.toLocaleString()}`);
157
- lines.push('');
158
- // Score bar
159
- const scoreBar = '█'.repeat(Math.round(avgScore)) + '░'.repeat(10 - Math.round(avgScore));
160
- lines.push(`\`${scoreBar}\` ${avgScore.toFixed(1)}/10`);
161
- lines.push('');
162
- // ── Top Fixes (by ROI) — the headline section users actually read first ──
163
- if (topFixes.length > 0) {
164
- lines.push('## Top Fixes (by ROI)');
165
- lines.push('');
166
- lines.push('Fix these first — sorted by impact × blast / effort:');
167
- lines.push('');
168
- for (let i = 0; i < topFixes.length; i++) {
169
- const f = topFixes[i];
170
- const tags = [];
171
- if (f.pageCount && f.pageCount >= 2)
172
- tags.push(`appears on ${f.pageCount} pages`);
173
- if (f.blast === 'system')
174
- tags.push('SYSTEMIC');
175
- const tagStr = tags.length > 0 ? ` _(${tags.join(', ')})_` : '';
176
- const roiStr = f.roi !== undefined ? f.roi.toFixed(1) : '—';
177
- lines.push(`### ${i + 1}. [${f.severity}] ${f.description}${tagStr}`);
178
- lines.push('');
179
- lines.push(`- **ROI:** ${roiStr} · Impact ${f.impact ?? '—'} · Effort ${f.effort ?? '—'} · Blast ${f.blast ?? '—'}`);
180
- lines.push(`- **Location:** ${f.location}`);
181
- lines.push(`- **Fix:** ${f.suggestion}`);
182
- if (f.cssSelector && f.cssFix) {
183
- lines.push(`- **CSS:** \`${f.cssSelector} { ${f.cssFix} }\``);
184
- }
185
- lines.push('');
186
- }
187
- lines.push('---');
188
- lines.push('');
113
+ catch {
114
+ return false;
189
115
  }
190
- // Per-page results
191
- for (const result of results) {
192
- lines.push(`## ${result.url}`);
193
- lines.push('');
194
- const pageBar = '█'.repeat(Math.round(result.score)) + '░'.repeat(10 - Math.round(result.score));
195
- lines.push(`**Score:** \`${pageBar}\` ${result.score}/10`);
196
- if (result.summary)
197
- lines.push(`**Summary:** ${result.summary}`);
198
- if (result.error)
199
- lines.push(`**Error:** ${result.error}`);
200
- lines.push('');
201
- if (result.strengths.length > 0) {
202
- lines.push('**Strengths:**');
203
- for (const s of result.strengths)
204
- lines.push(`- ${s}`);
205
- lines.push('');
206
- }
207
- if (result.designSystemScore) {
208
- lines.push('**Design System Breakdown:**');
209
- lines.push('');
210
- const ds = result.designSystemScore;
211
- // Universal dimensions in fixed order, then any custom dimensions (alpha)
212
- const universal = ['layout', 'typography', 'color', 'spacing', 'components', 'interactions', 'accessibility', 'polish'];
213
- const custom = Object.keys(ds).filter(k => !universal.includes(k)).sort();
214
- for (const key of [...universal, ...custom]) {
215
- if (typeof ds[key] === 'number') {
216
- const bar = '█'.repeat(Math.round(ds[key])) + '░'.repeat(10 - Math.round(ds[key]));
217
- lines.push(`- ${key}: \`${bar}\` ${ds[key]}/10`);
218
- }
219
- }
220
- lines.push('');
221
- }
222
- if (result.findings.length > 0) {
223
- lines.push('**Findings:**');
224
- lines.push('');
225
- lines.push('| Sev | Category | Description | Location | Fix | CSS |');
226
- lines.push('|-----|----------|-------------|----------|-----|-----|');
227
- for (const f of result.findings) {
228
- const esc = (s) => s.replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 120);
229
- const icon = f.severity === 'critical' ? '🔴' : f.severity === 'major' ? '🟡' : '⚪';
230
- const cssFix = f.cssFix ? `\`${esc(f.cssFix)}\`` : '';
231
- lines.push(`| ${icon} ${f.severity} | ${f.category} | ${esc(f.description)} | ${esc(f.location)} | ${esc(f.suggestion)} | ${cssFix} |`);
232
- }
233
- lines.push('');
234
- }
235
- else if (!result.error) {
236
- lines.push('No issues found.');
237
- lines.push('');
238
- }
116
+ }
117
+ /**
118
+ * Acquire the reference-grounded eval bundle once: lazily load the engine,
119
+ * resolve the operator reference + corpus, and pick the taste judge. Reached
120
+ * only when reference-grounded mode is active, so the v1 path never loads the
121
+ * engine. Fails closed with a clear diagnostic (a module-load failure or an
122
+ * unresolvable reference) instead of an unhandled rejection.
123
+ */
124
+ async function setupReferenceGrounded(opts, modelName, provider) {
125
+ try {
126
+ const { resolveReferenceConfig, createFileCorpusStore, resolveReferenceContext } = await import('./design/audit/reference/index.js');
127
+ const { createPageDnaExtractor } = await import('./design/audit/reference/dna/page-adapter.js');
128
+ // Vision judge: `--judge vision` selects it; `--judge-models` customises the
129
+ // ensemble (and implies vision when `--judge` is omitted). With vision on but
130
+ // no explicit models, default to the audit's OWN provider/model so the screenshot
131
+ // judge "just works" with whatever key the run already uses. Subjects without a
132
+ // screenshot (unrendered directions) fall back to the text judge inside the engine.
133
+ const visionRefs = parseModelRefs(opts.judgeModels);
134
+ const wantsVision = opts.judge === 'vision' || (!opts.judge && visionRefs.length > 0);
135
+ const referenceConfig = resolveReferenceConfig({
136
+ model: modelName,
137
+ ...(wantsVision ? { judge: 'vision' } : opts.judge === 'text' ? { judge: 'text' } : {}),
138
+ ...(wantsVision
139
+ ? { visionModels: visionRefs.length > 0 ? visionRefs : [{ provider, model: modelName }] }
140
+ : {}),
141
+ });
142
+ const reference = await resolveReferenceContext(opts.reference, { extractor: createPageDnaExtractor() }, { headless: opts.headless ?? true });
143
+ const corpus = await createFileCorpusStore(referenceConfig.corpusDir).load();
144
+ const refLabel = reference ? `, reference ${opts.reference ?? ''}` : '';
145
+ console.log(` ${chalk.cyan('reference-grounded')} ${chalk.dim('—')} corpus ${chalk.bold(String(corpus.length))} exemplar${corpus.length !== 1 ? 's' : ''}${refLabel}`);
146
+ return { evalMode: 'reference-grounded', reference, corpus, referenceConfig };
147
+ }
148
+ catch (err) {
149
+ cliError(`reference-grounded setup failed: ${err instanceof Error ? err.message : String(err)}`);
150
+ process.exit(1);
239
151
  }
240
- return lines.join('\n');
241
152
  }
242
153
  export async function runDesignAudit(opts) {
243
154
  loadLocalEnvFiles(process.cwd());
@@ -250,7 +161,13 @@ export async function runDesignAudit(opts) {
250
161
  // selects a single type fragment instead of letting the classifier decide.
251
162
  const profile = opts.profile;
252
163
  const maxPages = opts.pages ?? 5;
253
- const provider = (opts.provider ?? 'claude-code');
164
+ // Validate a user-supplied --provider before casting, so an unknown value
165
+ // fails fast with a clear message instead of surfacing deep in the pipeline.
166
+ if (opts.provider !== undefined && !isSupportedProvider(opts.provider)) {
167
+ cliError(`unknown --provider "${opts.provider}". Supported: ${SUPPORTED_PROVIDERS.join(', ')}`);
168
+ process.exit(1);
169
+ }
170
+ const provider = (opts.provider ?? resolveDefaultProvider());
254
171
  const modelName = resolveProviderModelName(provider, opts.model);
255
172
  const apiKey = opts.apiKey ?? resolveProviderApiKey(provider);
256
173
  const auditPasses = resolveAuditPasses(opts.auditPasses);
@@ -266,6 +183,23 @@ export async function runDesignAudit(opts) {
266
183
  audienceVulnerability: parseTagList(opts.audienceVulnerability),
267
184
  modality: parseTagList(opts.modality),
268
185
  };
186
+ // ── Reference-grounded eval — acquire-once. Default: reference-grounded WHEN a
187
+ // populated corpus is present (it needs exemplars to ground against), else v1.
188
+ // `--reference`/`--reference-grounded` force it on; `--v1` forces it off. The
189
+ // corpus check is a plain fs read (no engine import), so the v1 path stays
190
+ // lazy: evalMode 'v1' yields an empty bundle, `...referenceCommonOpts` adds only
191
+ // `evalMode:'v1'`, and the pipeline's stage-6 branch is never entered. When ON,
192
+ // resolve the operator reference and load the exemplar corpus a SINGLE time here
193
+ // — before the page/rep loops — so a multi-page/rep run never re-reads them
194
+ // (protects the ±0.5 reproducibility gate). The engine is imported lazily. ──
195
+ const evalMode = opts.v1
196
+ ? 'v1'
197
+ : opts.referenceGrounded || opts.reference || hasPopulatedCorpus()
198
+ ? 'reference-grounded'
199
+ : 'v1';
200
+ const referenceCommonOpts = evalMode === 'reference-grounded'
201
+ ? await setupReferenceGrounded(opts, modelName, provider)
202
+ : { evalMode };
269
203
  // Telemetry: every design-audit invocation gets a stable runId. Child
270
204
  // envelopes link back via parentRunId so fleet rollups can reconstruct
271
205
  // the run tree.
@@ -335,6 +269,7 @@ export async function runDesignAudit(opts) {
335
269
  provider,
336
270
  model: modelName,
337
271
  ...ethicsCommonOpts,
272
+ ...referenceCommonOpts,
338
273
  });
339
274
  const result = gen2;
340
275
  results.push(result);
@@ -356,8 +291,35 @@ export async function runDesignAudit(opts) {
356
291
  const deduped = detectSystemicFindings(perPage);
357
292
  topFixes = topByRoi(deduped, 5);
358
293
  }
294
+ // ── Reference-grounded: surface the rich redesign brief. Reference mode only —
295
+ // v1 results carry no `referenceArtifact`, so this block is skipped and both the
296
+ // report and the on-disk output are byte-identical to the legacy path. The
297
+ // render module is imported lazily (and is already module-cached in this mode),
298
+ // so a default audit never loads the reference engine. ──
299
+ let redesignSection;
300
+ if (evalMode === 'reference-grounded') {
301
+ const withArtifact = results.filter((r) => r.referenceArtifact);
302
+ if (withArtifact.length > 0) {
303
+ const { renderArtifactMarkdown, renderRedesignDirectionsSummary, renderRedesignTarget, artifactSlug } = await import('./design/audit/reference/index.js');
304
+ const sections = ['## Redesign directions', ''];
305
+ for (const r of withArtifact) {
306
+ const artifact = r.referenceArtifact;
307
+ const briefFile = `${artifactSlug(r.url)}.redesign.md`;
308
+ fs.writeFileSync(path.join(outputDir, briefFile), renderArtifactMarkdown(artifact));
309
+ sections.push(renderRedesignDirectionsSummary(artifact, briefFile));
310
+ console.log(` ${chalk.dim('Redesign brief →')} ${path.join(outputDir, briefFile)}`);
311
+ // Default (non-spawning) apply path: emit the implementation prompt a
312
+ // coding agent reads and runs ITSELF to apply this grounded redesign in
313
+ // its project. Spawning a coding agent stays opt-in (`--evolve --agent`).
314
+ const applyFile = `${artifactSlug(r.url)}.apply-prompt.md`;
315
+ fs.writeFileSync(path.join(outputDir, applyFile), buildApplyPrompt([r], profile, renderRedesignTarget(artifact)));
316
+ console.log(` ${chalk.dim('Apply prompt →')} ${path.join(outputDir, applyFile)}`);
317
+ }
318
+ redesignSection = sections.join('\n').trimEnd();
319
+ }
320
+ }
359
321
  // Generate report
360
- const report = generateReport(results, profile, topFixes);
322
+ const report = generateDesignReport(results, profile, topFixes, redesignSection);
361
323
  const reportPath = path.join(outputDir, 'report.md');
362
324
  fs.writeFileSync(reportPath, report);
363
325
  const allFindings = results.flatMap(r => r.findings);
@@ -420,6 +382,7 @@ export async function runDesignAudit(opts) {
420
382
  model: modelName,
421
383
  parentRunId: runId,
422
384
  ...ethicsCommonOpts,
385
+ ...referenceCommonOpts,
423
386
  });
424
387
  repResults.push(gen2);
425
388
  }
@@ -453,11 +416,11 @@ export async function runDesignAudit(opts) {
453
416
  if (evolveMode !== 'css') {
454
417
  // Agent-dispatched evolve — a coding agent edits the actual source code
455
418
  const projectDir = opts.projectDir ?? process.cwd();
456
- evolveResult = await runAgentEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3, evolveMode, projectDir, opts.debug, auditPasses, runId, provider, modelName);
419
+ evolveResult = await runAgentEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3, evolveMode, projectDir, opts.debug, auditPasses, runId, provider, modelName, referenceCommonOpts);
457
420
  }
458
421
  else {
459
422
  // CSS-injection evolve — ephemeral fixes injected into the browser page
460
- evolveResult = await runEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3, auditPasses, runId, provider, modelName);
423
+ evolveResult = await runEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3, auditPasses, runId, provider, modelName, referenceCommonOpts);
461
424
  }
462
425
  // Write evolve report
463
426
  const evolvePath = path.join(outputDir, 'evolve-report.md');
@@ -528,1478 +491,6 @@ export async function runDesignAudit(opts) {
528
491
  });
529
492
  await browser.close();
530
493
  }
531
- async function runEvolveLoop(brain, driver, page, pages, profile, initialResults, outputDir, maxRounds, auditPasses, parentRunId, provider, model) {
532
- const telemetry = parentRunId ? getTelemetry() : undefined;
533
- const initialAvg = initialResults.reduce((s, r) => s + r.score, 0) / initialResults.length;
534
- const scoreHistory = [initialAvg];
535
- const appliedFixes = [];
536
- const skippedFixes = [];
537
- let cumulativeCSS = '';
538
- let currentResults = initialResults;
539
- let currentAvg = initialAvg;
540
- console.log('');
541
- console.log(` ${chalk.bold('Design Evolve')} — ${maxRounds} rounds max`);
542
- console.log(` ${chalk.dim('Initial score:')} ${currentAvg.toFixed(1)}/10`);
543
- console.log('');
544
- for (let round = 1; round <= maxRounds; round++) {
545
- console.log(` ${chalk.dim(`Round ${round}/${maxRounds}`)}`);
546
- // Collect all findings with CSS fixes across all pages
547
- const fixableFixes = currentResults
548
- .flatMap(r => r.findings)
549
- .filter(f => f.cssSelector && f.cssFix);
550
- if (fixableFixes.length === 0) {
551
- console.log(` ${chalk.dim(' No CSS-fixable findings — generating fixes via LLM…')}`);
552
- // Ask the LLM to generate CSS fixes for the top findings
553
- const topFindings = currentResults
554
- .flatMap(r => r.findings)
555
- .filter(f => f.severity === 'critical' || f.severity === 'major')
556
- .slice(0, 10);
557
- if (topFindings.length === 0) {
558
- console.log(` ${chalk.green(' No major/critical findings remaining')}`);
559
- break;
560
- }
561
- const fixPrompt = buildFixGenerationPrompt(topFindings);
562
- const fixResult = await brain.auditDesign(await driver.observe(), 'Generate CSS fixes for the design issues listed below', [], fixPrompt);
563
- // Parse generated fixes
564
- try {
565
- let text = fixResult.raw.trim();
566
- if (text.startsWith('```'))
567
- text = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
568
- const start = text.indexOf('{');
569
- const end = text.lastIndexOf('}');
570
- if (start >= 0 && end > start)
571
- text = text.slice(start, end + 1);
572
- const parsed = JSON.parse(text);
573
- if (Array.isArray(parsed.fixes)) {
574
- for (const fix of parsed.fixes) {
575
- if (fix.cssSelector && fix.cssFix) {
576
- fixableFixes.push({
577
- category: 'ux',
578
- severity: 'major',
579
- description: fix.description || '',
580
- location: fix.location || '',
581
- suggestion: fix.cssFix,
582
- cssSelector: fix.cssSelector,
583
- cssFix: fix.cssFix,
584
- });
585
- }
586
- }
587
- }
588
- }
589
- catch { /* failed to parse fixes */ }
590
- }
591
- if (fixableFixes.length === 0) {
592
- console.log(` ${chalk.dim(' Could not generate fixable CSS — stopping')}`);
593
- break;
594
- }
595
- // Build CSS override from all fixable findings
596
- const roundCSS = fixableFixes
597
- .map(f => `/* ${f.severity}: ${f.description?.slice(0, 80)} */\n${f.cssSelector} { ${f.cssFix} }`)
598
- .join('\n\n');
599
- cumulativeCSS += '\n' + roundCSS;
600
- // Track applied fixes
601
- for (const f of fixableFixes) {
602
- appliedFixes.push({
603
- cssSelector: f.cssSelector,
604
- cssFix: f.cssFix,
605
- finding: f.description,
606
- });
607
- }
608
- console.log(` ${chalk.dim(` Applying ${fixableFixes.length} CSS fixes…`)}`);
609
- // Re-audit each page with CSS injected
610
- const roundResults = [];
611
- for (const url of pages) {
612
- try {
613
- await page.goto(url, { waitUntil: 'networkidle', timeout: 20_000 }).catch(() => page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15_000 }));
614
- await page.waitForTimeout(1500);
615
- // Inject cumulative CSS fixes
616
- await page.addStyleTag({ content: cumulativeCSS });
617
- await page.waitForTimeout(500);
618
- // Take screenshot of fixed state
619
- const screenshotDir = path.join(outputDir, `screenshots-round-${round}`);
620
- fs.mkdirSync(screenshotDir, { recursive: true });
621
- const result = (await auditOnePage({
622
- brain,
623
- driver,
624
- page,
625
- url,
626
- profileOverride: profile,
627
- screenshotDir,
628
- auditPasses,
629
- ...(parentRunId ? { runId: parentRunId, parentRunId } : {}),
630
- ...(provider ? { provider } : {}),
631
- ...(model ? { model } : {}),
632
- }));
633
- roundResults.push(result);
634
- }
635
- catch {
636
- roundResults.push({
637
- url,
638
- score: currentAvg,
639
- summary: 'Re-audit failed',
640
- strengths: [],
641
- findings: [],
642
- error: 'Re-audit with CSS injection failed',
643
- });
644
- }
645
- }
646
- const roundAvg = roundResults.reduce((s, r) => s + r.score, 0) / roundResults.length;
647
- scoreHistory.push(roundAvg);
648
- const delta = roundAvg - currentAvg;
649
- const deltaStr = delta >= 0 ? chalk.green(`+${delta.toFixed(1)}`) : chalk.red(delta.toFixed(1));
650
- console.log(` ${chalk.dim(' Score:')} ${roundAvg.toFixed(1)}/10 (${deltaStr})`);
651
- if (telemetry && parentRunId) {
652
- telemetry.emit({
653
- kind: 'design-evolve-round',
654
- runId: parentRunId,
655
- parentRunId,
656
- ok: true,
657
- durationMs: 0, // round-level wall time would require a per-round timer; the parent run captures total duration
658
- ...(provider && model ? { model: { provider, name: model } } : {}),
659
- data: {
660
- mode: 'css',
661
- round,
662
- fixesApplied: fixableFixes.length,
663
- },
664
- metrics: {
665
- round,
666
- beforeScore: currentAvg,
667
- afterScore: roundAvg,
668
- delta,
669
- fixesApplied: fixableFixes.length,
670
- },
671
- });
672
- }
673
- currentResults = roundResults;
674
- currentAvg = roundAvg;
675
- // Check convergence — if no improvement, stop
676
- if (delta <= 0.1 && round > 1) {
677
- console.log(` ${chalk.dim(' Converged — no further improvement')}`);
678
- break;
679
- }
680
- }
681
- const totalDelta = currentAvg - initialAvg;
682
- const deltaColor = totalDelta >= 2 ? chalk.green : totalDelta > 0 ? chalk.yellow : chalk.red;
683
- console.log('');
684
- console.log(` ${chalk.bold('Evolve complete')}`);
685
- console.log(` ${chalk.dim('Score:')} ${initialAvg.toFixed(1)} → ${currentAvg.toFixed(1)} (${deltaColor(`+${totalDelta.toFixed(1)}`)})`);
686
- console.log(` ${chalk.dim('Rounds:')} ${scoreHistory.length - 1}`);
687
- console.log(` ${chalk.dim('Fixes applied:')} ${appliedFixes.length}`);
688
- console.log('');
689
- return {
690
- beforeScore: initialAvg,
691
- afterScore: currentAvg,
692
- delta: totalDelta,
693
- rounds: scoreHistory.length - 1,
694
- appliedFixes,
695
- skippedFixes,
696
- scoreHistory,
697
- cssOverride: cumulativeCSS.trim(),
698
- };
699
- }
700
- function buildFixGenerationPrompt(findings) {
701
- const findingList = findings.map((f, i) => `${i + 1}. [${f.severity}/${f.category}] ${f.description}\n Location: ${f.location}\n Suggestion: ${f.suggestion}`).join('\n');
702
- return `You are a CSS engineer fixing design issues. For each finding, generate a precise CSS fix.
703
-
704
- FINDINGS TO FIX:
705
- ${findingList}
706
-
707
- RULES:
708
- - Use specific, targeted CSS selectors. Prefer class-based or semantic selectors.
709
- - Each fix should be a single CSS rule (selector + property:value pairs).
710
- - Fixes must not break other elements — be surgical.
711
- - For spacing: use consistent values (multiples of 4 or 8px).
712
- - For colors: ensure WCAG AA contrast (4.5:1 for text, 3:1 for large text).
713
- - For typography: use a limited scale (14px, 16px, 20px, 24px, 32px, 48px).
714
-
715
- RESPOND WITH ONLY a JSON object:
716
- {
717
- "fixes": [
718
- {
719
- "cssSelector": "main > section:first-child",
720
- "cssFix": "padding-bottom: 48px; margin-bottom: 0",
721
- "description": "Standardize hero section bottom spacing",
722
- "location": "Hero → features transition"
723
- }
724
- ]
725
- }`;
726
- }
727
- function generateEvolveReport(result) {
728
- const lines = [];
729
- lines.push('# Design Evolve Report');
730
- lines.push('');
731
- lines.push(`**Score:** ${result.beforeScore.toFixed(1)} → ${result.afterScore.toFixed(1)} (+${result.delta.toFixed(1)})`);
732
- lines.push(`**Rounds:** ${result.rounds}`);
733
- lines.push(`**Score progression:** ${result.scoreHistory.map(s => s.toFixed(1)).join(' → ')}`);
734
- lines.push('');
735
- if (result.appliedFixes.length > 0) {
736
- lines.push('## Applied Fixes');
737
- lines.push('');
738
- for (const fix of result.appliedFixes) {
739
- lines.push(`- \`${fix.cssSelector}\`: \`${fix.cssFix}\``);
740
- if (fix.finding)
741
- lines.push(` - ${fix.finding}`);
742
- }
743
- lines.push('');
744
- }
745
- if (result.cssOverride) {
746
- lines.push('## Generated CSS Override');
747
- lines.push('');
748
- lines.push('```css');
749
- lines.push(result.cssOverride);
750
- lines.push('```');
751
- lines.push('');
752
- lines.push('Apply this CSS to your app to fix the identified design issues:');
753
- lines.push('```html');
754
- lines.push('<link rel="stylesheet" href="design-fixes.css">');
755
- lines.push('```');
756
- }
757
- return lines.join('\n');
758
- }
759
- // ---------------------------------------------------------------------------
760
- // Agent-dispatched evolve — sends findings to a coding agent that edits source
761
- // ---------------------------------------------------------------------------
762
- const AGENT_COMMANDS = {
763
- 'claude-code': (prompt, dir) => ['claude', '-p', prompt, '--dangerously-skip-permissions', '--add-dir', dir],
764
- 'codex': (prompt, dir) => ['codex', 'exec', prompt, '-c', `cwd="${dir}"`],
765
- 'opencode': (prompt, dir) => ['opencode', 'run', prompt],
766
- };
767
- function resolveAgentCommand(agent, prompt, projectDir) {
768
- const builder = AGENT_COMMANDS[agent];
769
- if (builder) {
770
- const [cmd, ...args] = builder(prompt, projectDir);
771
- return { cmd, args, cwd: projectDir };
772
- }
773
- // Custom command — treat the agent string as a command template
774
- // e.g. "aider --message" becomes: aider --message "<prompt>"
775
- const parts = agent.split(/\s+/);
776
- return { cmd: parts[0], args: [...parts.slice(1), prompt], cwd: projectDir };
777
- }
778
- function buildAgentFixPrompt(results, profile, round) {
779
- const allFindings = results.flatMap(r => r.findings);
780
- const critical = allFindings.filter(f => f.severity === 'critical');
781
- const major = allFindings.filter(f => f.severity === 'major');
782
- const minor = allFindings.filter(f => f.severity === 'minor');
783
- const findingsList = [...critical, ...major, ...minor.slice(0, 5)]
784
- .map((f, i) => {
785
- let entry = `${i + 1}. [${f.severity}/${f.category}] ${f.description}`;
786
- entry += `\n Location: ${f.location}`;
787
- entry += `\n Suggestion: ${f.suggestion}`;
788
- if (f.cssSelector)
789
- entry += `\n CSS Selector: ${f.cssSelector}`;
790
- if (f.cssFix)
791
- entry += `\n CSS Fix: ${f.cssFix}`;
792
- return entry;
793
- })
794
- .join('\n\n');
795
- const scoreBreakdowns = results
796
- .filter(r => r.designSystemScore)
797
- .map(r => {
798
- const ds = r.designSystemScore;
799
- return ` ${r.url}: ${Object.entries(ds).map(([k, v]) => `${k}=${v}`).join(', ')}`;
800
- })
801
- .join('\n');
802
- return `You are fixing design issues found by an automated design audit.
803
-
804
- AUDIT PROFILE: ${profile}
805
- ROUND: ${round} (${round === 1 ? 'initial fixes' : 'fixing remaining issues from previous round'})
806
- CURRENT SCORES:
807
- Overall: ${(results.reduce((s, r) => s + r.score, 0) / results.length).toFixed(1)}/10
808
- ${scoreBreakdowns}
809
-
810
- FINDINGS TO FIX (${critical.length} critical, ${major.length} major, ${minor.length} minor):
811
-
812
- ${findingsList}
813
-
814
- INSTRUCTIONS:
815
- 1. Read the project's source files to understand the styling approach (Tailwind, CSS modules, plain CSS, styled-components, etc.)
816
- 2. Fix the findings by editing the ACTUAL SOURCE FILES — not by creating new CSS override files
817
- 3. Match the project's existing styling conventions
818
- 4. Fix the design SYSTEM (shared components, tokens, globals) not individual instances
819
- 5. Prioritize critical and major findings
820
- 6. Only change visual/styling properties — never change business logic, state, or event handlers
821
- 7. After making changes, verify the dev server is still running (no build errors)
822
-
823
- Do NOT:
824
- - Create new standalone CSS override files — edit the existing styles
825
- - Add comments explaining what you changed — just change it
826
- - Refactor unrelated code
827
- - Change component structure or HTML semantics unless a finding specifically requires it`;
828
- }
829
- async function runAgentEvolveLoop(brain, driver, page, pages, profile, initialResults, outputDir, maxRounds, agentName, projectDir, debug, auditPasses, parentRunId, provider, model) {
830
- const telemetry = parentRunId ? getTelemetry() : undefined;
831
- const initialAvg = initialResults.reduce((s, r) => s + r.score, 0) / initialResults.length;
832
- const scoreHistory = [initialAvg];
833
- const appliedFixes = [];
834
- let currentResults = initialResults;
835
- let currentAvg = initialAvg;
836
- const resolvedProjectDir = path.resolve(projectDir);
837
- if (!fs.existsSync(resolvedProjectDir)) {
838
- cliError(`project directory not found: ${resolvedProjectDir}`);
839
- process.exit(1);
840
- }
841
- console.log('');
842
- console.log(` ${chalk.bold('Design Evolve')} ${chalk.dim('via')} ${chalk.cyan(agentName)}`);
843
- console.log(` ${chalk.dim('Project:')} ${resolvedProjectDir}`);
844
- console.log(` ${chalk.dim('Initial score:')} ${currentAvg.toFixed(1)}/10`);
845
- console.log(` ${chalk.dim('Max rounds:')} ${maxRounds}`);
846
- console.log('');
847
- for (let round = 1; round <= maxRounds; round++) {
848
- console.log(` ${chalk.dim(`Round ${round}/${maxRounds}`)}`);
849
- // Build the prompt for the agent
850
- const prompt = buildAgentFixPrompt(currentResults, profile, round);
851
- // Write the prompt to a file for debugging
852
- const promptPath = path.join(outputDir, `agent-prompt-round-${round}.txt`);
853
- fs.writeFileSync(promptPath, prompt);
854
- // Also write the full report JSON so the agent could read it
855
- const findingsPath = path.join(outputDir, `findings-round-${round}.json`);
856
- fs.writeFileSync(findingsPath, JSON.stringify({
857
- round,
858
- score: currentAvg,
859
- results: currentResults.map(r => ({
860
- url: r.url,
861
- score: r.score,
862
- designSystemScore: r.designSystemScore,
863
- findings: r.findings,
864
- })),
865
- }, null, 2));
866
- // Dispatch to the coding agent
867
- const { cmd, args, cwd } = resolveAgentCommand(agentName, prompt, resolvedProjectDir);
868
- console.log(` ${chalk.dim(` Dispatching to ${agentName}…`)}`);
869
- if (debug) {
870
- console.log(` ${chalk.dim(` cmd: ${cmd} ${args.map(a => a.length > 80 ? a.slice(0, 80) + '…' : a).join(' ')}`)}`);
871
- }
872
- try {
873
- const result = execSync(`${cmd} ${args.map(a => JSON.stringify(a)).join(' ')}`, {
874
- cwd,
875
- stdio: debug ? 'inherit' : 'pipe',
876
- timeout: 300_000, // 5min max per agent round
877
- env: { ...process.env },
878
- });
879
- if (!debug && result) {
880
- const agentOutputPath = path.join(outputDir, `agent-output-round-${round}.txt`);
881
- fs.writeFileSync(agentOutputPath, result.toString());
882
- }
883
- console.log(` ${chalk.dim(' Agent completed')}`);
884
- }
885
- catch (err) {
886
- const exitCode = err.status ?? 'unknown';
887
- console.log(` ${chalk.yellow(` Agent exited with code ${exitCode} — continuing with re-audit`)}`);
888
- // Write stderr if available
889
- const stderr = err.stderr;
890
- if (stderr) {
891
- const errPath = path.join(outputDir, `agent-error-round-${round}.txt`);
892
- fs.writeFileSync(errPath, stderr.toString());
893
- }
894
- }
895
- // Wait for hot reload to settle
896
- console.log(` ${chalk.dim(' Waiting for hot reload…')}`);
897
- await new Promise(resolve => setTimeout(resolve, 5000));
898
- // Re-audit
899
- console.log(` ${chalk.dim(' Re-auditing…')}`);
900
- const roundResults = [];
901
- const roundScreenshotDir = path.join(outputDir, `screenshots-round-${round}`);
902
- fs.mkdirSync(roundScreenshotDir, { recursive: true });
903
- for (const url of pages) {
904
- const result = (await auditOnePage({
905
- brain,
906
- driver,
907
- page,
908
- url,
909
- profileOverride: profile,
910
- screenshotDir: roundScreenshotDir,
911
- auditPasses,
912
- ...(parentRunId ? { runId: parentRunId, parentRunId } : {}),
913
- ...(provider ? { provider } : {}),
914
- ...(model ? { model } : {}),
915
- }));
916
- roundResults.push(result);
917
- }
918
- const roundAvg = roundResults.reduce((s, r) => s + r.score, 0) / roundResults.length;
919
- scoreHistory.push(roundAvg);
920
- const delta = roundAvg - currentAvg;
921
- const deltaStr = delta >= 0 ? chalk.green(`+${delta.toFixed(1)}`) : chalk.red(delta.toFixed(1));
922
- console.log(` ${chalk.dim(' Score:')} ${roundAvg.toFixed(1)}/10 (${deltaStr})`);
923
- // Track what changed
924
- const prevFindingCount = currentResults.flatMap(r => r.findings).length;
925
- const newFindingCount = roundResults.flatMap(r => r.findings).length;
926
- const resolvedCount = Math.max(0, prevFindingCount - newFindingCount);
927
- if (resolvedCount > 0) {
928
- appliedFixes.push({
929
- cssSelector: `round-${round}`,
930
- cssFix: `${agentName} resolved ${resolvedCount} findings`,
931
- finding: `Score: ${currentAvg.toFixed(1)} → ${roundAvg.toFixed(1)}`,
932
- });
933
- }
934
- if (telemetry && parentRunId) {
935
- telemetry.emit({
936
- kind: 'design-evolve-round',
937
- runId: parentRunId,
938
- parentRunId,
939
- ok: true,
940
- durationMs: 0,
941
- ...(provider && model ? { model: { provider, name: model } } : {}),
942
- data: {
943
- mode: `agent:${agentName}`,
944
- round,
945
- resolvedCount,
946
- },
947
- metrics: {
948
- round,
949
- beforeScore: currentAvg,
950
- afterScore: roundAvg,
951
- delta,
952
- findingsResolved: resolvedCount,
953
- },
954
- });
955
- }
956
- currentResults = roundResults;
957
- currentAvg = roundAvg;
958
- // Check convergence
959
- if (delta <= 0.1 && round > 1) {
960
- console.log(` ${chalk.dim(' Converged — no further improvement')}`);
961
- break;
962
- }
963
- }
964
- const totalDelta = currentAvg - initialAvg;
965
- const deltaColor = totalDelta >= 2 ? chalk.green : totalDelta > 0 ? chalk.yellow : chalk.red;
966
- console.log('');
967
- console.log(` ${chalk.bold('Evolve complete')} ${chalk.dim('via')} ${chalk.cyan(agentName)}`);
968
- console.log(` ${chalk.dim('Score:')} ${initialAvg.toFixed(1)} → ${currentAvg.toFixed(1)} (${deltaColor(totalDelta >= 0 ? `+${totalDelta.toFixed(1)}` : totalDelta.toFixed(1))})`);
969
- console.log(` ${chalk.dim('Rounds:')} ${scoreHistory.length - 1}`);
970
- console.log('');
971
- return {
972
- beforeScore: initialAvg,
973
- afterScore: currentAvg,
974
- delta: totalDelta,
975
- rounds: scoreHistory.length - 1,
976
- appliedFixes,
977
- skippedFixes: [],
978
- scoreHistory,
979
- cssOverride: '', // no CSS override in agent mode — agent edited source directly
980
- };
981
- }
982
- // ---------------------------------------------------------------------------
983
- // Design Token Extraction — pure DOM, no LLM calls
984
- // ---------------------------------------------------------------------------
985
- const VIEWPORTS = [
986
- { name: 'mobile', width: 375, height: 812 },
987
- { name: 'tablet', width: 768, height: 1024 },
988
- { name: 'desktop', width: 1440, height: 900 },
989
- ];
990
- // The in-page extraction function — runs inside page.evaluate()
991
- function extractTokensFromDOM() {
992
- const MAX_ELEMENTS = 50_000;
993
- // --- CSS Custom Properties & Font Files ---
994
- const customProperties = {};
995
- const fontFiles = [];
996
- const externalStylesheetUrls = [];
997
- for (const sheet of document.styleSheets) {
998
- if (sheet.href && (sheet.href.startsWith('http://') || sheet.href.startsWith('https://'))) {
999
- externalStylesheetUrls.push(sheet.href);
1000
- }
1001
- try {
1002
- for (const rule of sheet.cssRules) {
1003
- if (rule instanceof CSSStyleRule &&
1004
- (rule.selectorText === ':root' || rule.selectorText?.includes('html') || rule.selectorText?.includes('body'))) {
1005
- for (let i = 0; i < rule.style.length; i++) {
1006
- const prop = rule.style[i];
1007
- if (prop.startsWith('--')) {
1008
- customProperties[prop] = rule.style.getPropertyValue(prop).trim();
1009
- }
1010
- }
1011
- }
1012
- if (rule instanceof CSSFontFaceRule) {
1013
- const family = rule.style.getPropertyValue('font-family').replace(/['"]/g, '').trim();
1014
- const weight = rule.style.getPropertyValue('font-weight') || '400';
1015
- const style = rule.style.getPropertyValue('font-style') || 'normal';
1016
- const src = rule.style.getPropertyValue('src');
1017
- const urlMatch = src.match(/url\(["']?([^"')]+)["']?\)/);
1018
- const formatMatch = src.match(/format\(["']?([^"')]+)["']?\)/);
1019
- if (urlMatch) {
1020
- fontFiles.push({
1021
- family,
1022
- weight,
1023
- style,
1024
- src: urlMatch[1],
1025
- format: formatMatch?.[1] || 'unknown',
1026
- });
1027
- }
1028
- }
1029
- }
1030
- }
1031
- catch { /* cross-origin stylesheet */ }
1032
- }
1033
- // Supplement with document.fonts API (catches cross-origin @font-face)
1034
- const seenFontSrcs = new Set(fontFiles.map(f => f.src));
1035
- try {
1036
- for (const face of document.fonts) {
1037
- if (face.status === 'loaded') {
1038
- // face.family is the CSS font-family, face.weight/style are strings
1039
- // Check if we already captured this via @font-face rules
1040
- const family = face.family.replace(/['"]/g, '').trim();
1041
- const weight = face.weight || '400';
1042
- const style = face.style || 'normal';
1043
- // document.fonts doesn't expose the URL, but we can match against loaded resources
1044
- if (!fontFiles.some(f => f.family === family && f.weight === weight && f.style === style)) {
1045
- // Will be supplemented by Performance API below
1046
- }
1047
- }
1048
- }
1049
- }
1050
- catch { /* older browser */ }
1051
- // Use Performance API to find all loaded font resources (works cross-origin)
1052
- try {
1053
- const resources = performance.getEntriesByType('resource');
1054
- for (const r of resources) {
1055
- if (/\.(woff2?|ttf|otf|eot)(\?|$)/i.test(r.name) && !seenFontSrcs.has(r.name)) {
1056
- seenFontSrcs.add(r.name);
1057
- const formatMatch = r.name.match(/\.(woff2?|ttf|otf|eot)/i);
1058
- fontFiles.push({
1059
- family: '', // can't determine from URL alone
1060
- weight: '',
1061
- style: '',
1062
- src: r.name,
1063
- format: formatMatch?.[1] || 'unknown',
1064
- });
1065
- }
1066
- }
1067
- }
1068
- catch { /* Performance API not available */ }
1069
- // --- Element traversal ---
1070
- const colorMap = new Map();
1071
- const spacingMap = new Map();
1072
- const borderRadiusMap = new Map();
1073
- const shadowMap = new Map();
1074
- const fontFamilyMap = new Map();
1075
- const typeScaleMap = new Map();
1076
- const animationMap = new Map();
1077
- const backgroundImageUrls = new Set();
1078
- function addColor(value, property) {
1079
- if (!value || value === 'transparent' || value === 'rgba(0, 0, 0, 0)' || value === 'inherit' || value === 'initial')
1080
- return;
1081
- const entry = colorMap.get(value);
1082
- if (entry) {
1083
- entry.count++;
1084
- entry.properties.add(property);
1085
- }
1086
- else {
1087
- colorMap.set(value, { count: 1, properties: new Set([property]) });
1088
- }
1089
- }
1090
- function addSpacing(value, property) {
1091
- if (!value || value === '0px' || value === 'auto' || value === 'normal')
1092
- return;
1093
- // Normalize multi-value shorthands — take individual values
1094
- const vals = value.split(/\s+/).filter(v => v !== '0px' && v !== 'auto');
1095
- for (const v of vals) {
1096
- if (!v.endsWith('px') && !v.endsWith('rem') && !v.endsWith('em'))
1097
- continue;
1098
- const entry = spacingMap.get(v);
1099
- if (entry) {
1100
- entry.count++;
1101
- entry.properties.add(property);
1102
- }
1103
- else {
1104
- spacingMap.set(v, { count: 1, properties: new Set([property]) });
1105
- }
1106
- }
1107
- }
1108
- function parseGradientColors(bg) {
1109
- const colorRegex = /(?:rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-fA-F]{3,8})/g;
1110
- const matches = bg.match(colorRegex) || [];
1111
- for (const c of matches)
1112
- addColor(c, 'gradient');
1113
- }
1114
- function parseShadowColors(shadow) {
1115
- if (shadow === 'none')
1116
- return;
1117
- const colorRegex = /(?:rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-fA-F]{3,8})/g;
1118
- const matches = shadow.match(colorRegex) || [];
1119
- for (const c of matches)
1120
- addColor(c, 'boxShadow');
1121
- }
1122
- const HEADING_TAGS = new Set(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
1123
- const MONO_FAMILIES = /mono|consolas|courier|fira\s*code|jetbrains|source\s*code/i;
1124
- let elementCount = 0;
1125
- // Per-element extraction — shared between light DOM walker and shadow DOM walker
1126
- function processElement(el) {
1127
- const htmlEl = el;
1128
- if (htmlEl.offsetWidth === 0 && htmlEl.offsetHeight === 0)
1129
- return;
1130
- const cs = getComputedStyle(el);
1131
- // Colors
1132
- addColor(cs.color, 'color');
1133
- addColor(cs.backgroundColor, 'backgroundColor');
1134
- addColor(cs.borderTopColor, 'borderColor');
1135
- addColor(cs.borderRightColor, 'borderColor');
1136
- addColor(cs.borderBottomColor, 'borderColor');
1137
- addColor(cs.borderLeftColor, 'borderColor');
1138
- addColor(cs.outlineColor, 'outlineColor');
1139
- if (cs.backgroundImage.includes('gradient'))
1140
- parseGradientColors(cs.backgroundImage);
1141
- if (cs.backgroundImage.includes('url(')) {
1142
- const urlMatches = cs.backgroundImage.matchAll(/url\(["']?([^"')]+)["']?\)/g);
1143
- for (const m of urlMatches) {
1144
- const u = m[1];
1145
- if (u && !u.startsWith('data:') && !u.startsWith('blob:'))
1146
- backgroundImageUrls.add(u);
1147
- }
1148
- }
1149
- if (cs.boxShadow !== 'none')
1150
- parseShadowColors(cs.boxShadow);
1151
- // Spacing
1152
- addSpacing(cs.paddingTop, 'padding');
1153
- addSpacing(cs.paddingRight, 'padding');
1154
- addSpacing(cs.paddingBottom, 'padding');
1155
- addSpacing(cs.paddingLeft, 'padding');
1156
- addSpacing(cs.marginTop, 'margin');
1157
- addSpacing(cs.marginRight, 'margin');
1158
- addSpacing(cs.marginBottom, 'margin');
1159
- addSpacing(cs.marginLeft, 'margin');
1160
- addSpacing(cs.gap, 'gap');
1161
- addSpacing(cs.rowGap, 'gap');
1162
- addSpacing(cs.columnGap, 'gap');
1163
- // Border radius
1164
- const br = cs.borderRadius;
1165
- if (br && br !== '0px') {
1166
- borderRadiusMap.set(br, (borderRadiusMap.get(br) ?? 0) + 1);
1167
- }
1168
- // Shadows
1169
- if (cs.boxShadow !== 'none') {
1170
- shadowMap.set(cs.boxShadow, (shadowMap.get(cs.boxShadow) ?? 0) + 1);
1171
- }
1172
- // Typography — only for elements with text content
1173
- if (el.childNodes.length > 0 && Array.from(el.childNodes).some(n => n.nodeType === Node.TEXT_NODE && n.textContent?.trim())) {
1174
- const family = cs.fontFamily;
1175
- const weight = parseInt(cs.fontWeight) || 400;
1176
- const isHeading = HEADING_TAGS.has(el.tagName);
1177
- const isMono = MONO_FAMILIES.test(family);
1178
- const fEntry = fontFamilyMap.get(family);
1179
- if (fEntry) {
1180
- fEntry.weights.add(weight);
1181
- if (isHeading)
1182
- fEntry.headingUse = true;
1183
- if (isMono)
1184
- fEntry.monoUse = true;
1185
- }
1186
- else {
1187
- fontFamilyMap.set(family, { weights: new Set([weight]), headingUse: isHeading, monoUse: isMono });
1188
- }
1189
- const scaleKey = `${cs.fontSize}|${cs.fontWeight}|${cs.lineHeight}|${cs.letterSpacing}|${family}`;
1190
- const scaleEntry = typeScaleMap.get(scaleKey);
1191
- if (scaleEntry) {
1192
- scaleEntry.count++;
1193
- }
1194
- else {
1195
- typeScaleMap.set(scaleKey, {
1196
- fontSize: cs.fontSize,
1197
- fontWeight: cs.fontWeight,
1198
- lineHeight: cs.lineHeight,
1199
- letterSpacing: cs.letterSpacing,
1200
- fontFamily: family,
1201
- tag: el.tagName.toLowerCase(),
1202
- count: 1,
1203
- });
1204
- }
1205
- }
1206
- // Animations/transitions
1207
- if (cs.transition && cs.transition !== 'all 0s ease 0s' && cs.transition !== 'none') {
1208
- const key = cs.transition;
1209
- const aEntry = animationMap.get(key);
1210
- if (aEntry) {
1211
- aEntry.count++;
1212
- }
1213
- else {
1214
- animationMap.set(key, { property: 'transition', value: cs.transition, count: 1 });
1215
- }
1216
- }
1217
- if (cs.animationName && cs.animationName !== 'none') {
1218
- const key = `animation:${cs.animationName}`;
1219
- const aEntry = animationMap.get(key);
1220
- if (aEntry) {
1221
- aEntry.count++;
1222
- }
1223
- else {
1224
- animationMap.set(key, { property: 'animation', value: `${cs.animationName} ${cs.animationDuration} ${cs.animationTimingFunction}`, count: 1 });
1225
- }
1226
- }
1227
- }
1228
- // --- Light DOM walk ---
1229
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
1230
- let el = walker.currentNode;
1231
- while (el && elementCount < MAX_ELEMENTS) {
1232
- elementCount++;
1233
- processElement(el);
1234
- el = walker.nextNode();
1235
- }
1236
- // --- Shadow DOM walk ---
1237
- function walkShadowRoots(root) {
1238
- const shadowWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
1239
- // currentNode starts at root (ShadowRoot, not Element) — advance to first element
1240
- let shadowEl = shadowWalker.nextNode();
1241
- while (shadowEl && elementCount < MAX_ELEMENTS) {
1242
- elementCount++;
1243
- processElement(shadowEl);
1244
- if (shadowEl.shadowRoot) {
1245
- walkShadowRoots(shadowEl.shadowRoot);
1246
- }
1247
- shadowEl = shadowWalker.nextNode();
1248
- }
1249
- }
1250
- const allElements = document.body.querySelectorAll('*');
1251
- for (const bodyEl of allElements) {
1252
- if (bodyEl.shadowRoot && elementCount < MAX_ELEMENTS) {
1253
- walkShadowRoots(bodyEl.shadowRoot);
1254
- }
1255
- }
1256
- // --- Components: Buttons ---
1257
- const buttonStyles = [];
1258
- const btnFingerprints = new Map();
1259
- const BUTTON_STYLE_KEYS = ['backgroundColor', 'color', 'borderRadius', 'padding', 'fontSize', 'fontWeight', 'border', 'boxShadow', 'textTransform', 'letterSpacing'];
1260
- for (const btn of document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]')) {
1261
- const htmlBtn = btn;
1262
- if (htmlBtn.offsetWidth === 0 && htmlBtn.offsetHeight === 0)
1263
- continue;
1264
- const cs = getComputedStyle(btn);
1265
- const styles = {};
1266
- for (const k of BUTTON_STYLE_KEYS)
1267
- styles[k] = cs[k];
1268
- const fp = JSON.stringify(styles);
1269
- const existing = btnFingerprints.get(fp);
1270
- if (existing) {
1271
- existing.count++;
1272
- }
1273
- else {
1274
- const entry = { fingerprint: fp, count: 1, exampleText: htmlBtn.textContent?.trim().slice(0, 60) || '', styles };
1275
- btnFingerprints.set(fp, entry);
1276
- }
1277
- }
1278
- buttonStyles.push(...btnFingerprints.values());
1279
- // --- Components: Inputs ---
1280
- const inputStyles = [];
1281
- const inputFingerprints = new Map();
1282
- const INPUT_STYLE_KEYS = ['backgroundColor', 'color', 'borderRadius', 'padding', 'fontSize', 'border', 'boxShadow', 'outline'];
1283
- for (const inp of document.querySelectorAll('input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="number"], input[type="tel"], input[type="url"], textarea, select, [role="textbox"], [role="combobox"]')) {
1284
- const htmlInp = inp;
1285
- if (htmlInp.offsetWidth === 0 && htmlInp.offsetHeight === 0)
1286
- continue;
1287
- const cs = getComputedStyle(inp);
1288
- const styles = {};
1289
- for (const k of INPUT_STYLE_KEYS)
1290
- styles[k] = cs[k];
1291
- const fp = JSON.stringify(styles);
1292
- const existing = inputFingerprints.get(fp);
1293
- if (existing) {
1294
- existing.count++;
1295
- }
1296
- else {
1297
- const entry = { fingerprint: fp, count: 1, exampleText: inp.placeholder?.slice(0, 60) || '', styles };
1298
- inputFingerprints.set(fp, entry);
1299
- }
1300
- }
1301
- inputStyles.push(...inputFingerprints.values());
1302
- // --- Components: Cards (elements with shadow or border-radius + multiple children) ---
1303
- const cardStyles = [];
1304
- const cardFingerprints = new Map();
1305
- const CARD_STYLE_KEYS = ['borderRadius', 'boxShadow', 'padding', 'backgroundColor', 'border'];
1306
- for (const el of document.querySelectorAll('div, section, article, li')) {
1307
- const htmlEl = el;
1308
- if (htmlEl.offsetWidth === 0 || htmlEl.children.length < 2)
1309
- continue;
1310
- const cs = getComputedStyle(el);
1311
- if (cs.boxShadow === 'none' && cs.borderRadius === '0px' && cs.border === '0px none rgb(0, 0, 0)')
1312
- continue;
1313
- const styles = {};
1314
- for (const k of CARD_STYLE_KEYS)
1315
- styles[k] = cs[k];
1316
- const fp = JSON.stringify(styles);
1317
- const existing = cardFingerprints.get(fp);
1318
- if (existing) {
1319
- existing.count++;
1320
- }
1321
- else {
1322
- const entry = { fingerprint: fp, count: 1, styles };
1323
- cardFingerprints.set(fp, entry);
1324
- }
1325
- }
1326
- // Only keep card patterns with 2+ instances (skip one-off containers)
1327
- for (const c of cardFingerprints.values()) {
1328
- if (c.count >= 2)
1329
- cardStyles.push(c);
1330
- }
1331
- // --- Components: Navigation ---
1332
- const navPatterns = [];
1333
- for (const nav of document.querySelectorAll('nav, [role="navigation"], header')) {
1334
- const cs = getComputedStyle(nav);
1335
- const links = nav.querySelectorAll('a');
1336
- if (links.length === 0)
1337
- continue;
1338
- const firstLinkCs = getComputedStyle(links[0]);
1339
- navPatterns.push({
1340
- selector: nav.tagName.toLowerCase() + (nav.className ? '.' + nav.className.split(/\s+/)[0] : ''),
1341
- layout: {
1342
- display: cs.display,
1343
- flexDirection: cs.flexDirection,
1344
- gap: cs.gap,
1345
- alignItems: cs.alignItems,
1346
- justifyContent: cs.justifyContent,
1347
- backgroundColor: cs.backgroundColor,
1348
- padding: cs.padding,
1349
- },
1350
- linkCount: links.length,
1351
- linkStyles: {
1352
- color: firstLinkCs.color,
1353
- fontSize: firstLinkCs.fontSize,
1354
- fontWeight: firstLinkCs.fontWeight,
1355
- textDecoration: firstLinkCs.textDecoration,
1356
- },
1357
- });
1358
- }
1359
- // --- Logos & Icons ---
1360
- const logos = [];
1361
- const icons = [];
1362
- // SVG logos (in header/nav or with logo-related attributes)
1363
- for (const svg of document.querySelectorAll('svg')) {
1364
- const parent = svg.closest('header, nav, [role="banner"]');
1365
- const classes = (svg.getAttribute('class') || '') + ' ' + (svg.parentElement?.getAttribute('class') || '');
1366
- const ariaLabel = svg.getAttribute('aria-label') || '';
1367
- const isLogo = parent !== null || /logo|brand|mark/i.test(classes) || /logo|brand/i.test(ariaLabel);
1368
- if (isLogo) {
1369
- const content = svg.outerHTML.slice(0, 4096);
1370
- logos.push({
1371
- type: 'svg',
1372
- width: svg.width?.baseVal?.value || undefined,
1373
- height: svg.height?.baseVal?.value || undefined,
1374
- svgContent: content,
1375
- });
1376
- }
1377
- else if (svg.outerHTML.length < 2048) {
1378
- // Collect as icon (small SVGs)
1379
- icons.push({
1380
- selector: svg.tagName + (svg.getAttribute('class') ? '.' + svg.getAttribute('class').split(/\s+/)[0] : ''),
1381
- viewBox: svg.getAttribute('viewBox') || undefined,
1382
- width: svg.width?.baseVal?.value || undefined,
1383
- height: svg.height?.baseVal?.value || undefined,
1384
- content: svg.outerHTML,
1385
- });
1386
- }
1387
- }
1388
- // IMG logos
1389
- for (const img of document.querySelectorAll('img')) {
1390
- const parent = img.closest('header, nav, [role="banner"]');
1391
- const src = img.src || '';
1392
- const alt = img.alt || '';
1393
- const classes = img.className || '';
1394
- if (parent !== null || /logo|brand|mark/i.test(src) || /logo|brand/i.test(alt) || /logo|brand/i.test(classes)) {
1395
- logos.push({
1396
- type: 'img',
1397
- src: img.src,
1398
- alt: img.alt || undefined,
1399
- width: img.naturalWidth || undefined,
1400
- height: img.naturalHeight || undefined,
1401
- });
1402
- }
1403
- }
1404
- // Limit icons to top 50 unique by viewBox
1405
- const seenViewBoxes = new Set();
1406
- const dedupedIcons = icons.filter(icon => {
1407
- const key = icon.viewBox || icon.content.slice(0, 100);
1408
- if (seenViewBoxes.has(key))
1409
- return false;
1410
- seenViewBoxes.add(key);
1411
- return true;
1412
- }).slice(0, 50);
1413
- // --- Brand Meta ---
1414
- const getMeta = (sel) => document.querySelector(sel)?.getAttribute('content') || undefined;
1415
- const getLink = (sel) => {
1416
- const el = document.querySelector(sel);
1417
- return el ? el.href || el.getAttribute('href') || undefined : undefined;
1418
- };
1419
- const brand = {
1420
- title: getMeta('meta[property="og:title"]') || document.title || undefined,
1421
- description: getMeta('meta[property="og:description"]') || getMeta('meta[name="description"]'),
1422
- themeColor: getMeta('meta[name="theme-color"]'),
1423
- favicon: getLink('link[rel="icon"]') || getLink('link[rel="shortcut icon"]'),
1424
- ogImage: getMeta('meta[property="og:image"]'),
1425
- appleTouchIcon: getLink('link[rel="apple-touch-icon"]'),
1426
- manifestUrl: getLink('link[rel="manifest"]'),
1427
- };
1428
- // --- All Image URLs ---
1429
- const imageUrls = new Set();
1430
- for (const img of document.querySelectorAll('img')) {
1431
- const src = img.src;
1432
- if (src && !src.startsWith('data:') && !src.startsWith('blob:'))
1433
- imageUrls.add(src);
1434
- const srcset = img.srcset;
1435
- if (srcset) {
1436
- for (const part of srcset.split(',')) {
1437
- const url = part.trim().split(/\s+/)[0];
1438
- if (url && !url.startsWith('data:') && !url.startsWith('blob:'))
1439
- imageUrls.add(url);
1440
- }
1441
- }
1442
- }
1443
- // --- Video URLs ---
1444
- const videoUrls = new Set();
1445
- for (const video of document.querySelectorAll('video')) {
1446
- const src = video.src;
1447
- if (src && !src.startsWith('data:') && !src.startsWith('blob:'))
1448
- videoUrls.add(src);
1449
- const poster = video.poster;
1450
- if (poster && !poster.startsWith('data:') && !poster.startsWith('blob:'))
1451
- imageUrls.add(poster);
1452
- for (const source of video.querySelectorAll('source')) {
1453
- const ssrc = source.src;
1454
- if (ssrc && !ssrc.startsWith('data:') && !ssrc.startsWith('blob:'))
1455
- videoUrls.add(ssrc);
1456
- }
1457
- }
1458
- // Orphan <source> elements (audio, picture)
1459
- for (const source of document.querySelectorAll('source[src]')) {
1460
- const ssrc = source.src;
1461
- if (ssrc && /\.(mp4|webm|mov|ogg|avi|m3u8)/i.test(ssrc))
1462
- videoUrls.add(ssrc);
1463
- }
1464
- // <picture> <source> with srcset
1465
- for (const source of document.querySelectorAll('picture source[srcset]')) {
1466
- const srcset = source.srcset;
1467
- if (srcset) {
1468
- for (const part of srcset.split(',')) {
1469
- const url = part.trim().split(/\s+/)[0];
1470
- if (url && !url.startsWith('data:') && !url.startsWith('blob:'))
1471
- imageUrls.add(url);
1472
- }
1473
- }
1474
- }
1475
- // --- Lazy-load data attributes ---
1476
- for (const el of document.querySelectorAll('[data-src], [data-bg], [data-background-image], [data-poster]')) {
1477
- for (const attr of ['data-src', 'data-bg', 'data-background-image', 'data-poster']) {
1478
- const val = el.getAttribute(attr);
1479
- if (!val || val.startsWith('data:') || val.startsWith('blob:'))
1480
- continue;
1481
- try {
1482
- const resolved = new URL(val, document.location.href).href;
1483
- if (/\.(mp4|webm|mov|ogg|avi|m3u8)/i.test(val))
1484
- videoUrls.add(resolved);
1485
- else
1486
- imageUrls.add(resolved);
1487
- }
1488
- catch { /* bad URL */ }
1489
- }
1490
- }
1491
- // --- Link Assets (favicons, icons, manifests) ---
1492
- const linkAssetUrls = [];
1493
- for (const link of document.querySelectorAll('link[href]')) {
1494
- const href = link.href;
1495
- const rel = link.rel || '';
1496
- if (href && !href.startsWith('data:') && !href.startsWith('blob:') &&
1497
- /^(icon|shortcut icon|apple-touch-icon|manifest|preload)$/i.test(rel)) {
1498
- linkAssetUrls.push({ url: href, rel });
1499
- }
1500
- }
1501
- // --- Inline script library detection ---
1502
- const detectedLibraries = [];
1503
- const seenLibs = new Set();
1504
- for (const script of document.querySelectorAll('script')) {
1505
- const src = script.src || '';
1506
- const text = script.textContent || '';
1507
- const content = src + ' ' + text.slice(0, 10_000);
1508
- const checks = [
1509
- [/gsap|ScrollTrigger|ScrollSmoother/i, 'gsap'],
1510
- [/new\s+p5\b|p5\.setup|p5\.draw|p5\.js/i, 'p5.js'],
1511
- [/THREE\.|new\s+THREE\.|from\s+['"]three/i, 'three.js'],
1512
- [/lottie|bodymovin/i, 'lottie'],
1513
- [/anime\s*\(|animejs/i, 'anime.js'],
1514
- [/framer-motion|motion\.div/i, 'framer-motion'],
1515
- [/webflow/i, 'webflow'],
1516
- [/swiper/i, 'swiper'],
1517
- [/locomotive.?scroll/i, 'locomotive-scroll'],
1518
- [/rive\.?app|@rive-app/i, 'rive'],
1519
- [/spline/i, 'spline'],
1520
- ];
1521
- for (const [regex, name] of checks) {
1522
- if (!seenLibs.has(name) && regex.test(content)) {
1523
- seenLibs.add(name);
1524
- detectedLibraries.push(name);
1525
- }
1526
- }
1527
- }
1528
- // --- Assemble result ---
1529
- return {
1530
- customProperties,
1531
- colors: Array.from(colorMap.entries())
1532
- .map(([value, data]) => ({ value, count: data.count, properties: Array.from(data.properties) }))
1533
- .sort((a, b) => b.count - a.count),
1534
- typography: {
1535
- families: Array.from(fontFamilyMap.entries()).map(([family, data]) => ({
1536
- family,
1537
- weights: Array.from(data.weights).sort((a, b) => a - b),
1538
- headingUse: data.headingUse,
1539
- monoUse: data.monoUse,
1540
- })),
1541
- scale: Array.from(typeScaleMap.values()).sort((a, b) => b.count - a.count),
1542
- },
1543
- brand,
1544
- logos,
1545
- icons: dedupedIcons,
1546
- spacing: Array.from(spacingMap.entries())
1547
- .map(([value, data]) => ({ value, count: data.count, properties: Array.from(data.properties) }))
1548
- .sort((a, b) => b.count - a.count),
1549
- borders: Array.from(borderRadiusMap.entries())
1550
- .map(([borderRadius, count]) => ({ borderRadius, count }))
1551
- .sort((a, b) => b.count - a.count),
1552
- shadows: Array.from(shadowMap.entries())
1553
- .map(([value, count]) => ({ value, count }))
1554
- .sort((a, b) => b.count - a.count),
1555
- components: {
1556
- buttons: buttonStyles.sort((a, b) => b.count - a.count),
1557
- inputs: inputStyles.sort((a, b) => b.count - a.count),
1558
- cards: cardStyles.sort((a, b) => b.count - a.count),
1559
- nav: navPatterns,
1560
- },
1561
- animations: Array.from(animationMap.values()).sort((a, b) => b.count - a.count),
1562
- fontFiles,
1563
- imageUrls: Array.from(imageUrls),
1564
- backgroundImageUrls: Array.from(backgroundImageUrls),
1565
- videoUrls: Array.from(videoUrls),
1566
- externalStylesheetUrls,
1567
- linkAssetUrls,
1568
- detectedLibraries,
1569
- };
1570
- }
1571
- // ---------------------------------------------------------------------------
1572
- // Color clustering — assign semantic roles to extracted colors
1573
- // ---------------------------------------------------------------------------
1574
- function rgbaToHsl(rgba) {
1575
- const match = rgba.match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/);
1576
- if (!match)
1577
- return null;
1578
- const r = parseFloat(match[1]) / 255;
1579
- const g = parseFloat(match[2]) / 255;
1580
- const b = parseFloat(match[3]) / 255;
1581
- const a = match[4] !== undefined ? parseFloat(match[4]) : 1;
1582
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
1583
- const l = (max + min) / 2;
1584
- if (max === min)
1585
- return { h: 0, s: 0, l, a };
1586
- const d = max - min;
1587
- const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1588
- let h = 0;
1589
- if (max === r)
1590
- h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
1591
- else if (max === g)
1592
- h = ((b - r) / d + 2) / 6;
1593
- else
1594
- h = ((r - g) / d + 4) / 6;
1595
- return { h: h * 360, s: s * 100, l: l * 100, a };
1596
- }
1597
- function rgbaToHex(rgba) {
1598
- const match = rgba.match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/);
1599
- if (!match)
1600
- return rgba;
1601
- const r = Math.round(parseFloat(match[1]));
1602
- const g = Math.round(parseFloat(match[2]));
1603
- const b = Math.round(parseFloat(match[3]));
1604
- const a = match[4] !== undefined ? parseFloat(match[4]) : 1;
1605
- const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
1606
- if (a < 1)
1607
- return `${hex}${Math.round(a * 255).toString(16).padStart(2, '0')}`;
1608
- return hex;
1609
- }
1610
- function clusterColors(colors) {
1611
- const result = [];
1612
- let primaryHue = -1;
1613
- let secondaryHue = -1;
1614
- for (const c of colors) {
1615
- const hsl = rgbaToHsl(c.value);
1616
- const hex = rgbaToHex(c.value);
1617
- let cluster;
1618
- if (!hsl || hsl.s < 10) {
1619
- // Low saturation = neutral (grays, black, white)
1620
- if (hsl && hsl.l > 90)
1621
- cluster = 'background';
1622
- else
1623
- cluster = 'neutral';
1624
- }
1625
- else if (c.properties.includes('borderColor') && !c.properties.includes('color') && !c.properties.includes('backgroundColor')) {
1626
- cluster = 'border';
1627
- }
1628
- else if (primaryHue < 0) {
1629
- primaryHue = hsl.h;
1630
- cluster = 'primary';
1631
- }
1632
- else if (Math.abs(hsl.h - primaryHue) < 30 || Math.abs(hsl.h - primaryHue) > 330) {
1633
- cluster = 'primary';
1634
- }
1635
- else if (secondaryHue < 0) {
1636
- secondaryHue = hsl.h;
1637
- cluster = 'secondary';
1638
- }
1639
- else if (Math.abs(hsl.h - secondaryHue) < 30 || Math.abs(hsl.h - secondaryHue) > 330) {
1640
- cluster = 'secondary';
1641
- }
1642
- else {
1643
- cluster = 'accent';
1644
- }
1645
- result.push({ value: c.value, hex, count: c.count, properties: c.properties, cluster });
1646
- }
1647
- return result;
1648
- }
1649
- // ---------------------------------------------------------------------------
1650
- // Grid base unit detection
1651
- // ---------------------------------------------------------------------------
1652
- function detectGridUnit(spacingValues) {
1653
- // Build frequency-weighted pairs
1654
- const pairs = spacingValues
1655
- .map(s => ({ px: parseFloat(s.value), count: s.count }))
1656
- .filter(p => p.px > 0 && p.px <= 200 && Number.isInteger(p.px));
1657
- if (pairs.length < 3)
1658
- return undefined;
1659
- // Score common grid units by how much of the total usage they explain
1660
- const CANDIDATES = [4, 5, 6, 8, 10];
1661
- let bestUnit = 0;
1662
- let bestScore = 0;
1663
- for (const unit of CANDIDATES) {
1664
- let aligned = 0;
1665
- let total = 0;
1666
- for (const p of pairs) {
1667
- total += p.count;
1668
- if (p.px % unit === 0)
1669
- aligned += p.count;
1670
- }
1671
- const score = aligned / total;
1672
- if (score > bestScore) {
1673
- bestScore = score;
1674
- bestUnit = unit;
1675
- }
1676
- }
1677
- // Require at least 60% of usage aligns with the grid
1678
- return bestScore >= 0.6 ? bestUnit : undefined;
1679
- }
1680
- // ---------------------------------------------------------------------------
1681
- // Token extraction orchestrator
1682
- // ---------------------------------------------------------------------------
1683
- async function dismissCookieBanners(page) {
1684
- for (const sel of ['button:has-text("Accept all")', 'button:has-text("Accept")', 'button:has-text("Reject all")', 'button:has-text("Got it")', 'button:has-text("Close")']) {
1685
- const btn = page.locator(sel).first();
1686
- if (await btn.isVisible({ timeout: 500 }).catch(() => false)) {
1687
- await btn.click({ timeout: 2000 }).catch(() => null);
1688
- await page.waitForTimeout(500);
1689
- break;
1690
- }
1691
- }
1692
- }
1693
- /**
1694
- * Extract design tokens from a URL at multiple viewports.
1695
- * Pure DOM extraction — no LLM calls.
1696
- *
1697
- * ```typescript
1698
- * import { extractDesignTokens } from '@tangle-network/browser-agent-driver'
1699
- *
1700
- * const { tokens } = await extractDesignTokens({ url: 'https://stripe.com' })
1701
- * console.log(tokens.colors.filter(c => c.cluster === 'primary'))
1702
- * console.log(tokens.typography.families)
1703
- * console.log(tokens.brand)
1704
- * ```
1705
- */
1706
- export async function extractDesignTokens(opts) {
1707
- const viewports = opts.viewports ?? [...VIEWPORTS];
1708
- const outputDir = opts.outputDir ?? `./audit-results/${new URL(opts.url).hostname}-tokens-${Date.now()}`;
1709
- fs.mkdirSync(outputDir, { recursive: true });
1710
- const screenshotDir = path.join(outputDir, 'screenshots');
1711
- fs.mkdirSync(screenshotDir, { recursive: true });
1712
- const browser = await chromium.launch({ headless: opts.headless ?? true });
1713
- const allCustomProps = {};
1714
- const allColors = new Map();
1715
- const allFamilies = new Map();
1716
- const allScaleEntries = [];
1717
- let brand = {};
1718
- const allLogos = [];
1719
- const allIcons = [];
1720
- const allFontFiles = new Map();
1721
- const allImageUrls = new Set();
1722
- const allBackgroundImageUrls = new Set();
1723
- const allVideoUrls = new Set();
1724
- const allExternalStylesheetUrls = new Set();
1725
- const allLinkAssetUrls = new Map(); // url -> rel
1726
- const allDetectedLibraries = new Set();
1727
- const responsiveTokens = {};
1728
- const screenshotPaths = {};
1729
- for (const vp of viewports) {
1730
- const context = await browser.newContext({ viewport: { width: vp.width, height: vp.height } });
1731
- const page = await context.newPage();
1732
- try {
1733
- await page.goto(opts.url, { waitUntil: 'networkidle', timeout: 20_000 }).catch(() => page.goto(opts.url, { waitUntil: 'domcontentloaded', timeout: 15_000 }));
1734
- await page.waitForTimeout(2000);
1735
- await dismissCookieBanners(page);
1736
- await page.waitForTimeout(500);
1737
- const screenshotPath = path.join(screenshotDir, `${vp.name}.png`);
1738
- await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => page.screenshot({ path: screenshotPath, fullPage: false }));
1739
- screenshotPaths[vp.name] = screenshotPath;
1740
- const raw = await page.evaluate(extractTokensFromDOM);
1741
- Object.assign(allCustomProps, raw.customProperties);
1742
- for (const c of raw.colors) {
1743
- const existing = allColors.get(c.value);
1744
- if (existing) {
1745
- existing.count += c.count;
1746
- for (const p of c.properties)
1747
- existing.properties.add(p);
1748
- }
1749
- else {
1750
- allColors.set(c.value, { count: c.count, properties: new Set(c.properties) });
1751
- }
1752
- }
1753
- for (const f of raw.typography.families) {
1754
- const existing = allFamilies.get(f.family);
1755
- if (existing) {
1756
- for (const w of f.weights)
1757
- existing.weights.add(w);
1758
- if (f.headingUse)
1759
- existing.headingUse = true;
1760
- if (f.monoUse)
1761
- existing.monoUse = true;
1762
- }
1763
- else {
1764
- allFamilies.set(f.family, { weights: new Set(f.weights), headingUse: f.headingUse, monoUse: f.monoUse });
1765
- }
1766
- }
1767
- allScaleEntries.push(...raw.typography.scale);
1768
- if (vp.name === 'desktop' || !brand.title) {
1769
- brand = raw.brand;
1770
- }
1771
- for (const logo of raw.logos) {
1772
- if (!allLogos.some(l => l.src === logo.src && l.svgContent === logo.svgContent)) {
1773
- allLogos.push(logo);
1774
- }
1775
- }
1776
- for (const icon of raw.icons) {
1777
- if (!allIcons.some(i => i.content === icon.content)) {
1778
- allIcons.push(icon);
1779
- }
1780
- }
1781
- for (const ff of raw.fontFiles) {
1782
- if (!allFontFiles.has(ff.src)) {
1783
- allFontFiles.set(ff.src, ff);
1784
- }
1785
- }
1786
- for (const u of raw.imageUrls)
1787
- allImageUrls.add(u);
1788
- for (const u of raw.backgroundImageUrls)
1789
- allBackgroundImageUrls.add(u);
1790
- for (const u of raw.videoUrls)
1791
- allVideoUrls.add(u);
1792
- for (const u of raw.externalStylesheetUrls)
1793
- allExternalStylesheetUrls.add(u);
1794
- for (const la of raw.linkAssetUrls)
1795
- allLinkAssetUrls.set(la.url, la.rel);
1796
- for (const lib of raw.detectedLibraries)
1797
- allDetectedLibraries.add(lib);
1798
- responsiveTokens[vp.name] = {
1799
- width: vp.width,
1800
- height: vp.height,
1801
- spacing: raw.spacing,
1802
- gridBaseUnit: detectGridUnit(raw.spacing),
1803
- borders: raw.borders,
1804
- shadows: raw.shadows,
1805
- components: raw.components,
1806
- animations: raw.animations,
1807
- screenshotPath,
1808
- };
1809
- opts.onProgress?.(vp.name, vp.width, vp.height, {
1810
- colors: raw.colors.length,
1811
- fonts: raw.typography.families.length,
1812
- buttons: raw.components.buttons.length,
1813
- inputs: raw.components.inputs.length,
1814
- cards: raw.components.cards.length,
1815
- });
1816
- }
1817
- finally {
1818
- await context.close();
1819
- }
1820
- }
1821
- await browser.close();
1822
- // Deduplicate type scale
1823
- const scaleMap = new Map();
1824
- for (const entry of allScaleEntries) {
1825
- const key = `${entry.fontSize}|${entry.fontWeight}|${entry.lineHeight}|${entry.fontFamily}`;
1826
- const existing = scaleMap.get(key);
1827
- if (existing) {
1828
- existing.count += entry.count;
1829
- }
1830
- else {
1831
- const isHeading = /^h[1-6]$/.test(entry.tag);
1832
- const fontSize = parseFloat(entry.fontSize);
1833
- let usage = 'body';
1834
- if (isHeading)
1835
- usage = 'heading';
1836
- else if (fontSize <= 12)
1837
- usage = 'caption';
1838
- else if (fontSize <= 14 && entry.fontWeight >= '500')
1839
- usage = 'label';
1840
- scaleMap.set(key, { fontSize: entry.fontSize, fontWeight: entry.fontWeight, lineHeight: entry.lineHeight, letterSpacing: entry.letterSpacing, fontFamily: entry.fontFamily, usage, tag: entry.tag, count: entry.count });
1841
- }
1842
- }
1843
- const mergedColors = Array.from(allColors.entries())
1844
- .map(([value, data]) => ({ value, count: data.count, properties: Array.from(data.properties) }))
1845
- .sort((a, b) => b.count - a.count);
1846
- const clusteredColors = clusterColors(mergedColors);
1847
- const families = Array.from(allFamilies.entries()).map(([family, data]) => ({
1848
- family,
1849
- weights: Array.from(data.weights).sort((a, b) => a - b),
1850
- classification: data.monoUse ? 'mono' :
1851
- (data.headingUse && allFamilies.size > 1) ? 'heading' :
1852
- data.headingUse ? 'display' : 'body',
1853
- }));
1854
- // Download font files
1855
- const downloadedFontFiles = [];
1856
- if (allFontFiles.size > 0) {
1857
- const fontDir = path.join(outputDir, 'fonts');
1858
- fs.mkdirSync(fontDir, { recursive: true });
1859
- for (const ff of allFontFiles.values()) {
1860
- try {
1861
- const res = await fetch(ff.src);
1862
- if (!res.ok)
1863
- continue;
1864
- const buffer = Buffer.from(await res.arrayBuffer());
1865
- const urlPath = decodeURIComponent(new URL(ff.src).pathname);
1866
- const filename = path.basename(urlPath) || `${ff.family.replace(/\s+/g, '-')}-${ff.weight}-${ff.style}.${ff.format === 'unknown' ? 'bin' : ff.format}`;
1867
- const localPath = path.join(fontDir, filename);
1868
- fs.writeFileSync(localPath, buffer);
1869
- downloadedFontFiles.push({ ...ff, localPath });
1870
- }
1871
- catch {
1872
- downloadedFontFiles.push({ ...ff });
1873
- }
1874
- }
1875
- }
1876
- // Download images
1877
- const downloadedImages = [];
1878
- const allAssetUrls = new Map();
1879
- for (const u of allImageUrls)
1880
- allAssetUrls.set(u, 'img');
1881
- for (const u of allBackgroundImageUrls)
1882
- allAssetUrls.set(u, 'background');
1883
- // Brand assets
1884
- if (brand.favicon)
1885
- allAssetUrls.set(brand.favicon, 'favicon');
1886
- if (brand.ogImage)
1887
- allAssetUrls.set(brand.ogImage, 'og-image');
1888
- if (brand.appleTouchIcon)
1889
- allAssetUrls.set(brand.appleTouchIcon, 'icon');
1890
- for (const [url, rel] of allLinkAssetUrls) {
1891
- if (/icon/i.test(rel))
1892
- allAssetUrls.set(url, 'favicon');
1893
- }
1894
- if (allAssetUrls.size > 0) {
1895
- const imageDir = path.join(outputDir, 'images');
1896
- fs.mkdirSync(imageDir, { recursive: true });
1897
- for (const [url, type] of allAssetUrls) {
1898
- try {
1899
- const res = await fetch(url);
1900
- if (!res.ok) {
1901
- downloadedImages.push({ url, type });
1902
- continue;
1903
- }
1904
- const buffer = Buffer.from(await res.arrayBuffer());
1905
- const urlPath = decodeURIComponent(new URL(url).pathname);
1906
- const filename = path.basename(urlPath) || `asset-${downloadedImages.length}.bin`;
1907
- const localPath = path.join(imageDir, filename);
1908
- fs.writeFileSync(localPath, buffer);
1909
- downloadedImages.push({
1910
- url, type, localPath,
1911
- mimeType: res.headers.get('content-type') || undefined,
1912
- sizeBytes: buffer.length,
1913
- });
1914
- }
1915
- catch {
1916
- downloadedImages.push({ url, type });
1917
- }
1918
- }
1919
- }
1920
- // Download external stylesheets
1921
- const downloadedStylesheets = [];
1922
- if (allExternalStylesheetUrls.size > 0) {
1923
- const cssDir = path.join(outputDir, 'stylesheets');
1924
- fs.mkdirSync(cssDir, { recursive: true });
1925
- for (const url of allExternalStylesheetUrls) {
1926
- try {
1927
- const res = await fetch(url);
1928
- if (!res.ok) {
1929
- downloadedStylesheets.push({ url });
1930
- continue;
1931
- }
1932
- const text = await res.text();
1933
- const urlPath = decodeURIComponent(new URL(url).pathname);
1934
- const filename = path.basename(urlPath) || `style-${downloadedStylesheets.length}.css`;
1935
- const localPath = path.join(cssDir, filename);
1936
- fs.writeFileSync(localPath, text);
1937
- downloadedStylesheets.push({ url, localPath });
1938
- }
1939
- catch {
1940
- downloadedStylesheets.push({ url });
1941
- }
1942
- }
1943
- }
1944
- // Download videos
1945
- const downloadedVideos = [];
1946
- if (allVideoUrls.size > 0) {
1947
- const videoDir = path.join(outputDir, 'videos');
1948
- fs.mkdirSync(videoDir, { recursive: true });
1949
- for (const url of allVideoUrls) {
1950
- try {
1951
- const res = await fetch(url);
1952
- if (!res.ok) {
1953
- downloadedVideos.push({ url, type: 'video' });
1954
- continue;
1955
- }
1956
- const buffer = Buffer.from(await res.arrayBuffer());
1957
- const urlPath = decodeURIComponent(new URL(url).pathname);
1958
- const filename = path.basename(urlPath) || `video-${downloadedVideos.length}.mp4`;
1959
- const localPath = path.join(videoDir, filename);
1960
- fs.writeFileSync(localPath, buffer);
1961
- downloadedVideos.push({
1962
- url, type: 'video', localPath,
1963
- mimeType: res.headers.get('content-type') || undefined,
1964
- sizeBytes: buffer.length,
1965
- });
1966
- }
1967
- catch {
1968
- downloadedVideos.push({ url, type: 'video' });
1969
- }
1970
- }
1971
- }
1972
- const tokens = {
1973
- url: opts.url,
1974
- extractedAt: new Date().toISOString(),
1975
- viewportsAudited: viewports.map(v => v.name),
1976
- customProperties: allCustomProps,
1977
- colors: clusteredColors,
1978
- typography: { families, scale: Array.from(scaleMap.values()).sort((a, b) => b.count - a.count) },
1979
- brand,
1980
- logos: allLogos,
1981
- icons: allIcons.slice(0, 50),
1982
- fontFiles: downloadedFontFiles,
1983
- images: downloadedImages,
1984
- videos: downloadedVideos,
1985
- stylesheets: downloadedStylesheets,
1986
- responsive: responsiveTokens,
1987
- detectedLibraries: Array.from(allDetectedLibraries),
1988
- };
1989
- const tokenPath = path.join(outputDir, 'tokens.json');
1990
- fs.writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
1991
- return { tokens, outputDir, screenshotPaths };
1992
- }
1993
- // ---------------------------------------------------------------------------
1994
- // Zip packaging
1995
- // ---------------------------------------------------------------------------
1996
- async function createZipBundle(outputDir, tokens) {
1997
- const { execSync } = await import('node:child_process');
1998
- const zipPath = `${outputDir}.zip`;
1999
- // Use system zip (available on macOS/Linux)
2000
- execSync(`cd "${path.dirname(outputDir)}" && zip -r "${path.basename(zipPath)}" "${path.basename(outputDir)}"`, { stdio: 'pipe' });
2001
- return zipPath;
2002
- }
2003
494
  // ---------------------------------------------------------------------------
2004
495
  // CLI wrapper for token extraction
2005
496
  // ---------------------------------------------------------------------------