@ztothez/design-engineering 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +448 -0
  3. package/ROADMAP.md +127 -0
  4. package/SKILL.md +698 -0
  5. package/V2-ROADMAP.md +431 -0
  6. package/V3-ROADMAP.md +605 -0
  7. package/V4-ROADMAP.md +283 -0
  8. package/clean-room-ux-design-spec.md +1431 -0
  9. package/dist/cli/aggregate-gates.d.ts +2 -0
  10. package/dist/cli/aggregate-gates.js +86 -0
  11. package/dist/cli/aggregate-gates.js.map +1 -0
  12. package/dist/cli/assess-maintainer-review.d.ts +2 -0
  13. package/dist/cli/assess-maintainer-review.js +72 -0
  14. package/dist/cli/assess-maintainer-review.js.map +1 -0
  15. package/dist/cli/audit.d.ts +2 -0
  16. package/dist/cli/audit.js +71 -0
  17. package/dist/cli/audit.js.map +1 -0
  18. package/dist/cli/compile-comparison-review.d.ts +2 -0
  19. package/dist/cli/compile-comparison-review.js +154 -0
  20. package/dist/cli/compile-comparison-review.js.map +1 -0
  21. package/dist/cli/compile-plan.d.ts +2 -0
  22. package/dist/cli/compile-plan.js +66 -0
  23. package/dist/cli/compile-plan.js.map +1 -0
  24. package/dist/cli/evaluate-corpus.d.ts +2 -0
  25. package/dist/cli/evaluate-corpus.js +65 -0
  26. package/dist/cli/evaluate-corpus.js.map +1 -0
  27. package/dist/cli/generate-react.d.ts +2 -0
  28. package/dist/cli/generate-react.js +83 -0
  29. package/dist/cli/generate-react.js.map +1 -0
  30. package/dist/cli/index.d.ts +2 -0
  31. package/dist/cli/index.js +58 -0
  32. package/dist/cli/index.js.map +1 -0
  33. package/dist/cli/portfolio.d.ts +2 -0
  34. package/dist/cli/portfolio.js +316 -0
  35. package/dist/cli/portfolio.js.map +1 -0
  36. package/dist/cli/quality-gate.d.ts +2 -0
  37. package/dist/cli/quality-gate.js +131 -0
  38. package/dist/cli/quality-gate.js.map +1 -0
  39. package/dist/cli/repair-react.d.ts +2 -0
  40. package/dist/cli/repair-react.js +163 -0
  41. package/dist/cli/repair-react.js.map +1 -0
  42. package/dist/cli/review-heuristics.d.ts +2 -0
  43. package/dist/cli/review-heuristics.js +68 -0
  44. package/dist/cli/review-heuristics.js.map +1 -0
  45. package/dist/cli/validate-brief.d.ts +2 -0
  46. package/dist/cli/validate-brief.js +58 -0
  47. package/dist/cli/validate-brief.js.map +1 -0
  48. package/dist/cli/validate-comparison.d.ts +2 -0
  49. package/dist/cli/validate-comparison.js +68 -0
  50. package/dist/cli/validate-comparison.js.map +1 -0
  51. package/dist/cli/validate-contract.d.ts +2 -0
  52. package/dist/cli/validate-contract.js +54 -0
  53. package/dist/cli/validate-contract.js.map +1 -0
  54. package/dist/cli/validate-design.d.ts +2 -0
  55. package/dist/cli/validate-design.js +53 -0
  56. package/dist/cli/validate-design.js.map +1 -0
  57. package/dist/cli/validate-information.d.ts +2 -0
  58. package/dist/cli/validate-information.js +53 -0
  59. package/dist/cli/validate-information.js.map +1 -0
  60. package/dist/cli/validate-trust.d.ts +2 -0
  61. package/dist/cli/validate-trust.js +53 -0
  62. package/dist/cli/validate-trust.js.map +1 -0
  63. package/dist/cli/verify-ui.d.ts +2 -0
  64. package/dist/cli/verify-ui.js +181 -0
  65. package/dist/cli/verify-ui.js.map +1 -0
  66. package/dist/src/acceptance/evaluator.d.ts +13 -0
  67. package/dist/src/acceptance/evaluator.js +201 -0
  68. package/dist/src/acceptance/evaluator.js.map +1 -0
  69. package/dist/src/acceptance/loader.d.ts +2 -0
  70. package/dist/src/acceptance/loader.js +18 -0
  71. package/dist/src/acceptance/loader.js.map +1 -0
  72. package/dist/src/acceptance/report.d.ts +2 -0
  73. package/dist/src/acceptance/report.js +18 -0
  74. package/dist/src/acceptance/report.js.map +1 -0
  75. package/dist/src/acceptance/schema.d.ts +64 -0
  76. package/dist/src/acceptance/schema.js +57 -0
  77. package/dist/src/acceptance/schema.js.map +1 -0
  78. package/dist/src/acceptance/types.d.ts +47 -0
  79. package/dist/src/acceptance/types.js +2 -0
  80. package/dist/src/acceptance/types.js.map +1 -0
  81. package/dist/src/aggregate/report.d.ts +2 -0
  82. package/dist/src/aggregate/report.js +33 -0
  83. package/dist/src/aggregate/report.js.map +1 -0
  84. package/dist/src/aggregate/runner.d.ts +2 -0
  85. package/dist/src/aggregate/runner.js +165 -0
  86. package/dist/src/aggregate/runner.js.map +1 -0
  87. package/dist/src/aggregate/schema.d.ts +182 -0
  88. package/dist/src/aggregate/schema.js +54 -0
  89. package/dist/src/aggregate/schema.js.map +1 -0
  90. package/dist/src/aggregate/types.d.ts +57 -0
  91. package/dist/src/aggregate/types.js +2 -0
  92. package/dist/src/aggregate/types.js.map +1 -0
  93. package/dist/src/audit/policy.d.ts +3 -0
  94. package/dist/src/audit/policy.js +45 -0
  95. package/dist/src/audit/policy.js.map +1 -0
  96. package/dist/src/audit/report.d.ts +2 -0
  97. package/dist/src/audit/report.js +32 -0
  98. package/dist/src/audit/report.js.map +1 -0
  99. package/dist/src/audit/rules/accessibility-name.d.ts +2 -0
  100. package/dist/src/audit/rules/accessibility-name.js +64 -0
  101. package/dist/src/audit/rules/accessibility-name.js.map +1 -0
  102. package/dist/src/audit/rules/component-size.d.ts +2 -0
  103. package/dist/src/audit/rules/component-size.js +27 -0
  104. package/dist/src/audit/rules/component-size.js.map +1 -0
  105. package/dist/src/audit/rules/credential-placeholder.d.ts +2 -0
  106. package/dist/src/audit/rules/credential-placeholder.js +83 -0
  107. package/dist/src/audit/rules/credential-placeholder.js.map +1 -0
  108. package/dist/src/audit/rules/helpers.d.ts +16 -0
  109. package/dist/src/audit/rules/helpers.js +64 -0
  110. package/dist/src/audit/rules/helpers.js.map +1 -0
  111. package/dist/src/audit/rules/index.d.ts +2 -0
  112. package/dist/src/audit/rules/index.js +23 -0
  113. package/dist/src/audit/rules/index.js.map +1 -0
  114. package/dist/src/audit/rules/interactive-integrity.d.ts +2 -0
  115. package/dist/src/audit/rules/interactive-integrity.js +111 -0
  116. package/dist/src/audit/rules/interactive-integrity.js.map +1 -0
  117. package/dist/src/audit/rules/mixed-responsibilities.d.ts +2 -0
  118. package/dist/src/audit/rules/mixed-responsibilities.js +39 -0
  119. package/dist/src/audit/rules/mixed-responsibilities.js.map +1 -0
  120. package/dist/src/audit/rules/mock-production-path.d.ts +2 -0
  121. package/dist/src/audit/rules/mock-production-path.js +39 -0
  122. package/dist/src/audit/rules/mock-production-path.js.map +1 -0
  123. package/dist/src/audit/rules/network-state.d.ts +2 -0
  124. package/dist/src/audit/rules/network-state.js +39 -0
  125. package/dist/src/audit/rules/network-state.js.map +1 -0
  126. package/dist/src/audit/rules/operational-claim.d.ts +2 -0
  127. package/dist/src/audit/rules/operational-claim.js +94 -0
  128. package/dist/src/audit/rules/operational-claim.js.map +1 -0
  129. package/dist/src/audit/rules/placeholder-interaction.d.ts +2 -0
  130. package/dist/src/audit/rules/placeholder-interaction.js +46 -0
  131. package/dist/src/audit/rules/placeholder-interaction.js.map +1 -0
  132. package/dist/src/audit/rules/raw-design-values.d.ts +2 -0
  133. package/dist/src/audit/rules/raw-design-values.js +97 -0
  134. package/dist/src/audit/rules/raw-design-values.js.map +1 -0
  135. package/dist/src/audit/scanner.d.ts +2 -0
  136. package/dist/src/audit/scanner.js +215 -0
  137. package/dist/src/audit/scanner.js.map +1 -0
  138. package/dist/src/audit/types.d.ts +62 -0
  139. package/dist/src/audit/types.js +2 -0
  140. package/dist/src/audit/types.js.map +1 -0
  141. package/dist/src/comparison/evaluator.d.ts +2 -0
  142. package/dist/src/comparison/evaluator.js +478 -0
  143. package/dist/src/comparison/evaluator.js.map +1 -0
  144. package/dist/src/comparison/loader.d.ts +3 -0
  145. package/dist/src/comparison/loader.js +26 -0
  146. package/dist/src/comparison/loader.js.map +1 -0
  147. package/dist/src/comparison/maintainer.d.ts +47 -0
  148. package/dist/src/comparison/maintainer.js +218 -0
  149. package/dist/src/comparison/maintainer.js.map +1 -0
  150. package/dist/src/comparison/report.d.ts +2 -0
  151. package/dist/src/comparison/report.js +48 -0
  152. package/dist/src/comparison/report.js.map +1 -0
  153. package/dist/src/comparison/schema.d.ts +464 -0
  154. package/dist/src/comparison/schema.js +502 -0
  155. package/dist/src/comparison/schema.js.map +1 -0
  156. package/dist/src/contracts/archetypes.d.ts +21 -0
  157. package/dist/src/contracts/archetypes.js +102 -0
  158. package/dist/src/contracts/archetypes.js.map +1 -0
  159. package/dist/src/contracts/journeys.d.ts +7 -0
  160. package/dist/src/contracts/journeys.js +32 -0
  161. package/dist/src/contracts/journeys.js.map +1 -0
  162. package/dist/src/contracts/report.d.ts +2 -0
  163. package/dist/src/contracts/report.js +33 -0
  164. package/dist/src/contracts/report.js.map +1 -0
  165. package/dist/src/contracts/schema.d.ts +1038 -0
  166. package/dist/src/contracts/schema.js +385 -0
  167. package/dist/src/contracts/schema.js.map +1 -0
  168. package/dist/src/contracts/validator.d.ts +12 -0
  169. package/dist/src/contracts/validator.js +303 -0
  170. package/dist/src/contracts/validator.js.map +1 -0
  171. package/dist/src/corpus/evaluator.d.ts +2 -0
  172. package/dist/src/corpus/evaluator.js +203 -0
  173. package/dist/src/corpus/evaluator.js.map +1 -0
  174. package/dist/src/corpus/loader.d.ts +2 -0
  175. package/dist/src/corpus/loader.js +20 -0
  176. package/dist/src/corpus/loader.js.map +1 -0
  177. package/dist/src/corpus/report.d.ts +2 -0
  178. package/dist/src/corpus/report.js +33 -0
  179. package/dist/src/corpus/report.js.map +1 -0
  180. package/dist/src/corpus/schema.d.ts +316 -0
  181. package/dist/src/corpus/schema.js +269 -0
  182. package/dist/src/corpus/schema.js.map +1 -0
  183. package/dist/src/design-intelligence/loader.d.ts +2 -0
  184. package/dist/src/design-intelligence/loader.js +21 -0
  185. package/dist/src/design-intelligence/loader.js.map +1 -0
  186. package/dist/src/design-intelligence/report.d.ts +2 -0
  187. package/dist/src/design-intelligence/report.js +36 -0
  188. package/dist/src/design-intelligence/report.js.map +1 -0
  189. package/dist/src/design-intelligence/schema.d.ts +838 -0
  190. package/dist/src/design-intelligence/schema.js +854 -0
  191. package/dist/src/design-intelligence/schema.js.map +1 -0
  192. package/dist/src/design-intelligence/validator.d.ts +2 -0
  193. package/dist/src/design-intelligence/validator.js +1371 -0
  194. package/dist/src/design-intelligence/validator.js.map +1 -0
  195. package/dist/src/design-plan/compiler.d.ts +8 -0
  196. package/dist/src/design-plan/compiler.js +288 -0
  197. package/dist/src/design-plan/compiler.js.map +1 -0
  198. package/dist/src/design-plan/report.d.ts +2 -0
  199. package/dist/src/design-plan/report.js +28 -0
  200. package/dist/src/design-plan/report.js.map +1 -0
  201. package/dist/src/design-plan/schema.d.ts +202 -0
  202. package/dist/src/design-plan/schema.js +76 -0
  203. package/dist/src/design-plan/schema.js.map +1 -0
  204. package/dist/src/generation/loader.d.ts +2 -0
  205. package/dist/src/generation/loader.js +21 -0
  206. package/dist/src/generation/loader.js.map +1 -0
  207. package/dist/src/generation/policy.d.ts +12 -0
  208. package/dist/src/generation/policy.js +62 -0
  209. package/dist/src/generation/policy.js.map +1 -0
  210. package/dist/src/generation/react-typescript.d.ts +6 -0
  211. package/dist/src/generation/react-typescript.js +538 -0
  212. package/dist/src/generation/react-typescript.js.map +1 -0
  213. package/dist/src/generation/report.d.ts +2 -0
  214. package/dist/src/generation/report.js +21 -0
  215. package/dist/src/generation/report.js.map +1 -0
  216. package/dist/src/generation/schema.d.ts +44 -0
  217. package/dist/src/generation/schema.js +50 -0
  218. package/dist/src/generation/schema.js.map +1 -0
  219. package/dist/src/generation/templates/tokens.d.ts +1 -0
  220. package/dist/src/generation/templates/tokens.js +59 -0
  221. package/dist/src/generation/templates/tokens.js.map +1 -0
  222. package/dist/src/heuristics/evaluator.d.ts +2 -0
  223. package/dist/src/heuristics/evaluator.js +65 -0
  224. package/dist/src/heuristics/evaluator.js.map +1 -0
  225. package/dist/src/heuristics/loader.d.ts +2 -0
  226. package/dist/src/heuristics/loader.js +21 -0
  227. package/dist/src/heuristics/loader.js.map +1 -0
  228. package/dist/src/heuristics/report.d.ts +3 -0
  229. package/dist/src/heuristics/report.js +31 -0
  230. package/dist/src/heuristics/report.js.map +1 -0
  231. package/dist/src/heuristics/schema.d.ts +198 -0
  232. package/dist/src/heuristics/schema.js +197 -0
  233. package/dist/src/heuristics/schema.js.map +1 -0
  234. package/dist/src/information-design/loader.d.ts +2 -0
  235. package/dist/src/information-design/loader.js +21 -0
  236. package/dist/src/information-design/loader.js.map +1 -0
  237. package/dist/src/information-design/report.d.ts +2 -0
  238. package/dist/src/information-design/report.js +27 -0
  239. package/dist/src/information-design/report.js.map +1 -0
  240. package/dist/src/information-design/schema.d.ts +370 -0
  241. package/dist/src/information-design/schema.js +329 -0
  242. package/dist/src/information-design/schema.js.map +1 -0
  243. package/dist/src/information-design/validator.d.ts +2 -0
  244. package/dist/src/information-design/validator.js +242 -0
  245. package/dist/src/information-design/validator.js.map +1 -0
  246. package/dist/src/interface-trust/loader.d.ts +2 -0
  247. package/dist/src/interface-trust/loader.js +21 -0
  248. package/dist/src/interface-trust/loader.js.map +1 -0
  249. package/dist/src/interface-trust/report.d.ts +2 -0
  250. package/dist/src/interface-trust/report.js +26 -0
  251. package/dist/src/interface-trust/report.js.map +1 -0
  252. package/dist/src/interface-trust/schema.d.ts +299 -0
  253. package/dist/src/interface-trust/schema.js +238 -0
  254. package/dist/src/interface-trust/schema.js.map +1 -0
  255. package/dist/src/interface-trust/validator.d.ts +2 -0
  256. package/dist/src/interface-trust/validator.js +283 -0
  257. package/dist/src/interface-trust/validator.js.map +1 -0
  258. package/dist/src/portfolio/adapter-report.d.ts +3 -0
  259. package/dist/src/portfolio/adapter-report.js +38 -0
  260. package/dist/src/portfolio/adapter-report.js.map +1 -0
  261. package/dist/src/portfolio/adapters.d.ts +37 -0
  262. package/dist/src/portfolio/adapters.js +325 -0
  263. package/dist/src/portfolio/adapters.js.map +1 -0
  264. package/dist/src/portfolio/evidence.d.ts +9 -0
  265. package/dist/src/portfolio/evidence.js +233 -0
  266. package/dist/src/portfolio/evidence.js.map +1 -0
  267. package/dist/src/portfolio/files.d.ts +8 -0
  268. package/dist/src/portfolio/files.js +77 -0
  269. package/dist/src/portfolio/files.js.map +1 -0
  270. package/dist/src/portfolio/fixture-server.d.ts +11 -0
  271. package/dist/src/portfolio/fixture-server.js +131 -0
  272. package/dist/src/portfolio/fixture-server.js.map +1 -0
  273. package/dist/src/portfolio/loader.d.ts +2 -0
  274. package/dist/src/portfolio/loader.js +21 -0
  275. package/dist/src/portfolio/loader.js.map +1 -0
  276. package/dist/src/portfolio/mcp.d.ts +186 -0
  277. package/dist/src/portfolio/mcp.js +117 -0
  278. package/dist/src/portfolio/mcp.js.map +1 -0
  279. package/dist/src/portfolio/promotion-schema.d.ts +220 -0
  280. package/dist/src/portfolio/promotion-schema.js +148 -0
  281. package/dist/src/portfolio/promotion-schema.js.map +1 -0
  282. package/dist/src/portfolio/promotion.d.ts +4 -0
  283. package/dist/src/portfolio/promotion.js +160 -0
  284. package/dist/src/portfolio/promotion.js.map +1 -0
  285. package/dist/src/portfolio/qualification-schema.d.ts +268 -0
  286. package/dist/src/portfolio/qualification-schema.js +112 -0
  287. package/dist/src/portfolio/qualification-schema.js.map +1 -0
  288. package/dist/src/portfolio/qualification.d.ts +4 -0
  289. package/dist/src/portfolio/qualification.js +157 -0
  290. package/dist/src/portfolio/qualification.js.map +1 -0
  291. package/dist/src/portfolio/registry.d.ts +34 -0
  292. package/dist/src/portfolio/registry.js +240 -0
  293. package/dist/src/portfolio/registry.js.map +1 -0
  294. package/dist/src/portfolio/report.d.ts +4 -0
  295. package/dist/src/portfolio/report.js +47 -0
  296. package/dist/src/portfolio/report.js.map +1 -0
  297. package/dist/src/portfolio/run-schema.d.ts +135 -0
  298. package/dist/src/portfolio/run-schema.js +81 -0
  299. package/dist/src/portfolio/run-schema.js.map +1 -0
  300. package/dist/src/portfolio/runner.d.ts +118 -0
  301. package/dist/src/portfolio/runner.js +423 -0
  302. package/dist/src/portfolio/runner.js.map +1 -0
  303. package/dist/src/portfolio/schema.d.ts +251 -0
  304. package/dist/src/portfolio/schema.js +317 -0
  305. package/dist/src/portfolio/schema.js.map +1 -0
  306. package/dist/src/portfolio/snapshot.d.ts +89 -0
  307. package/dist/src/portfolio/snapshot.js +548 -0
  308. package/dist/src/portfolio/snapshot.js.map +1 -0
  309. package/dist/src/portfolio/taxonomy-schema.d.ts +215 -0
  310. package/dist/src/portfolio/taxonomy-schema.js +74 -0
  311. package/dist/src/portfolio/taxonomy-schema.js.map +1 -0
  312. package/dist/src/portfolio/taxonomy.d.ts +5 -0
  313. package/dist/src/portfolio/taxonomy.js +236 -0
  314. package/dist/src/portfolio/taxonomy.js.map +1 -0
  315. package/dist/src/portfolio/vault.d.ts +53 -0
  316. package/dist/src/portfolio/vault.js +176 -0
  317. package/dist/src/portfolio/vault.js.map +1 -0
  318. package/dist/src/product-brief/loader.d.ts +2 -0
  319. package/dist/src/product-brief/loader.js +21 -0
  320. package/dist/src/product-brief/loader.js.map +1 -0
  321. package/dist/src/product-brief/report.d.ts +2 -0
  322. package/dist/src/product-brief/report.js +22 -0
  323. package/dist/src/product-brief/report.js.map +1 -0
  324. package/dist/src/product-brief/schema.d.ts +367 -0
  325. package/dist/src/product-brief/schema.js +272 -0
  326. package/dist/src/product-brief/schema.js.map +1 -0
  327. package/dist/src/product-brief/validator.d.ts +2 -0
  328. package/dist/src/product-brief/validator.js +251 -0
  329. package/dist/src/product-brief/validator.js.map +1 -0
  330. package/dist/src/product.d.ts +7 -0
  331. package/dist/src/product.js +28 -0
  332. package/dist/src/product.js.map +1 -0
  333. package/dist/src/quality-gate/report.d.ts +2 -0
  334. package/dist/src/quality-gate/report.js +46 -0
  335. package/dist/src/quality-gate/report.js.map +1 -0
  336. package/dist/src/quality-gate/runner.d.ts +2 -0
  337. package/dist/src/quality-gate/runner.js +175 -0
  338. package/dist/src/quality-gate/runner.js.map +1 -0
  339. package/dist/src/quality-gate/schema.d.ts +75 -0
  340. package/dist/src/quality-gate/schema.js +37 -0
  341. package/dist/src/quality-gate/schema.js.map +1 -0
  342. package/dist/src/quality-gate/types.d.ts +49 -0
  343. package/dist/src/quality-gate/types.js +2 -0
  344. package/dist/src/quality-gate/types.js.map +1 -0
  345. package/dist/src/repair/loader.d.ts +2 -0
  346. package/dist/src/repair/loader.js +11 -0
  347. package/dist/src/repair/loader.js.map +1 -0
  348. package/dist/src/repair/policy.d.ts +16 -0
  349. package/dist/src/repair/policy.js +65 -0
  350. package/dist/src/repair/policy.js.map +1 -0
  351. package/dist/src/repair/report.d.ts +2 -0
  352. package/dist/src/repair/report.js +32 -0
  353. package/dist/src/repair/report.js.map +1 -0
  354. package/dist/src/repair/runner.d.ts +17 -0
  355. package/dist/src/repair/runner.js +511 -0
  356. package/dist/src/repair/runner.js.map +1 -0
  357. package/dist/src/repair/schema.d.ts +264 -0
  358. package/dist/src/repair/schema.js +202 -0
  359. package/dist/src/repair/schema.js.map +1 -0
  360. package/dist/src/retrieval/report.d.ts +2 -0
  361. package/dist/src/retrieval/report.js +21 -0
  362. package/dist/src/retrieval/report.js.map +1 -0
  363. package/dist/src/retrieval/schema.d.ts +123 -0
  364. package/dist/src/retrieval/schema.js +77 -0
  365. package/dist/src/retrieval/schema.js.map +1 -0
  366. package/dist/src/retrieval/search.d.ts +26 -0
  367. package/dist/src/retrieval/search.js +415 -0
  368. package/dist/src/retrieval/search.js.map +1 -0
  369. package/dist/src/runtime/advanced-checks.d.ts +6 -0
  370. package/dist/src/runtime/advanced-checks.js +487 -0
  371. package/dist/src/runtime/advanced-checks.js.map +1 -0
  372. package/dist/src/runtime/composition-checks.d.ts +5 -0
  373. package/dist/src/runtime/composition-checks.js +307 -0
  374. package/dist/src/runtime/composition-checks.js.map +1 -0
  375. package/dist/src/runtime/interface-checks.d.ts +12 -0
  376. package/dist/src/runtime/interface-checks.js +224 -0
  377. package/dist/src/runtime/interface-checks.js.map +1 -0
  378. package/dist/src/runtime/policy.d.ts +13 -0
  379. package/dist/src/runtime/policy.js +97 -0
  380. package/dist/src/runtime/policy.js.map +1 -0
  381. package/dist/src/runtime/report.d.ts +2 -0
  382. package/dist/src/runtime/report.js +48 -0
  383. package/dist/src/runtime/report.js.map +1 -0
  384. package/dist/src/runtime/schema.d.ts +406 -0
  385. package/dist/src/runtime/schema.js +308 -0
  386. package/dist/src/runtime/schema.js.map +1 -0
  387. package/dist/src/runtime/types.d.ts +177 -0
  388. package/dist/src/runtime/types.js +2 -0
  389. package/dist/src/runtime/types.js.map +1 -0
  390. package/dist/src/runtime/verifier.d.ts +2 -0
  391. package/dist/src/runtime/verifier.js +1201 -0
  392. package/dist/src/runtime/verifier.js.map +1 -0
  393. package/dist/src/server.d.ts +2 -0
  394. package/dist/src/server.js +1352 -0
  395. package/dist/src/server.js.map +1 -0
  396. package/docs/installation.md +337 -0
  397. package/knowledge-base/INDEX.md +285 -0
  398. package/knowledge-base/benchmarks/aegisops/CALIBRATION.md +52 -0
  399. package/knowledge-base/benchmarks/aegisops/MANIFEST.md +56 -0
  400. package/knowledge-base/benchmarks/aegisops/SOURCE-EVIDENCE.md +45 -0
  401. package/knowledge-base/benchmarks/aegisops/acceptance-criteria.md +42 -0
  402. package/knowledge-base/benchmarks/aegisops/anti-patterns.md +19 -0
  403. package/knowledge-base/benchmarks/aegisops/attestations.template.yaml +3 -0
  404. package/knowledge-base/benchmarks/aegisops/attestations.yaml +115 -0
  405. package/knowledge-base/benchmarks/aegisops/journeys.json +91 -0
  406. package/knowledge-base/benchmarks/aegisops/product-contract.yaml +328 -0
  407. package/knowledge-base/benchmarks/azure-optimizer/SOURCE-EVIDENCE.md +26 -0
  408. package/knowledge-base/benchmarks/azure-optimizer/V2-IMPLEMENTATION.md +39 -0
  409. package/knowledge-base/benchmarks/azure-optimizer/acceptance-criteria.md +23 -0
  410. package/knowledge-base/benchmarks/azure-optimizer/anti-patterns.md +13 -0
  411. package/knowledge-base/benchmarks/azure-optimizer/journeys.json +88 -0
  412. package/knowledge-base/benchmarks/azure-optimizer/product-contract.yaml +216 -0
  413. package/knowledge-base/benchmarks/azure-optimizer/v2-human-review-methodology.yaml +171 -0
  414. package/knowledge-base/benchmarks/azure-optimizer/v2-journeys.json +139 -0
  415. package/knowledge-base/benchmarks/corpus/PROVENANCE.md +21 -0
  416. package/knowledge-base/benchmarks/corpus/README.md +30 -0
  417. package/knowledge-base/benchmarks/corpus/cases/anti-slop-negative/package.json +9 -0
  418. package/knowledge-base/benchmarks/corpus/cases/anti-slop-negative/src/FakeDashboard.tsx +10 -0
  419. package/knowledge-base/benchmarks/corpus/cases/anti-slop-positive/package.json +9 -0
  420. package/knowledge-base/benchmarks/corpus/cases/anti-slop-positive/src/Progress.tsx +15 -0
  421. package/knowledge-base/benchmarks/corpus/cases/architecture-negative/package.json +9 -0
  422. package/knowledge-base/benchmarks/corpus/cases/architecture-negative/src/OperationsPanel.tsx +40 -0
  423. package/knowledge-base/benchmarks/corpus/cases/architecture-positive/package.json +9 -0
  424. package/knowledge-base/benchmarks/corpus/cases/architecture-positive/src/Dashboard.tsx +17 -0
  425. package/knowledge-base/benchmarks/corpus/cases/architecture-positive/src/theme.css +15 -0
  426. package/knowledge-base/benchmarks/corpus/cases/task-incomplete/SOURCE-EVIDENCE.md +3 -0
  427. package/knowledge-base/benchmarks/corpus/cases/task-incomplete/journeys.json +20 -0
  428. package/knowledge-base/benchmarks/corpus/cases/task-incomplete/product-contract.yaml +78 -0
  429. package/knowledge-base/benchmarks/corpus/corpus.schema.yaml +159 -0
  430. package/knowledge-base/benchmarks/corpus/corpus.yaml +209 -0
  431. package/knowledge-base/benchmarks/interface-quality/README.md +26 -0
  432. package/knowledge-base/benchmarks/interface-quality/SOLO-MAINTAINER-TRACK.md +35 -0
  433. package/knowledge-base/benchmarks/interface-quality/comparison-methodology-v1.1.schema.yaml +288 -0
  434. package/knowledge-base/benchmarks/interface-quality/comparison-methodology.schema.yaml +235 -0
  435. package/knowledge-base/benchmarks/interface-quality/comparison-methodology.template.yaml +181 -0
  436. package/knowledge-base/benchmarks/interface-quality/evidence/example-runtime-report.json +11 -0
  437. package/knowledge-base/benchmarks/interface-quality/review-session.schema.yaml +14 -0
  438. package/knowledge-base/benchmarks/interface-quality/review.schema.yaml +296 -0
  439. package/knowledge-base/benchmarks/interface-quality/review.template.yaml +43 -0
  440. package/knowledge-base/benchmarks/portfolio-corpus/ADAPTERS.md +68 -0
  441. package/knowledge-base/benchmarks/portfolio-corpus/PORTFOLIO-RUNNER.md +131 -0
  442. package/knowledge-base/benchmarks/portfolio-corpus/PRODUCT-TASK-CONTRACTS.md +29 -0
  443. package/knowledge-base/benchmarks/portfolio-corpus/PROMOTED-RULES.md +31 -0
  444. package/knowledge-base/benchmarks/portfolio-corpus/archetype-profiles.yaml +26 -0
  445. package/knowledge-base/benchmarks/portfolio-corpus/product-task-profile.schema.yaml +105 -0
  446. package/knowledge-base/benchmarks/portfolio-corpus/registry.schema.yaml +188 -0
  447. package/knowledge-base/benchmarks/portfolio-corpus/registry.template.yaml +63 -0
  448. package/knowledge-base/benchmarks/portfolio-corpus/rule-fixtures/component-size-abstention.json +10 -0
  449. package/knowledge-base/benchmarks/portfolio-corpus/rule-fixtures/component-size-negative.json +10 -0
  450. package/knowledge-base/benchmarks/portfolio-corpus/rule-fixtures/component-size-positive.json +10 -0
  451. package/knowledge-base/benchmarks/portfolio-corpus/rule-fixtures/interactive-integrity-abstention.json +10 -0
  452. package/knowledge-base/benchmarks/portfolio-corpus/rule-fixtures/interactive-integrity-negative.json +10 -0
  453. package/knowledge-base/benchmarks/portfolio-corpus/rule-fixtures/interactive-integrity-positive.json +10 -0
  454. package/knowledge-base/benchmarks/portfolio-corpus/rule-fixtures/raw-design-values-abstention.json +10 -0
  455. package/knowledge-base/benchmarks/portfolio-corpus/rule-fixtures/raw-design-values-negative.json +10 -0
  456. package/knowledge-base/benchmarks/portfolio-corpus/rule-fixtures/raw-design-values-positive.json +10 -0
  457. package/knowledge-base/benchmarks/portfolio-corpus/rule-holdout-evaluation.template.json +14 -0
  458. package/knowledge-base/benchmarks/portfolio-corpus/rule-promotion-evidence.template.json +24 -0
  459. package/knowledge-base/benchmarks/portfolio-corpus/v3-command-evidence.template.json +17 -0
  460. package/knowledge-base/benchmarks/portfolio-corpus/v3-qualification-evidence.template.json +29 -0
  461. package/knowledge-base/benchmarks/scenestart/CALIBRATION.md +95 -0
  462. package/knowledge-base/benchmarks/scenestart/MANIFEST.md +53 -0
  463. package/knowledge-base/benchmarks/scenestart/SOURCE-EVIDENCE.md +43 -0
  464. package/knowledge-base/benchmarks/scenestart/acceptance-criteria.md +59 -0
  465. package/knowledge-base/benchmarks/scenestart/anti-patterns.md +26 -0
  466. package/knowledge-base/benchmarks/scenestart/attestations.template.yaml +3 -0
  467. package/knowledge-base/benchmarks/scenestart/attestations.yaml +21 -0
  468. package/knowledge-base/benchmarks/scenestart/human-review.md +99 -0
  469. package/knowledge-base/benchmarks/scenestart/human-review.template.md +105 -0
  470. package/knowledge-base/benchmarks/scenestart/journeys.json +141 -0
  471. package/knowledge-base/benchmarks/scenestart/product-contract.yaml +427 -0
  472. package/knowledge-base/dependencies.yaml +69 -0
  473. package/knowledge-base/design-intelligence/MASTER.md +87 -0
  474. package/knowledge-base/design-intelligence/asset-generation.md +66 -0
  475. package/knowledge-base/design-intelligence/brand-systems.md +69 -0
  476. package/knowledge-base/design-intelligence/closed-loop-repair.md +95 -0
  477. package/knowledge-base/design-intelligence/design-deliverable.schema.yaml +1194 -0
  478. package/knowledge-base/design-intelligence/design-deliverable.template.yaml +591 -0
  479. package/knowledge-base/design-intelligence/design-plan.md +78 -0
  480. package/knowledge-base/design-intelligence/design-plan.schema.yaml +621 -0
  481. package/knowledge-base/design-intelligence/figma-production.md +63 -0
  482. package/knowledge-base/design-intelligence/generation-adapter.md +72 -0
  483. package/knowledge-base/design-intelligence/generation-adapter.schema.yaml +47 -0
  484. package/knowledge-base/design-intelligence/iconography.md +46 -0
  485. package/knowledge-base/design-intelligence/information-design.md +104 -0
  486. package/knowledge-base/design-intelligence/information-design.schema.yaml +390 -0
  487. package/knowledge-base/design-intelligence/information-design.template.yaml +281 -0
  488. package/knowledge-base/design-intelligence/interaction-recovery-verification.md +72 -0
  489. package/knowledge-base/design-intelligence/interface-trust.md +103 -0
  490. package/knowledge-base/design-intelligence/interface-trust.schema.yaml +263 -0
  491. package/knowledge-base/design-intelligence/interface-trust.template.yaml +173 -0
  492. package/knowledge-base/design-intelligence/licensing-and-provenance.md +59 -0
  493. package/knowledge-base/design-intelligence/presentation-design.md +56 -0
  494. package/knowledge-base/design-intelligence/product-design-brief.md +67 -0
  495. package/knowledge-base/design-intelligence/product-design-brief.schema.yaml +279 -0
  496. package/knowledge-base/design-intelligence/product-design-brief.template.yaml +225 -0
  497. package/knowledge-base/design-intelligence/repair-request.schema.yaml +95 -0
  498. package/knowledge-base/design-intelligence/repair-request.template.yaml +39 -0
  499. package/knowledge-base/design-intelligence/visual-accessibility.md +56 -0
  500. package/knowledge-base/design-intelligence/visual-polish.md +172 -0
  501. package/knowledge-base/maintained/architecture/MASTER.md +22 -0
  502. package/knowledge-base/maintained/architecture/component-boundaries.md +30 -0
  503. package/knowledge-base/maintained/architecture/product-platforms.md +25 -0
  504. package/knowledge-base/maintained/architecture/quality-attributes-and-evaluation.md +35 -0
  505. package/knowledge-base/maintained/product-patterns/MASTER.md +10 -0
  506. package/knowledge-base/maintained/product-patterns/ai-workspaces.md +26 -0
  507. package/knowledge-base/maintained/product-patterns/operational-dashboards.md +22 -0
  508. package/knowledge-base/provenance.yaml +196 -0
  509. package/knowledge-base/retrieval-scope.yaml +43 -0
  510. package/knowledge-base/usability-evaluation/HEURISTIC_EVALUATION.md +115 -0
  511. package/knowledge-base/usability-evaluation/heuristic-review.schema.yaml +210 -0
  512. package/knowledge-base/usability-evaluation/heuristic-review.template.yaml +37 -0
  513. package/package.json +133 -0
@@ -0,0 +1,1201 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { chromium } from "playwright-core";
5
+ import { checkRenderedAssets, checkRenderedVisualContrast, checkVisualComposition, } from "./composition-checks.js";
6
+ import { checkReducedMotion, checkReflowAndTextResize, checkTextContrast, checkTouchTargets, } from "./advanced-checks.js";
7
+ import { addInterfaceCoverageFindings, checkChartContracts, checkInterfaceTrust, createInterfaceCoverage, } from "./interface-checks.js";
8
+ import { DEFAULT_NAVIGATION_TIMEOUT_MS, DEFAULT_RUNTIME_COLOR_SCHEMES, DEFAULT_RUNTIME_VIEWPORTS, DEFAULT_SETTLE_MS, MAX_JOURNEY_STEPS, MAX_RUNTIME_JOURNEYS, resolveChromiumCdpUrl, resolveChromiumPath, validateRuntimeUrl, validateColorSchemes, validateViewports, } from "./policy.js";
9
+ import { formatRuntimeReport } from "./report.js";
10
+ import { runtimeExpectedNetworkSchema, runtimeJourneySchema, runtimeScreenshotBaselineSchema, } from "./schema.js";
11
+ const RUNTIME_REPORT_VERSION = "1.1.0";
12
+ const MAX_DOWNLOAD_FALLBACK_BYTES = 20 * 1024 * 1024;
13
+ const CAPTURED_BLOB_STORE = "__ztothezDesignCapturedBlobsV1";
14
+ function createExpectedNetworkTrackers(policies) {
15
+ const validated = runtimeExpectedNetworkSchema.array().max(20).parse(policies);
16
+ const ids = validated.map((policy) => policy.id);
17
+ if (new Set(ids).size !== ids.length) {
18
+ throw new Error("Expected network policy IDs must be unique");
19
+ }
20
+ return validated.map((policy) => ({ ...policy, occurrences: 0, evidence: [] }));
21
+ }
22
+ function matchingResponsePolicy(trackers, method, url, status) {
23
+ return trackers.find((policy) => policy.status === status && policy.method === method && url.includes(policy.urlIncludes));
24
+ }
25
+ function matchingRequestFailurePolicy(trackers, method, url) {
26
+ return trackers.find((policy) => policy.allowRequestFailure === true &&
27
+ policy.method === method &&
28
+ url.includes(policy.urlIncludes));
29
+ }
30
+ function recordExpectedNetwork(policy, evidence) {
31
+ policy.occurrences += 1;
32
+ policy.evidence.push(evidence);
33
+ }
34
+ function safeName(value) {
35
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
36
+ return normalized.slice(0, 80) || "evidence";
37
+ }
38
+ function jsonValueAtPath(value, path) {
39
+ return path.split(".").reduce((current, segment) => {
40
+ if (!current || typeof current !== "object" || !(segment in current))
41
+ return undefined;
42
+ return current[segment];
43
+ }, value);
44
+ }
45
+ async function installBlobDownloadCapture(page) {
46
+ await page.addInitScript(({ storeName, retentionMs }) => {
47
+ const scope = globalThis;
48
+ if (scope[storeName])
49
+ return;
50
+ const blobs = new Map();
51
+ Object.defineProperty(scope, storeName, {
52
+ configurable: false,
53
+ enumerable: false,
54
+ value: blobs,
55
+ writable: false,
56
+ });
57
+ const createObjectUrl = URL.createObjectURL.bind(URL);
58
+ const revokeObjectUrl = URL.revokeObjectURL.bind(URL);
59
+ URL.createObjectURL = (object) => {
60
+ const url = createObjectUrl(object);
61
+ if (object instanceof Blob)
62
+ blobs.set(url, object);
63
+ return url;
64
+ };
65
+ URL.revokeObjectURL = (url) => {
66
+ revokeObjectUrl(url);
67
+ globalThis.setTimeout(() => blobs.delete(url), retentionMs);
68
+ };
69
+ }, { storeName: CAPTURED_BLOB_STORE, retentionMs: 60_000 });
70
+ }
71
+ async function capturedBlobBody(page, downloadUrl) {
72
+ if (!/^blob:/i.test(downloadUrl))
73
+ return undefined;
74
+ const bytes = await page.evaluate(async ({ url, storeName, maximumBytes }) => {
75
+ const blob = globalThis[storeName]?.get(url);
76
+ if (!blob)
77
+ return undefined;
78
+ if (blob.size > maximumBytes) {
79
+ throw new Error("Captured Blob exceeds the 20 MB evidence limit");
80
+ }
81
+ return [...new Uint8Array(await blob.arrayBuffer())];
82
+ }, {
83
+ url: downloadUrl,
84
+ storeName: CAPTURED_BLOB_STORE,
85
+ maximumBytes: MAX_DOWNLOAD_FALLBACK_BYTES,
86
+ });
87
+ return bytes ? Buffer.from(bytes) : undefined;
88
+ }
89
+ async function downloadFallbackBody(page, downloadUrl, timeoutMs) {
90
+ if (/^https?:/i.test(downloadUrl)) {
91
+ const response = await page.request.get(downloadUrl, { timeout: timeoutMs });
92
+ if (!response.ok()) {
93
+ throw new Error(`Download fallback request returned ${response.status()}`);
94
+ }
95
+ const body = await response.body();
96
+ if (body.byteLength > MAX_DOWNLOAD_FALLBACK_BYTES) {
97
+ throw new Error("Download fallback exceeded the 20 MB evidence limit");
98
+ }
99
+ return body;
100
+ }
101
+ if (/^data:/i.test(downloadUrl)) {
102
+ const match = /^data:([^,]*),(.*)$/s.exec(downloadUrl);
103
+ if (!match)
104
+ throw new Error("Downloaded data URL is malformed");
105
+ const metadata = match[1] ?? "";
106
+ const payload = match[2] ?? "";
107
+ const body = /;base64(?:;|$)/i.test(metadata)
108
+ ? Buffer.from(payload, "base64")
109
+ : Buffer.from(decodeURIComponent(payload), "utf8");
110
+ if (body.byteLength > MAX_DOWNLOAD_FALLBACK_BYTES) {
111
+ throw new Error("Download fallback exceeded the 20 MB evidence limit");
112
+ }
113
+ return body;
114
+ }
115
+ if (/^blob:/i.test(downloadUrl)) {
116
+ const bytes = await page.evaluate(async ({ url, maximumBytes }) => {
117
+ const response = await fetch(url);
118
+ if (!response.ok)
119
+ throw new Error(`Blob download returned ${response.status}`);
120
+ const body = await response.arrayBuffer();
121
+ if (body.byteLength > maximumBytes)
122
+ throw new Error("Blob download exceeds evidence limit");
123
+ return [...new Uint8Array(body)];
124
+ }, { url: downloadUrl, maximumBytes: MAX_DOWNLOAD_FALLBACK_BYTES });
125
+ return Buffer.from(bytes);
126
+ }
127
+ throw new Error(`Unsupported download fallback URL: ${downloadUrl.split(":", 1)[0] || "unknown"}`);
128
+ }
129
+ function addFinding(findings, checkId, severity, message, evidence, context, selector) {
130
+ findings.push({ checkId, severity, message, evidence, ...context, ...(selector ? { selector } : {}) });
131
+ }
132
+ function attachDiagnostics(page, findings, context, expectedNetwork) {
133
+ page.on("console", (message) => {
134
+ if (message.type() !== "error" && message.type() !== "warning") {
135
+ return;
136
+ }
137
+ const locationUrl = message.location().url;
138
+ const networkConsoleMessage = /failed to load resource|server responded with a status/i.test(message.text());
139
+ if (networkConsoleMessage &&
140
+ locationUrl &&
141
+ expectedNetwork.some((policy) => locationUrl.includes(policy.urlIncludes))) {
142
+ return;
143
+ }
144
+ addFinding(findings, "ZTDE-RUNTIME-002", message.type() === "error" ? "error" : "warning", `Browser console ${message.type()}: ${message.text()}`, [`Source: ${message.location().url || "unknown"}:${message.location().lineNumber ?? 0}.`], context);
145
+ });
146
+ page.on("pageerror", (error) => {
147
+ addFinding(findings, "ZTDE-RUNTIME-002", "error", `Uncaught page error: ${error.message}`, [error.stack ?? error.message], context);
148
+ });
149
+ page.on("requestfailed", (request) => {
150
+ const expected = matchingRequestFailurePolicy(expectedNetwork, request.method(), request.url());
151
+ if (expected) {
152
+ recordExpectedNetwork(expected, `${request.method()} ${request.url()} failed: ${request.failure()?.errorText ?? "unknown"}`);
153
+ return;
154
+ }
155
+ const resourceType = request.resourceType();
156
+ addFinding(findings, "ZTDE-RUNTIME-003", resourceType === "document" || resourceType === "fetch" || resourceType === "xhr"
157
+ ? "error"
158
+ : "warning", `Request failed: ${request.method()} ${request.url()}`, [`Resource type: ${resourceType}.`, `Failure: ${request.failure()?.errorText ?? "unknown"}.`], context);
159
+ });
160
+ page.on("response", (response) => {
161
+ if (response.status() < 400) {
162
+ return;
163
+ }
164
+ const expected = matchingResponsePolicy(expectedNetwork, response.request().method(), response.url(), response.status());
165
+ if (expected) {
166
+ recordExpectedNetwork(expected, `${response.request().method()} ${response.url()} returned ${response.status()}`);
167
+ return;
168
+ }
169
+ const resourceType = response.request().resourceType();
170
+ addFinding(findings, "ZTDE-RUNTIME-003", resourceType === "document" || resourceType === "fetch" || resourceType === "xhr"
171
+ ? "error"
172
+ : "warning", `HTTP ${response.status()} from ${response.url()}`, [`Resource type: ${resourceType}.`, `Status text: ${response.statusText() || "unavailable"}.`], context);
173
+ });
174
+ }
175
+ function assertSameOrigin(page, expectedOrigin) {
176
+ const currentUrl = new URL(page.url());
177
+ if (currentUrl.origin !== expectedOrigin) {
178
+ throw new Error(`Navigation left the allowed origin: expected ${expectedOrigin}, received ${currentUrl.origin}`);
179
+ }
180
+ }
181
+ async function checkOverflow(page, viewport, findings) {
182
+ const result = await page.evaluate(() => {
183
+ function selectorFor(element) {
184
+ if (element.id)
185
+ return `#${CSS.escape(element.id)}`;
186
+ const parts = [];
187
+ let current = element;
188
+ while (current && parts.length < 4 && current !== document.body) {
189
+ let part = current.tagName.toLowerCase();
190
+ const className = [...current.classList].find((name) => /^[a-zA-Z][\w-]*$/.test(name));
191
+ if (className)
192
+ part += `.${CSS.escape(className)}`;
193
+ parts.unshift(part);
194
+ current = current.parentElement;
195
+ }
196
+ return parts.join(" > ");
197
+ }
198
+ function hasScrollContainer(element) {
199
+ let parent = element.parentElement;
200
+ while (parent && parent !== document.body) {
201
+ const style = getComputedStyle(parent);
202
+ if (/auto|scroll/.test(style.overflowX) && parent.scrollWidth > parent.clientWidth) {
203
+ return true;
204
+ }
205
+ parent = parent.parentElement;
206
+ }
207
+ return false;
208
+ }
209
+ function isVisible(element) {
210
+ const rect = element.getBoundingClientRect();
211
+ const style = getComputedStyle(element);
212
+ return (rect.width > 1 &&
213
+ rect.height > 1 &&
214
+ style.display !== "none" &&
215
+ style.visibility !== "hidden" &&
216
+ style.opacity !== "0" &&
217
+ element.getAttribute("aria-hidden") !== "true");
218
+ }
219
+ function isSemanticCandidate(element) {
220
+ const interactiveSelector = 'a[href], button, input:not([type="hidden"]), select, textarea, [role="button"]';
221
+ if (element.matches(interactiveSelector))
222
+ return true;
223
+ if (element.closest(interactiveSelector))
224
+ return false;
225
+ return element.children.length === 0 && (element.textContent?.trim().length ?? 0) > 0;
226
+ }
227
+ function clippedByAncestor(element) {
228
+ const elementRect = element.getBoundingClientRect();
229
+ let ancestor = element.parentElement;
230
+ while (ancestor && ancestor !== document.body && ancestor !== document.documentElement) {
231
+ if (element.hasAttribute("data-ztothez-design-allow-clipping") ||
232
+ ancestor.hasAttribute("data-ztothez-design-allow-clipping")) {
233
+ return null;
234
+ }
235
+ const style = getComputedStyle(ancestor);
236
+ if (style.overflowX === "hidden" || style.overflowX === "clip") {
237
+ const ancestorRect = ancestor.getBoundingClientRect();
238
+ const clippedLeft = ancestorRect.left - elementRect.left;
239
+ const clippedRight = elementRect.right - ancestorRect.right;
240
+ const overlapsAncestor = elementRect.right > ancestorRect.left + 2 && elementRect.left < ancestorRect.right - 2;
241
+ const overlapsViewport = elementRect.right > 2 && elementRect.left < window.innerWidth - 2;
242
+ if (ancestorRect.width > 1 &&
243
+ overlapsAncestor &&
244
+ overlapsViewport &&
245
+ (clippedLeft > 2 || clippedRight > 2)) {
246
+ return {
247
+ selector: selectorFor(element),
248
+ ancestor: selectorFor(ancestor),
249
+ left: Math.round(elementRect.left),
250
+ right: Math.round(elementRect.right),
251
+ clipLeft: Math.round(ancestorRect.left),
252
+ clipRight: Math.round(ancestorRect.right),
253
+ };
254
+ }
255
+ }
256
+ ancestor = ancestor.parentElement;
257
+ }
258
+ return null;
259
+ }
260
+ const root = document.documentElement;
261
+ const globalOverflow = Math.max(0, root.scrollWidth - root.clientWidth);
262
+ const offenders = [...document.body.querySelectorAll("*")]
263
+ .filter((element) => {
264
+ const rect = element.getBoundingClientRect();
265
+ const style = getComputedStyle(element);
266
+ if (style.display === "none" || style.visibility === "hidden" || rect.width < 1)
267
+ return false;
268
+ if (hasScrollContainer(element))
269
+ return false;
270
+ return rect.left < -1 || rect.right > window.innerWidth + 1;
271
+ })
272
+ .slice(0, 12)
273
+ .map((element) => {
274
+ const rect = element.getBoundingClientRect();
275
+ return {
276
+ selector: selectorFor(element),
277
+ left: Math.round(rect.left),
278
+ right: Math.round(rect.right),
279
+ width: Math.round(rect.width),
280
+ };
281
+ });
282
+ const clipped = [...document.body.querySelectorAll("*")]
283
+ .filter((element) => isVisible(element) && isSemanticCandidate(element))
284
+ .map(clippedByAncestor)
285
+ .filter((entry) => entry !== null)
286
+ .filter((entry, index, entries) => entries.findIndex((candidate) => candidate.selector === entry.selector && candidate.ancestor === entry.ancestor) === index)
287
+ .slice(0, 12);
288
+ return {
289
+ globalOverflow,
290
+ documentWidth: root.scrollWidth,
291
+ viewportWidth: root.clientWidth,
292
+ offenders,
293
+ clipped,
294
+ };
295
+ });
296
+ if (result.globalOverflow > 1) {
297
+ addFinding(findings, "ZTDE-RUNTIME-004", "error", `Page overflows horizontally by ${result.globalOverflow}px at ${viewport.width}px.`, [
298
+ `Document width: ${result.documentWidth}px; viewport width: ${result.viewportWidth}px.`,
299
+ ...result.offenders.map((offender) => `${offender.selector}: left ${offender.left}px, right ${offender.right}px, width ${offender.width}px.`),
300
+ ], { viewport: viewport.name }, result.offenders[0]?.selector);
301
+ }
302
+ for (const clipped of result.clipped) {
303
+ addFinding(findings, "ZTDE-RUNTIME-004", "error", "Visible semantic content is clipped by an overflow-hidden ancestor.", [
304
+ `${clipped.selector}: left ${clipped.left}px, right ${clipped.right}px.`,
305
+ `${clipped.ancestor}: clipping bounds ${clipped.clipLeft}px to ${clipped.clipRight}px.`,
306
+ "Add data-ztothez-design-allow-clipping only when clipping this content is intentional and verified.",
307
+ ], { viewport: viewport.name }, clipped.selector);
308
+ }
309
+ }
310
+ async function checkLayoutCollisions(page, viewport, findings) {
311
+ const result = await page.evaluate(async () => {
312
+ function selectorFor(element) {
313
+ if (element.id)
314
+ return `#${CSS.escape(element.id)}`;
315
+ const parts = [];
316
+ let current = element;
317
+ while (current && parts.length < 4 && current !== document.body) {
318
+ let part = current.tagName.toLowerCase();
319
+ const className = [...current.classList].find((name) => /^[a-zA-Z][\w-]*$/.test(name));
320
+ if (className)
321
+ part += `.${CSS.escape(className)}`;
322
+ parts.unshift(part);
323
+ current = current.parentElement;
324
+ }
325
+ return parts.join(" > ");
326
+ }
327
+ function isVisible(element) {
328
+ const rect = element.getBoundingClientRect();
329
+ const style = getComputedStyle(element);
330
+ return (rect.width > 1 &&
331
+ rect.height > 1 &&
332
+ rect.bottom > 0 &&
333
+ rect.top < window.innerHeight &&
334
+ rect.right > 0 &&
335
+ rect.left < window.innerWidth &&
336
+ style.display !== "none" &&
337
+ style.visibility !== "hidden" &&
338
+ Number.parseFloat(style.opacity || "1") > 0.01 &&
339
+ element.getAttribute("aria-hidden") !== "true");
340
+ }
341
+ function allowsOverlap(element) {
342
+ return Boolean(element.closest("[data-ztothez-design-allow-overlap]"));
343
+ }
344
+ function independent(first, second) {
345
+ return !first.contains(second) && !second.contains(first);
346
+ }
347
+ function intersection(first, second) {
348
+ const left = Math.max(first.left, second.left, 0);
349
+ const right = Math.min(first.right, second.right, window.innerWidth);
350
+ const top = Math.max(first.top, second.top, 0);
351
+ const bottom = Math.min(first.bottom, second.bottom, window.innerHeight);
352
+ return { left, right, top, bottom, width: right - left, height: bottom - top };
353
+ }
354
+ function topElementInIntersection(first, second, overlap) {
355
+ const x = overlap.left + overlap.width / 2;
356
+ const y = overlap.top + overlap.height / 2;
357
+ const top = document.elementFromPoint(x, y);
358
+ if (!top)
359
+ return null;
360
+ if (first === top || first.contains(top))
361
+ return first;
362
+ if (second === top || second.contains(top))
363
+ return second;
364
+ return null;
365
+ }
366
+ const interactiveSelector = 'a[href], button:not([disabled]), input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), [role="button"], [role="switch"], [tabindex]:not([tabindex="-1"])';
367
+ const interactive = [...document.querySelectorAll(interactiveSelector)].filter((element) => isVisible(element) && !allowsOverlap(element));
368
+ const interactiveCollisions = [];
369
+ for (let firstIndex = 0; firstIndex < interactive.length; firstIndex += 1) {
370
+ for (let secondIndex = firstIndex + 1; secondIndex < interactive.length; secondIndex += 1) {
371
+ const first = interactive[firstIndex];
372
+ const second = interactive[secondIndex];
373
+ if (!independent(first, second))
374
+ continue;
375
+ const overlap = intersection(first.getBoundingClientRect(), second.getBoundingClientRect());
376
+ if (overlap.width <= 3 || overlap.height <= 3 || overlap.width * overlap.height < 36)
377
+ continue;
378
+ if (!topElementInIntersection(first, second, overlap))
379
+ continue;
380
+ interactiveCollisions.push({
381
+ first: selectorFor(first),
382
+ second: selectorFor(second),
383
+ width: Math.round(overlap.width),
384
+ height: Math.round(overlap.height),
385
+ });
386
+ if (interactiveCollisions.length >= 12)
387
+ break;
388
+ }
389
+ if (interactiveCollisions.length >= 12)
390
+ break;
391
+ }
392
+ const originalScrollY = window.scrollY;
393
+ const maximumScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
394
+ const scrollPositions = [...new Set([0, maximumScroll * 0.33, maximumScroll * 0.66, maximumScroll])]
395
+ .map((value) => Math.round(value))
396
+ .filter((value) => value >= 0);
397
+ const occlusions = [];
398
+ const semanticSelector = `${interactiveSelector}, h1, h2, h3, h4, h5, h6, p, li, td, th, label, [role="status"], [role="alert"]`;
399
+ for (const scrollY of scrollPositions) {
400
+ window.scrollTo(0, scrollY);
401
+ await new Promise((resolveFrame) => requestAnimationFrame(() => requestAnimationFrame(() => resolveFrame())));
402
+ const occluders = [...document.body.querySelectorAll("*")].filter((element) => {
403
+ if (!isVisible(element) || allowsOverlap(element))
404
+ return false;
405
+ if (element.closest('[role="dialog"], [aria-modal="true"]'))
406
+ return false;
407
+ const style = getComputedStyle(element);
408
+ if (style.position !== "fixed" && style.position !== "sticky")
409
+ return false;
410
+ const rect = element.getBoundingClientRect();
411
+ const viewportArea = window.innerWidth * window.innerHeight;
412
+ return ((rect.width * rect.height) / viewportArea >= 0.2 ||
413
+ (rect.width / window.innerWidth >= 0.9 && rect.height / window.innerHeight >= 0.25));
414
+ });
415
+ const targets = [...document.querySelectorAll(semanticSelector)].filter((element) => isVisible(element) &&
416
+ !allowsOverlap(element) &&
417
+ !element.closest('[inert], [aria-hidden="true"]'));
418
+ for (const occluder of occluders) {
419
+ for (const target of targets) {
420
+ if (!independent(occluder, target))
421
+ continue;
422
+ const overlap = intersection(occluder.getBoundingClientRect(), target.getBoundingClientRect());
423
+ if (overlap.width <= 4 || overlap.height <= 4 || overlap.width * overlap.height < 64)
424
+ continue;
425
+ if (topElementInIntersection(occluder, target, overlap) !== occluder)
426
+ continue;
427
+ occlusions.push({
428
+ first: selectorFor(occluder),
429
+ second: selectorFor(target),
430
+ width: Math.round(overlap.width),
431
+ height: Math.round(overlap.height),
432
+ scrollY,
433
+ });
434
+ if (occlusions.length >= 12)
435
+ break;
436
+ }
437
+ if (occlusions.length >= 12)
438
+ break;
439
+ }
440
+ if (occlusions.length >= 12)
441
+ break;
442
+ }
443
+ window.scrollTo(0, originalScrollY);
444
+ await new Promise((resolveFrame) => requestAnimationFrame(() => resolveFrame()));
445
+ return { interactiveCollisions, occlusions };
446
+ });
447
+ for (const collision of result.interactiveCollisions) {
448
+ addFinding(findings, "ZTDE-RUNTIME-010", "error", "Independent interactive controls overlap in the rendered layout.", [
449
+ `${collision.first} overlaps ${collision.second} by ${collision.width}px by ${collision.height}px.`,
450
+ "Recompose the layout or use data-ztothez-design-allow-overlap only for an intentional, verified interaction.",
451
+ ], { viewport: viewport.name }, collision.first);
452
+ }
453
+ for (const collision of result.occlusions) {
454
+ addFinding(findings, "ZTDE-RUNTIME-010", "error", "Oversized fixed or sticky UI occludes semantic content while scrolling.", [
455
+ `${collision.first} covers ${collision.second} by ${collision.width}px by ${collision.height}px at scrollY ${collision.scrollY}px.`,
456
+ "Reduce the sticky region, reserve layout space, change the mobile navigation pattern, or annotate a verified intentional overlap.",
457
+ ], { viewport: viewport.name }, collision.first);
458
+ }
459
+ }
460
+ async function checkAccessibleNames(page, viewport, findings) {
461
+ const issues = await page.evaluate(() => {
462
+ function selectorFor(element) {
463
+ if (element.id)
464
+ return `#${CSS.escape(element.id)}`;
465
+ const name = element.getAttribute("name");
466
+ if (name)
467
+ return `${element.tagName.toLowerCase()}[name=${JSON.stringify(name)}]`;
468
+ const className = [...element.classList].find((entry) => /^[a-zA-Z][\w-]*$/.test(entry));
469
+ return `${element.tagName.toLowerCase()}${className ? `.${CSS.escape(className)}` : ""}`;
470
+ }
471
+ function labelledByText(element) {
472
+ return (element.getAttribute("aria-labelledby") ?? "")
473
+ .split(/\s+/)
474
+ .filter(Boolean)
475
+ .map((id) => document.getElementById(id)?.textContent ?? "")
476
+ .join(" ")
477
+ .trim();
478
+ }
479
+ return [
480
+ ...document.querySelectorAll('button, a[href], input:not([type="hidden"]), select, textarea, [role="button"]'),
481
+ ]
482
+ .filter((element) => {
483
+ const rect = element.getBoundingClientRect();
484
+ const style = getComputedStyle(element);
485
+ return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
486
+ })
487
+ .filter((element) => {
488
+ const label = element.getAttribute("aria-label")?.trim();
489
+ const labelledBy = labelledByText(element);
490
+ const text = element.textContent?.trim();
491
+ const title = element.getAttribute("title")?.trim();
492
+ const imageAlt = element.querySelector("img[alt]")?.getAttribute("alt")?.trim();
493
+ const inputLabel = element instanceof HTMLInputElement ||
494
+ element instanceof HTMLSelectElement ||
495
+ element instanceof HTMLTextAreaElement
496
+ ? (element.labels?.[0]?.textContent?.trim() ?? "")
497
+ : "";
498
+ return !label && !labelledBy && !text && !title && !imageAlt && !inputLabel;
499
+ })
500
+ .slice(0, 20)
501
+ .map((element) => selectorFor(element));
502
+ });
503
+ for (const selector of issues) {
504
+ addFinding(findings, "ZTDE-RUNTIME-005", "error", "Rendered interactive control has no detectable accessible name.", ["No visible text, associated label, aria-label, aria-labelledby, title, or descendant image alt was found."], { viewport: viewport.name }, selector);
505
+ }
506
+ }
507
+ async function checkKeyboard(page, viewport, findings) {
508
+ const overview = await page.evaluate(() => {
509
+ const modal = document.querySelector('[aria-modal="true"], dialog[open]');
510
+ const scope = modal ?? document;
511
+ const selector = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
512
+ function visible(element) {
513
+ const rect = element.getBoundingClientRect();
514
+ const style = getComputedStyle(element);
515
+ return (rect.width > 1 &&
516
+ rect.height > 1 &&
517
+ style.display !== "none" &&
518
+ style.visibility !== "hidden" &&
519
+ !element.closest('[aria-hidden="true"], [inert], nextjs-portal, [data-ztothez-design-runtime-ignore]'));
520
+ }
521
+ function selectorFor(element) {
522
+ if (element.id)
523
+ return `#${CSS.escape(element.id)}`;
524
+ const name = element.getAttribute("name");
525
+ return `${element.tagName.toLowerCase()}${name ? `[name=${JSON.stringify(name)}]` : ""}`;
526
+ }
527
+ const focusables = [...scope.querySelectorAll(selector)].filter(visible);
528
+ const positiveTabindex = focusables
529
+ .filter((element) => Number.parseInt(element.getAttribute("tabindex") ?? "0", 10) > 0)
530
+ .map(selectorFor)
531
+ .slice(0, 12);
532
+ return { focusableCount: focusables.length, positiveTabindex, modalOpen: Boolean(modal) };
533
+ });
534
+ if (overview.focusableCount === 0)
535
+ return;
536
+ for (const selector of overview.positiveTabindex) {
537
+ addFinding(findings, "ZTDE-RUNTIME-014", "warning", "Positive tabindex overrides the document's natural keyboard order.", [
538
+ "Use DOM order and tabindex=0 so keyboard navigation follows the same logical sequence as the interface.",
539
+ ], { viewport: viewport.name }, selector);
540
+ }
541
+ await page.evaluate(() => document.activeElement?.blur());
542
+ const focusedSelectors = new Set();
543
+ const focusedIndexes = new Set();
544
+ let visibleFocusCount = 0;
545
+ let previousIndex;
546
+ let trapDetected = false;
547
+ const attempts = Math.min(overview.focusableCount + 2, 50);
548
+ for (let index = 0; index < attempts; index += 1) {
549
+ await page.keyboard.press("Tab");
550
+ const focused = await page.evaluate(() => {
551
+ const element = document.activeElement;
552
+ if (!element || element === document.body || element === document.documentElement)
553
+ return null;
554
+ if (element.matches("nextjs-portal") || element.closest("[data-ztothez-design-runtime-ignore]"))
555
+ return null;
556
+ const modal = document.querySelector('[aria-modal="true"], dialog[open]');
557
+ const scope = modal ?? document;
558
+ const focusableSelector = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
559
+ const visibleFocusables = [...scope.querySelectorAll(focusableSelector)].filter((candidate) => {
560
+ const rect = candidate.getBoundingClientRect();
561
+ const style = getComputedStyle(candidate);
562
+ return (rect.width > 1 &&
563
+ rect.height > 1 &&
564
+ style.display !== "none" &&
565
+ style.visibility !== "hidden" &&
566
+ !candidate.closest('[aria-hidden="true"], [inert], nextjs-portal, [data-ztothez-design-runtime-ignore]'));
567
+ });
568
+ const selector = element.id
569
+ ? `#${CSS.escape(element.id)}`
570
+ : `${element.tagName.toLowerCase()}${element.getAttribute("name") ? `[name=${JSON.stringify(element.getAttribute("name"))}]` : ""}`;
571
+ const focusedStyle = getComputedStyle(element);
572
+ const focusedValues = {
573
+ outline: focusedStyle.outline,
574
+ boxShadow: focusedStyle.boxShadow,
575
+ borderColor: focusedStyle.borderColor,
576
+ backgroundColor: focusedStyle.backgroundColor,
577
+ textDecoration: focusedStyle.textDecoration,
578
+ };
579
+ element.blur();
580
+ const blurredStyle = getComputedStyle(element);
581
+ const blurredValues = {
582
+ outline: blurredStyle.outline,
583
+ boxShadow: blurredStyle.boxShadow,
584
+ borderColor: blurredStyle.borderColor,
585
+ backgroundColor: blurredStyle.backgroundColor,
586
+ textDecoration: blurredStyle.textDecoration,
587
+ };
588
+ element.focus();
589
+ const changed = Object.keys(focusedValues).some((key) => focusedValues[key] !==
590
+ blurredValues[key]);
591
+ const browserOutline = focusedStyle.outlineStyle !== "none" && focusedStyle.outlineWidth !== "0px";
592
+ const rect = element.getBoundingClientRect();
593
+ const centerX = Math.max(0, Math.min(window.innerWidth - 1, rect.left + rect.width / 2));
594
+ const centerY = Math.max(0, Math.min(window.innerHeight - 1, rect.top + rect.height / 2));
595
+ let occluder;
596
+ let occlusionReason;
597
+ if (!element.closest("[data-ztothez-design-allow-focus-occlusion]")) {
598
+ if (rect.left + rect.width / 2 < 0 ||
599
+ rect.left + rect.width / 2 > window.innerWidth ||
600
+ rect.top + rect.height / 2 < 0 ||
601
+ rect.top + rect.height / 2 > window.innerHeight) {
602
+ occlusionReason = "Focused control center is outside the visible viewport.";
603
+ }
604
+ else {
605
+ const top = document.elementFromPoint(centerX, centerY);
606
+ if (top && top !== element && !element.contains(top) && !top.contains(element)) {
607
+ let layer = top;
608
+ while (layer && layer !== document.body) {
609
+ const position = getComputedStyle(layer).position;
610
+ if (position === "fixed" || position === "sticky") {
611
+ occluder = layer.id
612
+ ? `#${CSS.escape(layer.id)}`
613
+ : layer.tagName.toLowerCase();
614
+ occlusionReason = `Focused control center is covered by ${position} element ${occluder}.`;
615
+ break;
616
+ }
617
+ layer = layer.parentElement;
618
+ }
619
+ }
620
+ }
621
+ }
622
+ return {
623
+ selector,
624
+ domIndex: visibleFocusables.indexOf(element),
625
+ visibleFocus: changed || browserOutline,
626
+ occluder,
627
+ occlusionReason,
628
+ };
629
+ });
630
+ if (!focused)
631
+ continue;
632
+ focusedSelectors.add(focused.selector);
633
+ if (focused.visibleFocus)
634
+ visibleFocusCount += 1;
635
+ if (focused.occlusionReason) {
636
+ addFinding(findings, "ZTDE-RUNTIME-013", "error", "Keyboard focus is hidden or centered beneath fixed or sticky content.", [
637
+ focused.occlusionReason,
638
+ "Reserve scroll padding, adjust sticky geometry, or move focus so the focused control and indicator remain visible.",
639
+ ], { viewport: viewport.name }, focused.selector);
640
+ }
641
+ if (focused.domIndex >= 0) {
642
+ const repeatedBeforeCompletion = focusedIndexes.has(focused.domIndex) && focusedIndexes.size < overview.focusableCount;
643
+ const focusDidNotAdvance = previousIndex === focused.domIndex && overview.focusableCount > 1;
644
+ if (!overview.modalOpen && (repeatedBeforeCompletion || focusDidNotAdvance)) {
645
+ addFinding(findings, "ZTDE-RUNTIME-014", "error", "Keyboard traversal repeated focus before reaching all visible controls.", [
646
+ `Reached ${focusedIndexes.size} of ${overview.focusableCount} visible controls before focus repeated at ${focused.selector}.`,
647
+ "Remove Tab interception or focus redirection that prevents users from reaching the remaining controls.",
648
+ ], { viewport: viewport.name }, focused.selector);
649
+ trapDetected = true;
650
+ break;
651
+ }
652
+ focusedIndexes.add(focused.domIndex);
653
+ previousIndex = focused.domIndex;
654
+ }
655
+ }
656
+ if (focusedSelectors.size === 0) {
657
+ addFinding(findings, "ZTDE-RUNTIME-006", "error", "Interactive controls exist but Tab did not reach a control.", [`Focusable DOM query found ${overview.focusableCount} visible controls.`], { viewport: viewport.name });
658
+ }
659
+ else if (visibleFocusCount === 0 && !trapDetected) {
660
+ addFinding(findings, "ZTDE-RUNTIME-006", "warning", "Keyboard traversal reached controls without a detectable visual focus change.", [`Sampled ${focusedSelectors.size} unique controls.`], { viewport: viewport.name }, [...focusedSelectors][0]);
661
+ }
662
+ }
663
+ async function checkMedia(page, viewport, findings) {
664
+ const media = await page.evaluate(() => {
665
+ function selectorFor(element) {
666
+ if (element.id)
667
+ return `#${CSS.escape(element.id)}`;
668
+ return element.tagName.toLowerCase();
669
+ }
670
+ function visible(element) {
671
+ const rect = element.getBoundingClientRect();
672
+ const style = getComputedStyle(element);
673
+ return rect.width > 2 && rect.height > 2 && style.display !== "none" && style.visibility !== "hidden";
674
+ }
675
+ const brokenImages = [...document.images]
676
+ .filter((image) => visible(image) && image.complete && image.naturalWidth === 0)
677
+ .map(selectorFor);
678
+ const unavailableVideos = [...document.querySelectorAll("video")]
679
+ .filter((video) => visible(video) && video.readyState === HTMLMediaElement.HAVE_NOTHING && !video.poster)
680
+ .map(selectorFor);
681
+ const blankCanvases = [...document.querySelectorAll("canvas")]
682
+ .filter((canvas) => visible(canvas) && !canvas.hasAttribute("data-ztothez-design-allow-blank"))
683
+ .flatMap((canvas) => {
684
+ try {
685
+ const context = canvas.getContext("2d", { willReadFrequently: true });
686
+ if (!context || canvas.width === 0 || canvas.height === 0)
687
+ return [];
688
+ const width = Math.min(canvas.width, 32);
689
+ const height = Math.min(canvas.height, 32);
690
+ const sample = document.createElement("canvas");
691
+ sample.width = width;
692
+ sample.height = height;
693
+ const sampleContext = sample.getContext("2d", { willReadFrequently: true });
694
+ if (!sampleContext)
695
+ return [];
696
+ sampleContext.drawImage(canvas, 0, 0, width, height);
697
+ const pixels = sampleContext.getImageData(0, 0, width, height).data;
698
+ const colors = new Set();
699
+ for (let index = 0; index < pixels.length; index += 4) {
700
+ colors.add(`${pixels[index]},${pixels[index + 1]},${pixels[index + 2]},${pixels[index + 3]}`);
701
+ if (colors.size > 1)
702
+ return [];
703
+ }
704
+ return [selectorFor(canvas)];
705
+ }
706
+ catch {
707
+ return [];
708
+ }
709
+ });
710
+ const bodyText = document.body.innerText.trim();
711
+ const visibleMediaCount = [...document.querySelectorAll("img, canvas, video, svg")].filter(visible).length;
712
+ return { brokenImages, unavailableVideos, blankCanvases, pageAppearsBlank: bodyText.length === 0 && visibleMediaCount === 0 };
713
+ });
714
+ for (const selector of media.brokenImages) {
715
+ addFinding(findings, "ZTDE-RUNTIME-007", "error", "Visible image failed to render.", ["naturalWidth is zero after loading."], { viewport: viewport.name }, selector);
716
+ }
717
+ for (const selector of media.unavailableVideos) {
718
+ addFinding(findings, "ZTDE-RUNTIME-007", "warning", "Visible video has no loaded media or poster.", ["Video readyState remained HAVE_NOTHING."], { viewport: viewport.name }, selector);
719
+ }
720
+ for (const selector of media.blankCanvases) {
721
+ addFinding(findings, "ZTDE-RUNTIME-007", "warning", "Visible 2D canvas contains only one sampled pixel value.", ["A 32 by 32 sample did not contain visual variation. Add data-ztothez-design-allow-blank only when blank is an intentional state."], { viewport: viewport.name }, selector);
722
+ }
723
+ if (media.pageAppearsBlank) {
724
+ addFinding(findings, "ZTDE-RUNTIME-007", "error", "Rendered page appears blank.", ["No visible text or media was detected."], { viewport: viewport.name });
725
+ }
726
+ }
727
+ async function captureScreenshot(page, outputDirectory, name, viewport, screenshots, dynamicSelectors) {
728
+ const screenshotPath = join(outputDirectory, `${safeName(name)}.png`);
729
+ await page.screenshot({
730
+ path: screenshotPath,
731
+ fullPage: true,
732
+ animations: "disabled",
733
+ mask: dynamicSelectors.map((selector) => page.locator(selector)),
734
+ maskColor: "#7f7f7f",
735
+ });
736
+ const sha256 = createHash("sha256").update(await readFile(screenshotPath)).digest("hex");
737
+ screenshots.push({
738
+ name,
739
+ path: screenshotPath,
740
+ width: viewport.width,
741
+ height: viewport.height,
742
+ fullPage: true,
743
+ sha256,
744
+ dynamicSelectors,
745
+ });
746
+ return screenshotPath;
747
+ }
748
+ async function checkRenderedPage(page, viewport, findings, interfaceCoverage, journey) {
749
+ const findingStart = findings.length;
750
+ await checkOverflow(page, viewport, findings);
751
+ await checkLayoutCollisions(page, viewport, findings);
752
+ await checkTextContrast(page, viewport, findings);
753
+ await checkTouchTargets(page, viewport, findings);
754
+ await checkAccessibleNames(page, viewport, findings);
755
+ await checkKeyboard(page, viewport, findings);
756
+ await checkReflowAndTextResize(page, viewport, findings);
757
+ await checkReducedMotion(page, viewport, findings);
758
+ await checkMedia(page, viewport, findings);
759
+ await checkInterfaceTrust(page, viewport, findings, interfaceCoverage);
760
+ await checkChartContracts(page, viewport, findings);
761
+ await checkVisualComposition(page, viewport, findings);
762
+ await checkRenderedVisualContrast(page, viewport, findings);
763
+ await checkRenderedAssets(page, viewport, findings);
764
+ if (journey) {
765
+ for (let index = findingStart; index < findings.length; index += 1) {
766
+ findings[index].journey ??= journey;
767
+ }
768
+ }
769
+ }
770
+ async function evaluateScreenshotRegression(screenshots, baselinePath, updateBaseline, findings) {
771
+ if (!baselinePath) {
772
+ return { status: "not-configured", compared: 0, mismatches: [] };
773
+ }
774
+ const resolvedPath = resolve(baselinePath);
775
+ const records = screenshots.map(({ name, width, height, sha256, dynamicSelectors }) => ({
776
+ name,
777
+ width,
778
+ height,
779
+ sha256,
780
+ dynamicSelectors,
781
+ }));
782
+ if (updateBaseline) {
783
+ await mkdir(dirname(resolvedPath), { recursive: true });
784
+ await writeFile(resolvedPath, `${JSON.stringify({ version: "1.0", screenshots: records }, null, 2)}\n`, "utf8");
785
+ return {
786
+ status: "created",
787
+ baselinePath: resolvedPath,
788
+ compared: records.length,
789
+ mismatches: [],
790
+ };
791
+ }
792
+ const baseline = runtimeScreenshotBaselineSchema.parse(JSON.parse(await readFile(resolvedPath, "utf8")));
793
+ const expected = new Map(baseline.screenshots.map((entry) => [entry.name, entry]));
794
+ const actual = new Map(records.map((entry) => [entry.name, entry]));
795
+ const mismatches = [];
796
+ let compared = 0;
797
+ for (const [name, screenshot] of actual) {
798
+ const reference = expected.get(name);
799
+ if (!reference) {
800
+ mismatches.push(`No baseline record for ${name}.`);
801
+ continue;
802
+ }
803
+ compared += 1;
804
+ if (reference.width !== screenshot.width ||
805
+ reference.height !== screenshot.height ||
806
+ reference.sha256 !== screenshot.sha256 ||
807
+ JSON.stringify(reference.dynamicSelectors) !== JSON.stringify(screenshot.dynamicSelectors)) {
808
+ mismatches.push(`Screenshot ${name} differs from its baseline hash, dimensions, or dynamic-region policy.`);
809
+ }
810
+ }
811
+ for (const name of expected.keys()) {
812
+ if (!actual.has(name))
813
+ mismatches.push(`Baseline screenshot ${name} was not captured.`);
814
+ }
815
+ if (mismatches.length > 0) {
816
+ findings.push({
817
+ checkId: "ZTDE-RUNTIME-019",
818
+ severity: "error",
819
+ message: "Rendered screenshot regression baseline does not match.",
820
+ evidence: [
821
+ ...mismatches.slice(0, 20),
822
+ "A hash mismatch detects change only. It does not establish whether the design is good or whether an intentional change is acceptable.",
823
+ ],
824
+ });
825
+ }
826
+ return {
827
+ status: mismatches.length === 0 ? "matched" : "mismatched",
828
+ baselinePath: resolvedPath,
829
+ compared,
830
+ mismatches,
831
+ };
832
+ }
833
+ async function runJourney(page, baseUrl, journey, timeoutMs, settleMs, outputDirectory, findings) {
834
+ const result = {
835
+ name: journey.name,
836
+ passed: true,
837
+ stepsCompleted: 0,
838
+ totalSteps: journey.steps.length,
839
+ evidence: [],
840
+ checkpoints: [],
841
+ };
842
+ const observations = new Map();
843
+ for (const [index, step] of journey.steps.entries()) {
844
+ try {
845
+ switch (step.action) {
846
+ case "navigate": {
847
+ const destination = new URL(step.value, baseUrl);
848
+ if (destination.origin !== baseUrl.origin) {
849
+ throw new Error("Journey navigation must remain on the target origin");
850
+ }
851
+ await page.goto(destination.toString(), { waitUntil: "domcontentloaded", timeout: timeoutMs });
852
+ assertSameOrigin(page, baseUrl.origin);
853
+ await page.waitForTimeout(settleMs);
854
+ break;
855
+ }
856
+ case "setNetwork":
857
+ await page.context().setOffline(step.state === "offline");
858
+ break;
859
+ case "click":
860
+ await page.locator(step.selector).click({ timeout: timeoutMs });
861
+ break;
862
+ case "fill":
863
+ await page.locator(step.selector).fill(step.value, { timeout: timeoutMs });
864
+ break;
865
+ case "press":
866
+ if (step.selector) {
867
+ await page.locator(step.selector).press(step.value, { timeout: timeoutMs });
868
+ }
869
+ else {
870
+ await page.keyboard.press(step.value);
871
+ }
872
+ break;
873
+ case "waitFor":
874
+ await page.locator(step.selector).waitFor({ state: "visible", timeout: timeoutMs });
875
+ break;
876
+ case "expectVisible":
877
+ await page.locator(step.selector).waitFor({ state: "visible", timeout: timeoutMs });
878
+ observations.set(index + 1, `${step.selector} is visible`);
879
+ break;
880
+ case "expectValue": {
881
+ const locator = page.locator(step.selector);
882
+ await locator.waitFor({ state: "visible", timeout: timeoutMs });
883
+ const value = await locator.inputValue({ timeout: timeoutMs });
884
+ if (value !== step.value) {
885
+ throw new Error(`Expected value ${JSON.stringify(step.value)}, received ${JSON.stringify(value)}`);
886
+ }
887
+ observations.set(index + 1, `${step.selector} value matched ${JSON.stringify(value)}`);
888
+ break;
889
+ }
890
+ case "expectAttribute": {
891
+ const locator = page.locator(step.selector);
892
+ await locator.waitFor({ state: "visible", timeout: timeoutMs });
893
+ const value = await locator.getAttribute(step.name, { timeout: timeoutMs });
894
+ if (value !== step.value) {
895
+ throw new Error(`Expected attribute ${step.name}=${JSON.stringify(step.value)}, received ${JSON.stringify(value)}`);
896
+ }
897
+ result.evidence?.push({
898
+ kind: "attribute",
899
+ step: index + 1,
900
+ description: `${step.selector} has ${step.name}=${JSON.stringify(value)}`,
901
+ });
902
+ observations.set(index + 1, `${step.selector} has ${step.name}=${JSON.stringify(value)}`);
903
+ break;
904
+ }
905
+ case "expectJson": {
906
+ const locator = page.locator(step.selector);
907
+ await locator.waitFor({ state: "visible", timeout: timeoutMs });
908
+ const text = (await locator.textContent()) ?? "";
909
+ const actual = jsonValueAtPath(JSON.parse(text), step.path);
910
+ if (JSON.stringify(actual) !== JSON.stringify(step.value)) {
911
+ throw new Error(`Expected JSON path ${step.path}=${JSON.stringify(step.value)}, received ${JSON.stringify(actual)}`);
912
+ }
913
+ result.evidence?.push({
914
+ kind: "json",
915
+ step: index + 1,
916
+ description: `${step.selector} JSON path ${step.path} matched ${JSON.stringify(step.value)}`,
917
+ });
918
+ observations.set(index + 1, `${step.selector} JSON path ${step.path} matched ${JSON.stringify(step.value)}`);
919
+ break;
920
+ }
921
+ case "expectDownload": {
922
+ const [download] = await Promise.all([
923
+ page.waitForEvent("download", { timeout: timeoutMs }),
924
+ page.locator(step.selector).click({ timeout: timeoutMs }),
925
+ ]);
926
+ const suggestedFilename = basename(download.suggestedFilename());
927
+ if (step.filenameIncludes && !suggestedFilename.includes(step.filenameIncludes)) {
928
+ throw new Error(`Expected downloaded filename to include ${JSON.stringify(step.filenameIncludes)}, received ${JSON.stringify(suggestedFilename)}`);
929
+ }
930
+ const downloadPath = join(outputDirectory, `${safeName(`journey-${journey.name}-step-${index + 1}`)}-${suggestedFilename}`);
931
+ let evidenceMethod = "browser download";
932
+ try {
933
+ await download.saveAs(downloadPath);
934
+ }
935
+ catch (saveError) {
936
+ const downloadUrl = download.url();
937
+ try {
938
+ const capturedBody = await capturedBlobBody(page, downloadUrl);
939
+ if (capturedBody) {
940
+ await writeFile(downloadPath, capturedBody);
941
+ evidenceMethod = "captured Blob";
942
+ }
943
+ else {
944
+ await writeFile(downloadPath, await downloadFallbackBody(page, downloadUrl, timeoutMs));
945
+ evidenceMethod = "URL fallback";
946
+ }
947
+ }
948
+ catch (fallbackError) {
949
+ const browserFailure = await download.failure().catch(() => null);
950
+ throw new Error([
951
+ `Download artifact could not be retained as verifier evidence.`,
952
+ `Browser save: ${saveError instanceof Error ? saveError.message : String(saveError)}.`,
953
+ browserFailure ? `Browser failure: ${browserFailure}.` : undefined,
954
+ `Fallback: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}.`,
955
+ ]
956
+ .filter(Boolean)
957
+ .join(" "), { cause: saveError });
958
+ }
959
+ }
960
+ result.evidence?.push({
961
+ kind: "download",
962
+ step: index + 1,
963
+ description: `Downloaded ${suggestedFilename} via ${evidenceMethod}`,
964
+ path: downloadPath,
965
+ });
966
+ observations.set(index + 1, `Downloaded ${suggestedFilename} via ${evidenceMethod}`);
967
+ break;
968
+ }
969
+ case "expectResponse": {
970
+ const [response] = await Promise.all([
971
+ page.waitForResponse((candidate) => candidate.url().includes(step.urlIncludes) &&
972
+ candidate.status() === step.status &&
973
+ (!step.method || candidate.request().method() === step.method), { timeout: timeoutMs }),
974
+ page.locator(step.selector).click({ timeout: timeoutMs }),
975
+ ]);
976
+ result.evidence?.push({
977
+ kind: "response",
978
+ step: index + 1,
979
+ description: `${response.request().method()} ${response.url()} returned ${response.status()}`,
980
+ });
981
+ observations.set(index + 1, `${response.request().method()} ${response.url()} returned ${response.status()}`);
982
+ break;
983
+ }
984
+ case "expectText": {
985
+ const locator = page.locator(step.selector).filter({ hasText: step.value });
986
+ await locator.waitFor({ state: "visible", timeout: timeoutMs });
987
+ const text = (await locator.textContent()) ?? "";
988
+ if (!text.includes(step.value))
989
+ throw new Error(`Expected text ${JSON.stringify(step.value)}`);
990
+ observations.set(index + 1, `${step.selector} contains ${JSON.stringify(step.value)}`);
991
+ break;
992
+ }
993
+ case "checkpoint": {
994
+ const evidenceStep = index;
995
+ const description = observations.get(evidenceStep);
996
+ if (!description) {
997
+ throw new Error("Checkpoint must immediately follow a successful observable expectation");
998
+ }
999
+ result.checkpoints?.push({
1000
+ checkpoint: step.checkpoint,
1001
+ step: index + 1,
1002
+ evidenceStep,
1003
+ description,
1004
+ });
1005
+ result.evidence?.push({
1006
+ kind: "checkpoint",
1007
+ step: index + 1,
1008
+ description: `${step.checkpoint} checkpoint: ${description}`,
1009
+ });
1010
+ break;
1011
+ }
1012
+ }
1013
+ result.stepsCompleted = index + 1;
1014
+ }
1015
+ catch (error) {
1016
+ result.passed = false;
1017
+ addFinding(findings, "ZTDE-RUNTIME-008", "error", `Journey failed at step ${index + 1} (${step.action}).`, [error instanceof Error ? error.message : String(error)], { journey: journey.name }, "selector" in step ? step.selector : undefined);
1018
+ break;
1019
+ }
1020
+ }
1021
+ return result;
1022
+ }
1023
+ function deduplicateFindings(findings) {
1024
+ const unique = new Map();
1025
+ for (const finding of findings) {
1026
+ const key = [finding.checkId, finding.severity, finding.viewport, finding.journey, finding.selector, finding.message].join("|");
1027
+ if (!unique.has(key))
1028
+ unique.set(key, finding);
1029
+ }
1030
+ return [...unique.values()];
1031
+ }
1032
+ export async function verifyUiRuntime(options) {
1033
+ const targetUrl = validateRuntimeUrl(options.url);
1034
+ const outputDirectory = resolve(options.outputDirectory);
1035
+ const viewports = [...(options.viewports ?? DEFAULT_RUNTIME_VIEWPORTS)];
1036
+ const colorSchemes = [...(options.colorSchemes ?? DEFAULT_RUNTIME_COLOR_SCHEMES)];
1037
+ const journeys = runtimeJourneySchema.array().max(MAX_RUNTIME_JOURNEYS).parse(options.journeys ?? []);
1038
+ const timeoutMs = options.navigationTimeoutMs ?? DEFAULT_NAVIGATION_TIMEOUT_MS;
1039
+ const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
1040
+ const expectedNetworkTrackers = createExpectedNetworkTrackers(options.expectedNetwork ?? []);
1041
+ const dynamicSelectors = [...(options.dynamicSelectors ?? [])];
1042
+ if (dynamicSelectors.length > 20 || dynamicSelectors.some((selector) => selector.length === 0 || selector.length > 1_024)) {
1043
+ throw new Error("Provide no more than 20 non-empty dynamic selectors of at most 1024 characters");
1044
+ }
1045
+ if (new Set(dynamicSelectors).size !== dynamicSelectors.length) {
1046
+ throw new Error("Dynamic screenshot selectors must be unique");
1047
+ }
1048
+ if (options.updateScreenshotBaseline && !options.screenshotBaselinePath) {
1049
+ throw new Error("updateScreenshotBaseline requires screenshotBaselinePath");
1050
+ }
1051
+ validateViewports(viewports);
1052
+ validateColorSchemes(colorSchemes);
1053
+ for (const journey of journeys) {
1054
+ if (journey.steps.length > MAX_JOURNEY_STEPS) {
1055
+ throw new Error(`Journey ${journey.name} exceeds the ${MAX_JOURNEY_STEPS}-step limit`);
1056
+ }
1057
+ }
1058
+ await mkdir(outputDirectory, { recursive: true });
1059
+ const chromiumCdpUrl = resolveChromiumCdpUrl(options.chromiumCdpUrl);
1060
+ const executablePath = chromiumCdpUrl ? undefined : resolveChromiumPath(options.chromiumPath);
1061
+ const browser = chromiumCdpUrl
1062
+ ? await chromium.connectOverCDP(chromiumCdpUrl)
1063
+ : await chromium.launch({
1064
+ headless: true,
1065
+ ...(executablePath ? { executablePath } : {}),
1066
+ args: ["--disable-dev-shm-usage", "--no-sandbox"],
1067
+ });
1068
+ const findings = [];
1069
+ const screenshots = [];
1070
+ const journeyResults = [];
1071
+ const interfaceCoverage = createInterfaceCoverage();
1072
+ try {
1073
+ for (const viewport of viewports) {
1074
+ const context = await browser.newContext({ viewport: { width: viewport.width, height: viewport.height }, deviceScaleFactor: 1, reducedMotion: "reduce", colorScheme: colorSchemes[0], acceptDownloads: true });
1075
+ const page = await context.newPage();
1076
+ attachDiagnostics(page, findings, { viewport: viewport.name }, expectedNetworkTrackers);
1077
+ try {
1078
+ const response = await page.goto(targetUrl.toString(), { waitUntil: "domcontentloaded", timeout: timeoutMs });
1079
+ assertSameOrigin(page, targetUrl.origin);
1080
+ if (!response) {
1081
+ addFinding(findings, "ZTDE-RUNTIME-001", "warning", "Navigation completed without an HTTP response object.", [targetUrl.toString()], { viewport: viewport.name });
1082
+ }
1083
+ await page.waitForTimeout(settleMs);
1084
+ for (const colorScheme of colorSchemes) {
1085
+ await page.emulateMedia({ colorScheme, reducedMotion: "reduce" });
1086
+ await page.waitForTimeout(settleMs);
1087
+ const themedViewport = colorSchemes.length === 1 && colorScheme === "light"
1088
+ ? viewport
1089
+ : { ...viewport, name: `${viewport.name}-${colorScheme}` };
1090
+ await checkRenderedPage(page, themedViewport, findings, interfaceCoverage);
1091
+ await captureScreenshot(page, outputDirectory, themedViewport.name, themedViewport, screenshots, dynamicSelectors);
1092
+ }
1093
+ }
1094
+ catch (error) {
1095
+ addFinding(findings, "ZTDE-RUNTIME-001", "error", "Viewport verification could not complete.", [error instanceof Error ? error.message : String(error)], { viewport: viewport.name });
1096
+ }
1097
+ finally {
1098
+ await context.close();
1099
+ }
1100
+ }
1101
+ if (journeys.length > 0) {
1102
+ const viewport = viewports[viewports.length - 1];
1103
+ for (const journey of journeys) {
1104
+ const context = await browser.newContext({ viewport: { width: viewport.width, height: viewport.height }, deviceScaleFactor: 1, reducedMotion: "reduce", colorScheme: colorSchemes[0], acceptDownloads: true });
1105
+ const page = await context.newPage();
1106
+ await installBlobDownloadCapture(page);
1107
+ attachDiagnostics(page, findings, { journey: journey.name }, expectedNetworkTrackers);
1108
+ try {
1109
+ await page.goto(targetUrl.toString(), { waitUntil: "domcontentloaded", timeout: timeoutMs });
1110
+ assertSameOrigin(page, targetUrl.origin);
1111
+ await page.waitForTimeout(settleMs);
1112
+ const result = await runJourney(page, targetUrl, journey, timeoutMs, settleMs, outputDirectory, findings);
1113
+ for (const journeyViewport of viewports) {
1114
+ for (const colorScheme of colorSchemes) {
1115
+ const themedViewport = colorSchemes.length === 1 && colorScheme === "light"
1116
+ ? journeyViewport
1117
+ : { ...journeyViewport, name: `${journeyViewport.name}-${colorScheme}` };
1118
+ try {
1119
+ await page.setViewportSize({
1120
+ width: journeyViewport.width,
1121
+ height: journeyViewport.height,
1122
+ });
1123
+ await page.emulateMedia({ colorScheme, reducedMotion: "reduce" });
1124
+ await page.waitForTimeout(settleMs);
1125
+ await checkRenderedPage(page, themedViewport, findings, interfaceCoverage, journey.name);
1126
+ const screenshot = await captureScreenshot(page, outputDirectory, `journey-${journey.name}-${themedViewport.name}`, themedViewport, screenshots, dynamicSelectors);
1127
+ result.screenshot ??= screenshot;
1128
+ }
1129
+ catch (error) {
1130
+ addFinding(findings, "ZTDE-RUNTIME-001", "warning", `Could not verify journey ${journey.name} at ${themedViewport.name}.`, [error instanceof Error ? error.message : String(error)], { viewport: themedViewport.name, journey: journey.name });
1131
+ }
1132
+ }
1133
+ }
1134
+ journeyResults.push(result);
1135
+ }
1136
+ catch (error) {
1137
+ addFinding(findings, "ZTDE-RUNTIME-001", "error", `Could not initialize journey ${journey.name}.`, [error instanceof Error ? error.message : String(error)], { journey: journey.name });
1138
+ journeyResults.push({ name: journey.name, passed: false, stepsCompleted: 0, totalSteps: journey.steps.length });
1139
+ }
1140
+ finally {
1141
+ await context.close();
1142
+ }
1143
+ }
1144
+ }
1145
+ }
1146
+ finally {
1147
+ await browser.close();
1148
+ }
1149
+ const expectedNetwork = expectedNetworkTrackers.map((policy) => ({
1150
+ ...policy,
1151
+ satisfied: policy.occurrences >= policy.minOccurrences &&
1152
+ (policy.maxOccurrences === undefined || policy.occurrences <= policy.maxOccurrences),
1153
+ }));
1154
+ for (const observation of expectedNetwork.filter((entry) => !entry.satisfied)) {
1155
+ addFinding(findings, "ZTDE-RUNTIME-009", "error", `Expected network policy ${observation.id} observed ${observation.occurrences} times; expected at least ${observation.minOccurrences}${observation.maxOccurrences === undefined ? "" : ` and at most ${observation.maxOccurrences}`}.`, observation.evidence.length > 0
1156
+ ? observation.evidence
1157
+ : [
1158
+ `No matching ${observation.method} response containing ${JSON.stringify(observation.urlIncludes)} was observed.`,
1159
+ ], {});
1160
+ }
1161
+ addInterfaceCoverageFindings(findings, interfaceCoverage);
1162
+ const screenshotRegression = await evaluateScreenshotRegression(screenshots, options.screenshotBaselinePath, options.updateScreenshotBaseline ?? false, findings);
1163
+ const uniqueFindings = deduplicateFindings(findings);
1164
+ const summary = {
1165
+ errors: uniqueFindings.filter((finding) => finding.severity === "error").length,
1166
+ warnings: uniqueFindings.filter((finding) => finding.severity === "warning").length,
1167
+ info: uniqueFindings.filter((finding) => finding.severity === "info").length,
1168
+ };
1169
+ const report = {
1170
+ version: RUNTIME_REPORT_VERSION,
1171
+ url: targetUrl.toString(),
1172
+ generatedAt: new Date().toISOString(),
1173
+ browser: `Chromium ${browser.version()}`,
1174
+ outputDirectory,
1175
+ viewports,
1176
+ colorSchemes,
1177
+ screenshots,
1178
+ screenshotRegression,
1179
+ journeys: journeyResults,
1180
+ expectedNetwork,
1181
+ findings: uniqueFindings,
1182
+ summary,
1183
+ passed: summary.errors === 0,
1184
+ evidenceBoundary: {
1185
+ verifierLimitations: [
1186
+ "Solid-color contrast sampling cannot establish contrast over gradients, images, video, canvas, or transparency without separate evidence.",
1187
+ "Composition and rendered-asset checks apply only to explicitly instrumented roots. DOM metadata cannot establish source truth, legal clearance, or whether an unmarked visual should have been declared.",
1188
+ "Static DOM and browser checks cannot establish metric correctness, backend availability beyond observed requests, legal clearance, or representative-user comprehension.",
1189
+ "Screenshot hashes detect rendered change only; they do not prove visual quality or improvement.",
1190
+ ],
1191
+ humanReviewRequired: [
1192
+ "An attributable reviewer must assess hierarchy, balance, scanability, density, domain fit, and intentional baseline changes.",
1193
+ "Representative-user evidence is required for claims about task comprehension, confidence, efficiency, or usability.",
1194
+ ],
1195
+ },
1196
+ };
1197
+ await writeFile(join(outputDirectory, "runtime-report.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
1198
+ await writeFile(join(outputDirectory, "runtime-report.md"), `${formatRuntimeReport(report)}\n`, "utf8");
1199
+ return report;
1200
+ }
1201
+ //# sourceMappingURL=verifier.js.map