@pixel-point/toolcraft 0.0.16 → 0.0.18

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 (313) hide show
  1. package/package.json +1 -1
  2. package/src/generate-performance-iteration-execution.test.mjs +2 -2
  3. package/src/generate-test-demand-only-lifecycle.mjs +14 -3
  4. package/src/generate-test-dependency-sandbox.mjs +42 -5
  5. package/src/generate-test-doc-assertions.mjs +109 -69
  6. package/src/generate-test-doc-assertions.test.mjs +65 -0
  7. package/src/generate-test-doc-policy-assertions.mjs +92 -0
  8. package/src/generate-test-file-assertions.mjs +36 -118
  9. package/src/generate-test-output-assertions.mjs +95 -0
  10. package/src/generate-test-package-assertions.mjs +6 -0
  11. package/src/generate-test-performance-boundary-assertions.mjs +45 -0
  12. package/src/generate-test-product-fixture.mjs +1 -0
  13. package/src/generate-test-product-fixture.test.mjs +18 -0
  14. package/src/generate-test-product-iteration.mjs +1 -0
  15. package/src/generate-test-product-iteration.test.mjs +18 -0
  16. package/src/generate-test-runtime-boundary-assertions.mjs +158 -0
  17. package/src/generate-test-standalone-assertions.mjs +10 -0
  18. package/src/generate-test-standalone-delivery-stages.mjs +14 -6
  19. package/src/generate.test.mjs +14 -0
  20. package/templates/runtime/contracts/component-contracts.choices.test.ts +59 -12
  21. package/templates/runtime/contracts/component-contracts.choices.ts +58 -12
  22. package/templates/runtime/contracts/component-contracts.inputs.test.ts +6 -6
  23. package/templates/runtime/contracts/component-contracts.inputs.ts +6 -6
  24. package/templates/runtime/contracts/component-contracts.media-custom.test.ts +8 -2
  25. package/templates/runtime/contracts/component-contracts.media-custom.ts +8 -2
  26. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +21 -6
  27. package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +7 -1
  28. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +4 -1
  29. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +4 -1
  30. package/templates/runtime/contracts/component-contracts.runtime.ts +15 -10
  31. package/templates/runtime/contracts/component-contracts.test.ts +2 -0
  32. package/templates/runtime/contracts/decision-contracts.test.ts +43 -4
  33. package/templates/runtime/contracts/decision-contracts.ts +10 -10
  34. package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
  35. package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
  36. package/templates/runtime/export/artifact-download.test.ts +71 -0
  37. package/templates/runtime/export/artifact-download.ts +80 -0
  38. package/templates/runtime/export/artifact-export-request.ts +23 -0
  39. package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
  40. package/templates/runtime/export/artifact-export-settings.ts +96 -0
  41. package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
  42. package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
  43. package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
  44. package/templates/runtime/export/artifact-frame-state.ts +44 -0
  45. package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
  46. package/templates/runtime/export/artifact-scene-frame.ts +118 -0
  47. package/templates/runtime/export/export-background.ts +46 -0
  48. package/templates/runtime/export/export-error.ts +43 -0
  49. package/templates/runtime/export/export.test.ts +49 -226
  50. package/templates/runtime/export/image-artifact-export.test.ts +111 -0
  51. package/templates/runtime/export/image-artifact-export.ts +145 -0
  52. package/templates/runtime/export/index.ts +44 -3
  53. package/templates/runtime/export/product-export-renderer.test.ts +77 -0
  54. package/templates/runtime/export/product-export-renderer.ts +63 -0
  55. package/templates/runtime/export/video-artifact-export.test.ts +166 -0
  56. package/templates/runtime/export/video-artifact-export.ts +164 -0
  57. package/templates/runtime/export/video-encoding-backend.ts +124 -0
  58. package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
  59. package/templates/runtime/export/video-encoding-policy.ts +86 -0
  60. package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
  61. package/templates/runtime/export/video-frame-schedule.ts +42 -0
  62. package/templates/runtime/index.ts +3 -0
  63. package/templates/runtime/model-import/model-import-limits.ts +15 -4
  64. package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
  65. package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
  66. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +15 -304
  67. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +41 -27
  68. package/templates/runtime/react/app-shell/toolcraft-app.tsx +39 -11
  69. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
  70. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
  71. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
  72. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
  73. package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
  74. package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
  75. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
  76. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
  77. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +76 -0
  78. package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +13 -1
  79. package/templates/runtime/react/canvas/canvas-shell.tsx +16 -1
  80. package/templates/runtime/react/canvas/product-scene-surface.test.tsx +195 -0
  81. package/templates/runtime/react/canvas/product-scene-surface.tsx +155 -0
  82. package/templates/runtime/react/canvas/runtime-scene-export.ts +1 -1
  83. package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +61 -0
  84. package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +89 -0
  85. package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +40 -19
  86. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
  87. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +18 -84
  88. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-collection.test.tsx +62 -0
  89. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-color-bank.test.tsx +67 -0
  90. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +43 -87
  91. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +68 -0
  92. package/templates/runtime/react/controls-panel/conditions/control-conditions.ts +32 -119
  93. package/templates/runtime/react/controls-panel/layout/controls-panel-control-group.tsx +5 -2
  94. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +10 -8
  95. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +3 -3
  96. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.test.tsx +63 -0
  97. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.tsx +138 -0
  98. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-items.tsx +104 -0
  99. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +115 -236
  100. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.test.tsx +95 -0
  101. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.tsx +82 -0
  102. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.test.tsx +73 -1
  103. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +50 -1
  104. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +1 -1
  105. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +2 -1
  106. package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +38 -6
  107. package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +1 -0
  108. package/templates/runtime/react/index.ts +4 -0
  109. package/templates/runtime/react/model-rendering/model-export.ts +1 -1
  110. package/templates/runtime/react/orientation-gizmo/orientation-gizmo-selection.ts +5 -5
  111. package/templates/runtime/react/orientation-gizmo/use-toolcraft-orientation-control-selection.ts +2 -2
  112. package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
  113. package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
  114. package/templates/runtime/scene/scene-bounds.test.ts +23 -0
  115. package/templates/runtime/scene/scene-bounds.ts +32 -19
  116. package/templates/runtime/schema/app-capabilities.test.ts +4 -0
  117. package/templates/runtime/schema/canvas-render-scale.ts +62 -0
  118. package/templates/runtime/schema/collection-item-controls.ts +29 -0
  119. package/templates/runtime/schema/control-applicability.test.ts +127 -0
  120. package/templates/runtime/schema/control-applicability.ts +433 -0
  121. package/templates/runtime/schema/control-schema-normalization.ts +177 -11
  122. package/templates/runtime/schema/controls-panel-actions.ts +2 -2
  123. package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
  124. package/templates/runtime/schema/controls-panel-section-layout.ts +14 -9
  125. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +62 -3
  126. package/templates/runtime/schema/define-toolcraft.collection-actions-item-controls.test.ts +127 -0
  127. package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +10 -4
  128. package/templates/runtime/schema/define-toolcraft.file-drop-item-controls.test.ts +98 -0
  129. package/templates/runtime/schema/define-toolcraft.file-drop-normalization.test.ts +27 -0
  130. package/templates/runtime/schema/define-toolcraft.segmented-control-fit.test.ts +20 -2
  131. package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
  132. package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +16 -3
  133. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
  134. package/templates/runtime/schema/define-toolcraft.source-collection.test.ts +79 -0
  135. package/templates/runtime/schema/define-toolcraft.ts +1 -1
  136. package/templates/runtime/schema/runtime-setup-section.ts +24 -5
  137. package/templates/runtime/schema/schema-resolvers.ts +0 -56
  138. package/templates/runtime/schema/types.ts +72 -17
  139. package/templates/runtime/state/canvas-background-state.ts +6 -0
  140. package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
  141. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
  142. package/templates/starter/AGENTS.md +18 -15
  143. package/templates/starter/NOTICE.md +6 -0
  144. package/templates/starter/docs/toolcraft/README.md +2 -1
  145. package/templates/starter/docs/toolcraft/acceptance-testing.md +23 -35
  146. package/templates/starter/docs/toolcraft/agent-worklog.md +18 -3
  147. package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -5
  148. package/templates/starter/docs/toolcraft/component-rules.md +8 -6
  149. package/templates/starter/docs/toolcraft/core/control-selection.md +9 -3
  150. package/templates/starter/docs/toolcraft/core/layout.md +7 -7
  151. package/templates/starter/docs/toolcraft/core/media-upload.md +4 -2
  152. package/templates/starter/docs/toolcraft/core/performance.md +14 -3
  153. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +16 -3
  154. package/templates/starter/docs/toolcraft/core/setup-export.md +49 -19
  155. package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
  156. package/templates/starter/docs/toolcraft/custom-controls.md +6 -4
  157. package/templates/starter/docs/toolcraft/decision-contract.md +3 -1
  158. package/templates/starter/docs/toolcraft/performance.md +20 -6
  159. package/templates/starter/docs/toolcraft/renderer-technique.md +5 -1
  160. package/templates/starter/docs/toolcraft/schema-reference.md +23 -25
  161. package/templates/starter/docs/toolcraft/workflow.md +7 -2
  162. package/templates/starter/e2e/app-browser-control-applicability-requirements.spec.ts +163 -0
  163. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
  164. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +50 -3
  165. package/templates/starter/e2e/app-browser-orientation-evidence.spec.ts +174 -21
  166. package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
  167. package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
  168. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
  169. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
  170. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +5 -34
  171. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +61 -16
  172. package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
  173. package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
  174. package/templates/starter/e2e/app-performance.spec.ts +16 -9
  175. package/templates/starter/e2e/{browser-conditional-output-evidence-helpers.ts → browser-background-output-evidence.ts} +0 -68
  176. package/templates/starter/e2e/browser-control-applicability-evidence.ts +114 -0
  177. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +29 -341
  178. package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
  179. package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
  180. package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
  181. package/templates/starter/e2e/browser-orientation-gizmo-evidence-helpers.ts +4 -0
  182. package/templates/starter/e2e/browser-orientation-gizmo-live-preconditions.ts +86 -0
  183. package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
  184. package/templates/starter/e2e/browser-runtime-applicability-requirements.ts +84 -0
  185. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +13 -3
  186. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +72 -35
  187. package/templates/starter/e2e/browser-semantic-evidence-test-helpers.ts +25 -0
  188. package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
  189. package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
  190. package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
  191. package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
  192. package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
  193. package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
  194. package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
  195. package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
  196. package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
  197. package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
  198. package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
  199. package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
  200. package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
  201. package/templates/starter/e2e/performance-path-helpers.ts +210 -91
  202. package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
  203. package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
  204. package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
  205. package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
  206. package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
  207. package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
  208. package/templates/starter/gitignore +1 -0
  209. package/templates/starter/package.json +1 -0
  210. package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
  211. package/templates/starter/scripts/toolcraft-contract-manifest.json +1 -0
  212. package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
  213. package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
  214. package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
  215. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +95 -103
  216. package/templates/starter/scripts/toolcraft-product-boundary-module-policy.mjs +101 -0
  217. package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
  218. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +0 -26
  219. package/templates/starter/scripts/toolcraft-product-control-bindings.mjs +182 -0
  220. package/templates/starter/scripts/toolcraft-product-control-boundary.mjs +146 -0
  221. package/templates/starter/scripts/toolcraft-product-control-boundary.test.mjs +186 -0
  222. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
  223. package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
  224. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +157 -0
  225. package/templates/starter/scripts/toolcraft-product-export-boundary.test.mjs +196 -0
  226. package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
  227. package/templates/starter/scripts/toolcraft-typescript-source-evidence-structure.test.mjs +20 -0
  228. package/templates/starter/scripts/toolcraft-typescript-source-evidence.mjs +1 -0
  229. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
  230. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +157 -206
  231. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
  232. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
  233. package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
  234. package/templates/starter/src/app/acceptance/artifact-export-intent.ts +130 -0
  235. package/templates/starter/src/app/acceptance/conditions.ts +1 -1
  236. package/templates/starter/src/app/acceptance/control-acceptance-context.ts +2 -2
  237. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +6 -20
  238. package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +24 -5
  239. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +2 -13
  240. package/templates/starter/src/app/acceptance/control-applicability-cases.test.ts +321 -0
  241. package/templates/starter/src/app/acceptance/control-applicability-cases.ts +290 -0
  242. package/templates/starter/src/app/acceptance/control-applicability.ts +332 -0
  243. package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +20 -9
  244. package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +4 -6
  245. package/templates/starter/src/app/acceptance/control-layout-model.ts +4 -4
  246. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +3 -10
  247. package/templates/starter/src/app/acceptance/control-parts.ts +8 -0
  248. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.test.ts +215 -0
  249. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.ts +132 -0
  250. package/templates/starter/src/app/acceptance/control-section-inventory.test.ts +87 -0
  251. package/templates/starter/src/app/acceptance/control-section-inventory.ts +74 -44
  252. package/templates/starter/src/app/acceptance/custom-controls.ts +11 -3
  253. package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
  254. package/templates/starter/src/app/acceptance/inline-layout.ts +64 -9
  255. package/templates/starter/src/app/acceptance/media-upload.ts +5 -1
  256. package/templates/starter/src/app/acceptance/orientation-gizmo.ts +18 -11
  257. package/templates/starter/src/app/acceptance/output-background-rules.ts +2 -2
  258. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +18 -10
  259. package/templates/starter/src/app/acceptance/output-export-model.ts +54 -11
  260. package/templates/starter/src/app/acceptance/output-export.ts +35 -12
  261. package/templates/starter/src/app/acceptance/output-video-export-rules.ts +99 -0
  262. package/templates/starter/src/app/acceptance/types.ts +30 -8
  263. package/templates/starter/src/app/acceptance/validate-coverage.ts +20 -2
  264. package/templates/starter/src/app/starter-acceptance.artifact-export-intent.test.ts +239 -0
  265. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +33 -0
  266. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +16 -1
  267. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +65 -0
  268. package/templates/starter/src/app/starter-acceptance.contract-fixtures.test.ts +41 -0
  269. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +112 -16
  270. package/templates/starter/src/app/starter-acceptance.control-applicability.test.ts +224 -0
  271. package/templates/starter/src/app/starter-acceptance.control-order.test.ts +58 -3
  272. package/templates/starter/src/app/starter-acceptance.control-state.test.ts +0 -111
  273. package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +3 -1
  274. package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +1 -1
  275. package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
  276. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +22 -0
  277. package/templates/starter/src/app/{starter-acceptance.image-video-export-settings.test.ts → starter-acceptance.image-export-settings.test.ts} +37 -9
  278. package/templates/starter/src/app/starter-acceptance.interaction-ownership.test.ts +12 -0
  279. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
  280. package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +11 -1
  281. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +28 -0
  282. package/templates/starter/src/app/starter-acceptance.output-export-intent.test.ts +384 -0
  283. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +46 -1
  284. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +112 -105
  285. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +12 -0
  286. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +1 -1
  287. package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +35 -24
  288. package/templates/starter/src/app/starter-acceptance.section-size.test.ts +72 -0
  289. package/templates/starter/src/app/starter-acceptance.ts +13 -2
  290. package/templates/starter/src/app/starter-acceptance.video-export-settings.test.ts +336 -0
  291. package/templates/starter/src/app/starter-acceptance.view-interaction.test.ts +8 -0
  292. package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
  293. package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
  294. package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
  295. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +4 -2
  296. package/templates/starter/src/routes/index.tsx +2 -0
  297. package/templates/ui/components/composites/tabs.tsx +2 -2
  298. package/templates/ui/components/controls/collection-actions/collection-item-groups.test.tsx +38 -0
  299. package/templates/ui/components/controls/collection-actions/collection-item-groups.tsx +36 -0
  300. package/templates/ui/components/controls/collection-actions/index.ts +2 -0
  301. package/templates/ui/components/controls/file-drop/file-drop-collection-control.tsx +137 -0
  302. package/templates/ui/components/controls/file-drop/file-drop-control.test.tsx +189 -0
  303. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +81 -88
  304. package/templates/ui/components/controls/file-drop/file-drop-presentation.tsx +131 -9
  305. package/templates/ui/components/controls/file-drop/file-drop-types.ts +36 -22
  306. package/templates/ui/components/controls/file-drop/index.ts +1 -0
  307. package/templates/ui/components/controls/index.ts +3 -0
  308. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +1 -1
  309. package/templates/ui/components/controls/range-slider/range-slider-value.test.ts +14 -0
  310. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -2
  311. package/templates/runtime/export/export.ts +0 -166
  312. package/templates/starter/src/app/acceptance/condition-exclusivity.test.ts +0 -60
  313. package/templates/starter/src/app/acceptance/condition-exclusivity.ts +0 -221
@@ -0,0 +1,74 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ import {
4
+ assertToolcraftExpectedDecodedPixels,
5
+ observeToolcraftDecodedPixels,
6
+ validateToolcraftExpectedDecodedPixels,
7
+ } from "./decoded-pixel-observation";
8
+
9
+ function createCenteredFixture(): Uint8ClampedArray {
10
+ const pixels = new Uint8ClampedArray(64 * 64 * 4);
11
+ for (let y = 0; y < 64; y += 1) {
12
+ for (let x = 0; x < 64; x += 1) {
13
+ const offset = (y * 64 + x) * 4;
14
+ const product = x >= 16 && x < 48 && y >= 16 && y < 48;
15
+ pixels.set(product ? [255, 0, 0, 255] : [0, 0, 0, 255], offset);
16
+ }
17
+ }
18
+ return pixels;
19
+ }
20
+
21
+ test("decoded pixels produce stable semantic bounds and hash", async () => {
22
+ const observation = await observeToolcraftDecodedPixels({
23
+ backgroundRgba: [0, 0, 0, 255],
24
+ pixels: createCenteredFixture(),
25
+ sourceHeight: 64,
26
+ sourceWidth: 64,
27
+ });
28
+
29
+ expect(observation.decodedPixelHash).toMatch(/^[a-f0-9]{64}$/u);
30
+ expect(observation.nonBackgroundBounds).toEqual({
31
+ height: 0.5,
32
+ width: 0.5,
33
+ x: 0.25,
34
+ y: 0.25,
35
+ });
36
+ expect(observation.centroid).toEqual({ x: 0.5, y: 0.5 });
37
+ expect(observation.occupiedAreaRatio).toBe(0.25);
38
+ expect(() =>
39
+ assertToolcraftExpectedDecodedPixels({
40
+ expectedPixels: [
41
+ { rgba: [255, 0, 0, 255], xRatio: 0.5, yRatio: 0.5 },
42
+ ],
43
+ mediaType: "image/png",
44
+ observation,
45
+ }),
46
+ ).not.toThrow();
47
+ });
48
+
49
+ test("decoded pixel expectations cannot provide empty, invalid, or loosened checks", async () => {
50
+ const observation = await observeToolcraftDecodedPixels({
51
+ backgroundRgba: [0, 0, 0, 255],
52
+ pixels: createCenteredFixture(),
53
+ sourceHeight: 64,
54
+ sourceWidth: 64,
55
+ });
56
+
57
+ expect(() => validateToolcraftExpectedDecodedPixels([])).toThrow(
58
+ "at least one exact pixel sample",
59
+ );
60
+ expect(() =>
61
+ validateToolcraftExpectedDecodedPixels([
62
+ { rgba: [256, 0, 0, 255], xRatio: 2, yRatio: 0 } as never,
63
+ ]),
64
+ ).toThrow();
65
+ expect(() =>
66
+ assertToolcraftExpectedDecodedPixels({
67
+ expectedPixels: [
68
+ { rgba: [0, 255, 0, 255], xRatio: 0.5, yRatio: 0.5 },
69
+ ],
70
+ mediaType: "image/png",
71
+ observation,
72
+ }),
73
+ ).toThrow("fixed tolerance is 0");
74
+ });
@@ -0,0 +1,227 @@
1
+ import type { ToolcraftNormalizedPixelBounds } from "./export-artifact-helpers";
2
+
3
+ export const TOOLCRAFT_DECODED_OBSERVATION_SIZE = 64;
4
+ export const TOOLCRAFT_LOSSLESS_PIXEL_DISTANCE_TOLERANCE = 0;
5
+ export const TOOLCRAFT_LOSSY_PIXEL_DISTANCE_TOLERANCE = 48;
6
+ export const TOOLCRAFT_BACKGROUND_PIXEL_DISTANCE_THRESHOLD = 24;
7
+
8
+ export type ToolcraftExpectedDecodedPixel = Readonly<{
9
+ rgba: readonly [number, number, number, number];
10
+ xRatio: number;
11
+ yRatio: number;
12
+ }>;
13
+
14
+ export type ToolcraftExpectedDecodedVideoSample = Readonly<{
15
+ pixels: readonly ToolcraftExpectedDecodedPixel[];
16
+ timeSeconds: number;
17
+ }>;
18
+
19
+ export type ToolcraftDecodedPixelObservation = Readonly<{
20
+ centroid: Readonly<{ x: number; y: number }> | null;
21
+ decodedPixelHash: string;
22
+ nonBackgroundBounds: ToolcraftNormalizedPixelBounds | null;
23
+ normalizedPixels: Uint8ClampedArray;
24
+ occupiedAreaRatio: number;
25
+ }>;
26
+
27
+ function assertRgba(
28
+ rgba: readonly number[],
29
+ label: string,
30
+ ): asserts rgba is readonly [number, number, number, number] {
31
+ if (
32
+ rgba.length !== 4 ||
33
+ rgba.some(
34
+ (channel) =>
35
+ !Number.isSafeInteger(channel) || channel < 0 || channel > 255,
36
+ )
37
+ ) {
38
+ throw new Error(`${label} must contain four integer RGBA channels from 0 to 255.`);
39
+ }
40
+ }
41
+
42
+ function assertRatio(value: number, label: string): void {
43
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
44
+ throw new Error(`${label} must be a finite normalized coordinate from 0 to 1.`);
45
+ }
46
+ }
47
+
48
+ export function validateToolcraftExpectedDecodedPixels(
49
+ pixels: readonly ToolcraftExpectedDecodedPixel[],
50
+ label = "Decoded pixel expectations",
51
+ ): void {
52
+ if (!Array.isArray(pixels) || pixels.length === 0) {
53
+ throw new Error(`${label} must contain at least one exact pixel sample.`);
54
+ }
55
+ for (const [index, pixel] of pixels.entries()) {
56
+ assertRatio(pixel.xRatio, `${label}[${index}].xRatio`);
57
+ assertRatio(pixel.yRatio, `${label}[${index}].yRatio`);
58
+ assertRgba(pixel.rgba, `${label}[${index}].rgba`);
59
+ }
60
+ }
61
+
62
+ export function getToolcraftRgbaDistance(
63
+ left: readonly number[],
64
+ right: readonly number[],
65
+ ): number {
66
+ return Math.sqrt(
67
+ left.reduce((sum, channel, index) => {
68
+ const delta = channel - (right[index] ?? 0);
69
+ return sum + delta * delta;
70
+ }, 0),
71
+ );
72
+ }
73
+
74
+ function normalizePixels({
75
+ pixels,
76
+ sourceHeight,
77
+ sourceWidth,
78
+ }: Readonly<{
79
+ pixels: Uint8ClampedArray;
80
+ sourceHeight: number;
81
+ sourceWidth: number;
82
+ }>): Uint8ClampedArray {
83
+ if (
84
+ !Number.isSafeInteger(sourceWidth) ||
85
+ sourceWidth <= 0 ||
86
+ !Number.isSafeInteger(sourceHeight) ||
87
+ sourceHeight <= 0 ||
88
+ pixels.length !== sourceWidth * sourceHeight * 4
89
+ ) {
90
+ throw new Error("Decoded pixel input must match positive source dimensions.");
91
+ }
92
+ if (
93
+ sourceWidth === TOOLCRAFT_DECODED_OBSERVATION_SIZE &&
94
+ sourceHeight === TOOLCRAFT_DECODED_OBSERVATION_SIZE
95
+ ) {
96
+ return new Uint8ClampedArray(pixels);
97
+ }
98
+
99
+ const normalized = new Uint8ClampedArray(
100
+ TOOLCRAFT_DECODED_OBSERVATION_SIZE * TOOLCRAFT_DECODED_OBSERVATION_SIZE * 4,
101
+ );
102
+ for (let y = 0; y < TOOLCRAFT_DECODED_OBSERVATION_SIZE; y += 1) {
103
+ const sourceY = Math.min(
104
+ sourceHeight - 1,
105
+ Math.floor(((y + 0.5) / TOOLCRAFT_DECODED_OBSERVATION_SIZE) * sourceHeight),
106
+ );
107
+ for (let x = 0; x < TOOLCRAFT_DECODED_OBSERVATION_SIZE; x += 1) {
108
+ const sourceX = Math.min(
109
+ sourceWidth - 1,
110
+ Math.floor(((x + 0.5) / TOOLCRAFT_DECODED_OBSERVATION_SIZE) * sourceWidth),
111
+ );
112
+ const sourceOffset = (sourceY * sourceWidth + sourceX) * 4;
113
+ const outputOffset =
114
+ (y * TOOLCRAFT_DECODED_OBSERVATION_SIZE + x) * 4;
115
+ normalized.set(pixels.subarray(sourceOffset, sourceOffset + 4), outputOffset);
116
+ }
117
+ }
118
+ return normalized;
119
+ }
120
+
121
+ async function hashPixels(pixels: Uint8ClampedArray): Promise<string> {
122
+ const source = new Uint8Array(
123
+ pixels.buffer,
124
+ pixels.byteOffset,
125
+ pixels.byteLength,
126
+ );
127
+ const hash = await crypto.subtle.digest("SHA-256", source);
128
+ return Array.from(new Uint8Array(hash), (byte) =>
129
+ byte.toString(16).padStart(2, "0"),
130
+ ).join("");
131
+ }
132
+
133
+ export async function observeToolcraftDecodedPixels({
134
+ backgroundRgba,
135
+ pixels,
136
+ sourceHeight,
137
+ sourceWidth,
138
+ }: Readonly<{
139
+ backgroundRgba: readonly [number, number, number, number];
140
+ pixels: Uint8ClampedArray;
141
+ sourceHeight: number;
142
+ sourceWidth: number;
143
+ }>): Promise<ToolcraftDecodedPixelObservation> {
144
+ assertRgba(backgroundRgba, "backgroundRgba");
145
+ const normalizedPixels = normalizePixels({
146
+ pixels,
147
+ sourceHeight,
148
+ sourceWidth,
149
+ });
150
+ let count = 0;
151
+ let minX = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
152
+ let minY = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
153
+ let maxX = -1;
154
+ let maxY = -1;
155
+ let sumX = 0;
156
+ let sumY = 0;
157
+
158
+ for (let y = 0; y < TOOLCRAFT_DECODED_OBSERVATION_SIZE; y += 1) {
159
+ for (let x = 0; x < TOOLCRAFT_DECODED_OBSERVATION_SIZE; x += 1) {
160
+ const offset = (y * TOOLCRAFT_DECODED_OBSERVATION_SIZE + x) * 4;
161
+ const pixel = normalizedPixels.subarray(offset, offset + 4);
162
+ if (
163
+ getToolcraftRgbaDistance(pixel, backgroundRgba) <=
164
+ TOOLCRAFT_BACKGROUND_PIXEL_DISTANCE_THRESHOLD
165
+ ) {
166
+ continue;
167
+ }
168
+ count += 1;
169
+ minX = Math.min(minX, x);
170
+ minY = Math.min(minY, y);
171
+ maxX = Math.max(maxX, x);
172
+ maxY = Math.max(maxY, y);
173
+ sumX += x + 0.5;
174
+ sumY += y + 0.5;
175
+ }
176
+ }
177
+
178
+ const size = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
179
+ return Object.freeze({
180
+ centroid:
181
+ count === 0
182
+ ? null
183
+ : Object.freeze({ x: sumX / count / size, y: sumY / count / size }),
184
+ decodedPixelHash: await hashPixels(normalizedPixels),
185
+ nonBackgroundBounds:
186
+ count === 0
187
+ ? null
188
+ : Object.freeze({
189
+ height: (maxY - minY + 1) / size,
190
+ width: (maxX - minX + 1) / size,
191
+ x: minX / size,
192
+ y: minY / size,
193
+ }),
194
+ normalizedPixels,
195
+ occupiedAreaRatio: count / (size * size),
196
+ });
197
+ }
198
+
199
+ export function assertToolcraftExpectedDecodedPixels({
200
+ expectedPixels,
201
+ mediaType,
202
+ observation,
203
+ }: Readonly<{
204
+ expectedPixels: readonly ToolcraftExpectedDecodedPixel[];
205
+ mediaType: "image/jpeg" | "image/png" | "video/mp4" | "video/webm";
206
+ observation: ToolcraftDecodedPixelObservation;
207
+ }>): void {
208
+ validateToolcraftExpectedDecodedPixels(expectedPixels);
209
+ const tolerance =
210
+ mediaType === "image/png"
211
+ ? TOOLCRAFT_LOSSLESS_PIXEL_DISTANCE_TOLERANCE
212
+ : TOOLCRAFT_LOSSY_PIXEL_DISTANCE_TOLERANCE;
213
+ const size = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
214
+
215
+ for (const expected of expectedPixels) {
216
+ const x = Math.min(size - 1, Math.floor(expected.xRatio * size));
217
+ const y = Math.min(size - 1, Math.floor(expected.yRatio * size));
218
+ const offset = (y * size + x) * 4;
219
+ const actual = observation.normalizedPixels.subarray(offset, offset + 4);
220
+ const distance = getToolcraftRgbaDistance(actual, expected.rgba);
221
+ if (distance > tolerance) {
222
+ throw new Error(
223
+ `Decoded pixel at (${expected.xRatio}, ${expected.yRatio}) differs from expected RGBA by ${distance.toFixed(2)}; fixed tolerance is ${tolerance}.`,
224
+ );
225
+ }
226
+ }
227
+ }
@@ -1,12 +1,52 @@
1
- export type ToolcraftExportArtifactInspection = {
1
+ export type ToolcraftNormalizedPixelBounds = Readonly<{
2
+ height: number;
3
+ width: number;
4
+ x: number;
5
+ y: number;
6
+ }>;
7
+
8
+ export type ToolcraftBinaryArtifactInspection = Readonly<{
2
9
  byteLength: number;
3
10
  contentHash?: string;
4
11
  durationMs?: number;
5
12
  frameCount?: number;
6
13
  height?: number;
14
+ kind?: "binary";
7
15
  mediaType?: string;
8
16
  width?: number;
9
- };
17
+ }>;
18
+
19
+ export type ToolcraftImageArtifactInspection = Readonly<{
20
+ byteLength: number;
21
+ decodedPixelHash: string;
22
+ height: number;
23
+ kind: "image";
24
+ mediaType: "image/jpeg" | "image/png";
25
+ nonBackgroundBounds: ToolcraftNormalizedPixelBounds | null;
26
+ width: number;
27
+ }>;
28
+
29
+ export type ToolcraftVideoPacketTiming = Readonly<{
30
+ durationSeconds: number;
31
+ timeSeconds: number;
32
+ }>;
33
+
34
+ export type ToolcraftVideoArtifactInspection = Readonly<{
35
+ byteLength: number;
36
+ durationMs: number;
37
+ frameCount: number;
38
+ height: number;
39
+ kind: "video";
40
+ mediaType: "video/mp4" | "video/webm";
41
+ packetTimings: readonly ToolcraftVideoPacketTiming[];
42
+ samplePixelHashes: readonly string[];
43
+ width: number;
44
+ }>;
45
+
46
+ export type ToolcraftExportArtifactInspection =
47
+ | ToolcraftBinaryArtifactInspection
48
+ | ToolcraftImageArtifactInspection
49
+ | ToolcraftVideoArtifactInspection;
10
50
 
11
51
  export type ToolcraftExportArtifactInspectionResult =
12
52
  | ToolcraftExportArtifactInspection
@@ -33,13 +73,46 @@ function assertPositiveInteger(
33
73
  field: string,
34
74
  requirementId: string,
35
75
  ): asserts value is number {
76
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
77
+ throw new Error(
78
+ `Export requirement "${requirementId}" must report a positive ${field} integer.`,
79
+ );
80
+ }
81
+ }
82
+
83
+ function validateNormalizedPixelBounds(
84
+ value: unknown,
85
+ requirementId: string,
86
+ ): void {
87
+ if (value === null) return;
88
+ if (!isRecord(value)) {
89
+ throw new Error(
90
+ `Export requirement "${requirementId}" must report normalized nonBackgroundBounds or null.`,
91
+ );
92
+ }
93
+ const { height, width, x, y } = value;
94
+ for (const [field, coordinate] of Object.entries({ height, width, x, y })) {
95
+ if (
96
+ typeof coordinate !== "number" ||
97
+ !Number.isFinite(coordinate) ||
98
+ coordinate < 0 ||
99
+ coordinate > 1 ||
100
+ ((field === "height" || field === "width") && coordinate === 0)
101
+ ) {
102
+ throw new Error(
103
+ `Export requirement "${requirementId}" must report normalized nonBackgroundBounds.${field}.`,
104
+ );
105
+ }
106
+ }
36
107
  if (
37
- typeof value !== "number" ||
38
- !Number.isSafeInteger(value) ||
39
- value <= 0
108
+ typeof x === "number" &&
109
+ typeof y === "number" &&
110
+ typeof width === "number" &&
111
+ typeof height === "number" &&
112
+ (x + width > 1 || y + height > 1)
40
113
  ) {
41
114
  throw new Error(
42
- `Export requirement "${requirementId}" must report a positive ${field} integer.`,
115
+ `Export requirement "${requirementId}" nonBackgroundBounds must remain inside the normalized artifact.`,
43
116
  );
44
117
  }
45
118
  }
@@ -64,25 +137,12 @@ export function assertToolcraftProducedArtifact(
64
137
  }
65
138
  }
66
139
 
67
- export function validateToolcraftExportArtifactInspection(
68
- observation: unknown,
140
+ function validateInspection(
141
+ inspection: Record<string, unknown>,
69
142
  requirementId: string,
70
- ): ToolcraftExportArtifactInspectionResult {
71
- const observations = Array.isArray(observation) ? observation : [observation];
72
- if (observations.length === 0) {
73
- throw new Error(
74
- `Export requirement "${requirementId}" must inspect at least one artifact.`,
75
- );
76
- }
77
-
78
- for (const inspection of observations) {
79
- if (!isRecord(inspection) || !("byteLength" in inspection)) {
80
- throw new Error(
81
- `Export requirement "${requirementId}" must return a typed artifact inspection with byteLength.`,
82
- );
83
- }
84
-
85
- assertPositiveInteger(inspection.byteLength, "byteLength", requirementId);
143
+ ): void {
144
+ assertPositiveInteger(inspection.byteLength, "byteLength", requirementId);
145
+ if (inspection.kind === undefined || inspection.kind === "binary") {
86
146
  if (inspection.durationMs !== undefined) {
87
147
  assertPositiveFiniteNumber(
88
148
  inspection.durationMs,
@@ -105,6 +165,108 @@ export function validateToolcraftExportArtifactInspection(
105
165
  );
106
166
  }
107
167
  }
168
+ return;
169
+ }
170
+
171
+ if (inspection.kind === "image") {
172
+ assertPositiveInteger(inspection.width, "width", requirementId);
173
+ assertPositiveInteger(inspection.height, "height", requirementId);
174
+ if (
175
+ inspection.mediaType !== "image/png" &&
176
+ inspection.mediaType !== "image/jpeg"
177
+ ) {
178
+ throw new Error(
179
+ `Export requirement "${requirementId}" must report a supported image mediaType.`,
180
+ );
181
+ }
182
+ if (
183
+ typeof inspection.decodedPixelHash !== "string" ||
184
+ !inspection.decodedPixelHash.trim()
185
+ ) {
186
+ throw new Error(
187
+ `Export requirement "${requirementId}" must hash decoded image pixels.`,
188
+ );
189
+ }
190
+ validateNormalizedPixelBounds(
191
+ inspection.nonBackgroundBounds,
192
+ requirementId,
193
+ );
194
+ return;
195
+ }
196
+
197
+ if (inspection.kind === "video") {
198
+ assertPositiveInteger(inspection.width, "width", requirementId);
199
+ assertPositiveInteger(inspection.height, "height", requirementId);
200
+ assertPositiveInteger(inspection.frameCount, "frameCount", requirementId);
201
+ assertPositiveFiniteNumber(inspection.durationMs, "durationMs", requirementId);
202
+ if (
203
+ inspection.mediaType !== "video/mp4" &&
204
+ inspection.mediaType !== "video/webm"
205
+ ) {
206
+ throw new Error(
207
+ `Export requirement "${requirementId}" must report a supported video mediaType.`,
208
+ );
209
+ }
210
+ if (
211
+ !Array.isArray(inspection.packetTimings) ||
212
+ inspection.packetTimings.length !== inspection.frameCount
213
+ ) {
214
+ throw new Error(
215
+ `Export requirement "${requirementId}" must report every real encoded packet timing.`,
216
+ );
217
+ }
218
+ for (const [index, timing] of inspection.packetTimings.entries()) {
219
+ if (
220
+ !isRecord(timing) ||
221
+ typeof timing.timeSeconds !== "number" ||
222
+ !Number.isFinite(timing.timeSeconds) ||
223
+ timing.timeSeconds < 0 ||
224
+ typeof timing.durationSeconds !== "number" ||
225
+ !Number.isFinite(timing.durationSeconds) ||
226
+ timing.durationSeconds <= 0
227
+ ) {
228
+ throw new Error(
229
+ `Export requirement "${requirementId}" must report valid timing for encoded packet ${index}.`,
230
+ );
231
+ }
232
+ }
233
+ if (
234
+ !Array.isArray(inspection.samplePixelHashes) ||
235
+ inspection.samplePixelHashes.length === 0 ||
236
+ inspection.samplePixelHashes.some(
237
+ (hash) => typeof hash !== "string" || !hash.trim(),
238
+ )
239
+ ) {
240
+ throw new Error(
241
+ `Export requirement "${requirementId}" must hash decoded video samples.`,
242
+ );
243
+ }
244
+ return;
245
+ }
246
+
247
+ throw new Error(
248
+ `Export requirement "${requirementId}" returned an unknown artifact inspection kind.`,
249
+ );
250
+ }
251
+
252
+ export function validateToolcraftExportArtifactInspection(
253
+ observation: unknown,
254
+ requirementId: string,
255
+ ): ToolcraftExportArtifactInspectionResult {
256
+ const observations = Array.isArray(observation) ? observation : [observation];
257
+ if (observations.length === 0) {
258
+ throw new Error(
259
+ `Export requirement "${requirementId}" must inspect at least one artifact.`,
260
+ );
261
+ }
262
+
263
+ for (const inspection of observations) {
264
+ if (!isRecord(inspection) || !("byteLength" in inspection)) {
265
+ throw new Error(
266
+ `Export requirement "${requirementId}" must return a typed artifact inspection with byteLength.`,
267
+ );
268
+ }
269
+ validateInspection(inspection, requirementId);
108
270
  }
109
271
 
110
272
  return observation as ToolcraftExportArtifactInspectionResult;
@@ -115,12 +277,31 @@ export function getToolcraftSemanticArtifactSignature(
115
277
  requirementId: string,
116
278
  ): string {
117
279
  validateToolcraftExportArtifactInspection(inspection, requirementId);
280
+ if (inspection.kind === "image") {
281
+ return JSON.stringify({
282
+ decodedPixelHash: inspection.decodedPixelHash,
283
+ height: inspection.height,
284
+ mediaType: inspection.mediaType,
285
+ nonBackgroundBounds: inspection.nonBackgroundBounds,
286
+ width: inspection.width,
287
+ });
288
+ }
289
+ if (inspection.kind === "video") {
290
+ return JSON.stringify({
291
+ durationMs: inspection.durationMs,
292
+ frameCount: inspection.frameCount,
293
+ height: inspection.height,
294
+ mediaType: inspection.mediaType,
295
+ packetTimings: inspection.packetTimings,
296
+ samplePixelHashes: inspection.samplePixelHashes,
297
+ width: inspection.width,
298
+ });
299
+ }
118
300
  if (!inspection.contentHash?.trim()) {
119
301
  throw new Error(
120
302
  `Export requirement "${requirementId}" must report contentHash from decoded product output for semantic comparison.`,
121
303
  );
122
304
  }
123
-
124
305
  return JSON.stringify({
125
306
  contentHash: inspection.contentHash,
126
307
  durationMs: inspection.durationMs,
@@ -0,0 +1,41 @@
1
+ import fs from "node:fs/promises";
2
+
3
+ import { expect, test, type Download } from "@playwright/test";
4
+
5
+ import { inspectToolcraftImageDownload } from "./image-artifact-inspection";
6
+
7
+ test("image inspection hashes decoded pixels and reports product bounds", async ({
8
+ page,
9
+ }, testInfo) => {
10
+ await page.goto("/");
11
+ const base64 = await page.evaluate(() => {
12
+ const canvas = document.createElement("canvas");
13
+ canvas.width = 64;
14
+ canvas.height = 32;
15
+ const context = canvas.getContext("2d");
16
+ if (!context) throw new Error("Missing test canvas context.");
17
+ context.fillStyle = "#000000";
18
+ context.fillRect(0, 0, 64, 32);
19
+ context.fillStyle = "#FF0000";
20
+ context.fillRect(16, 8, 32, 16);
21
+ return canvas.toDataURL("image/png").split(",")[1]!;
22
+ });
23
+ const artifactPath = testInfo.outputPath("decoded-fixture.png");
24
+ await fs.writeFile(artifactPath, Buffer.from(base64, "base64"));
25
+ const download = { path: async () => artifactPath } as Download;
26
+
27
+ const result = await inspectToolcraftImageDownload({
28
+ backgroundRgba: [0, 0, 0, 255],
29
+ download,
30
+ page,
31
+ });
32
+
33
+ expect(result.inspection).toMatchObject({
34
+ height: 32,
35
+ kind: "image",
36
+ mediaType: "image/png",
37
+ nonBackgroundBounds: { height: 0.5, width: 0.5, x: 0.25, y: 0.25 },
38
+ width: 64,
39
+ });
40
+ expect(result.inspection.decodedPixelHash).toMatch(/^[a-f0-9]{64}$/u);
41
+ });