@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
@@ -26,7 +26,8 @@ export const TOOLCRAFT_RUNTIME_COMPONENT_CONTRACTS = {
26
26
  "Infinity canvas removes finite artboard bounds and clipping, hides Aspect ratio, Canvas width, and Canvas height, and preserves the dormant finite canvas size for exact restoration when disabled.",
27
27
  "Infinity canvas suppresses the bounded product-rendered preview background so the dormant finite output does not appear as a second canvas; when the standard Background pair is enabled, CanvasShell fills the complete infinite viewport with the selected Background color.",
28
28
  "When the standard Background pair exists, disabling Background atomically restores finite mode and disables Infinity canvas; re-enabling Background restores switch availability without enabling Infinity automatically.",
29
- "Infinite exports use canonical world-space scene frames: ToolcraftAppComposition.sceneBoundsProvider contributes product bounds, runtime image and model bounds are unioned once, hidden or suppressed content is excluded, PNG crops tightly, and video uses one time-range envelope.",
29
+ "Infinite exports use canonical world-space scene frames: ToolcraftAppComposition.sceneBoundsProvider contributes exact-state product bounds, runtime image and model bounds are unioned, hidden or suppressed content is excluded, PNG crops tightly, and runtime video unions every scheduled frame state into one envelope.",
30
+ "Infinite live preview uses that same exact-state product frame: runtime positions one product scene surface from sceneBoundsProvider, and custom raster/WebGL output consumes useToolcraftProductSceneFrame for backing size and world-to-local translation instead of dormant canvas.size.",
30
31
  "Do not let canvas.upload choose intrinsic-media by default. Upload without explicit sizing resolves to editable-output so source/background images do not own product output size.",
31
32
  "Use intrinsic-media only for true media-viewer or source-native apps where the natural uploaded/generated media size is the product output; record the reason and prove it with intrinsic-media-size acceptance.",
32
33
  "Use editable-output for generated, exportable, shader, poster, badge, wall, banner, thumbnail, procedural, reference-clone, and product-output apps so users always see Aspect ratio, Canvas width, and Canvas height.",
@@ -104,6 +105,7 @@ export const TOOLCRAFT_RUNTIME_COMPONENT_CONTRACTS = {
104
105
  runtimeSetupModeRowRule,
105
106
  runtimeSetupModeHelpRule,
106
107
  "Keep sticky footer panelActions for product delivery actions only, such as Export PNG, Export Video, Copy, Generate, Apply, or Download.",
108
+ "Typed Export PNG and Export Video actions are runtime-owned and consume one ToolcraftAppComposition.exportRenderer; use onPanelAction only for non-export product actions.",
107
109
  ],
108
110
  capabilities: ["settings-import-export"],
109
111
  commands: ["controls.setValue", "timeline.setCurrentTime", "timeline.setDuration"],
@@ -126,11 +128,12 @@ export const TOOLCRAFT_RUNTIME_COMPONENT_CONTRACTS = {
126
128
  "A generic canvas hash difference is not enough for workload or semantic controls; assert the intended direction of the effect.",
127
129
  "Component variants are accepted entities too; tests should fail if a non-default Toolcraft control variant falls back to the default variant or custom markup.",
128
130
  "Conditional entities require fixtures that make the condition observable.",
129
- "Use visibleWhen for mode-, type-, source-, include-, variant-, or count-exclusive sections or controls that do not belong to the current selected state.",
130
- "When visibleWhen references a selector for the same product entity or selected branch, keep the selector and its dependent controls in one semantic section. A section that exists only because one selector option is active is not a separate product section just because the branch uses a standalone control.",
131
- "When a count/quantity control determines how many sibling controls are available, hide unavailable siblings with visibleWhen; do not render all possible controls while the renderer reads only the first N.",
132
- "If a switch/select/segmented/tabs/imagePicker/checkbox chooses a branch for the same product entity, controls outside the current branch use visibleWhen, not disabledWhen.",
133
- "Do not use schema disabled: true or disabledWhen for generated product controls; the panel should show only controls usable in the current state. Runtime primitives may still have disabled styling internally, but app schemas should model product availability with visibleWhen.",
131
+ 'Every generated product control declares applicability as mode: "always" or mode: "conditional"; omitted applicability and legacy visibleWhen are runtime compatibility only and fail starter acceptance.',
132
+ "Conditional applicability combines every predicate with AND, hides controls outside mode/type/source/include/variant/count branches, and preserves their runtime values while hidden.",
133
+ "A visible product control must prove its existing product outcome in every supported finite sibling-selector branch from its Control Section Inventory, including always controls and selectors omitted from its predicates.",
134
+ "When applicability references a selector for the same product entity or selected branch, keep the selector and its dependent controls in one semantic section. A section that exists only because one selector option is active is not a separate product section just because the branch uses a standalone control.",
135
+ "When a count/quantity control determines how many sibling controls are applicable, conditional applicability hides unavailable siblings; do not render all possible controls while the renderer reads only the first N.",
136
+ "Do not use schema disabled: true or disabledWhen for generated product availability; runtime primitives may use disabled styling internally, but product controls outside the active branch are absent.",
134
137
  "Do not leave inactive conditional controls visible while making the renderer ignore them.",
135
138
  "Before controls or canvas interactions, declare typed interactionOwnership for every operation that could plausibly live on either surface. User request, inspected reference, or product usability selects one primary owner.",
136
139
  "Do not mirror one operation across canvas and panel, even with different labels, ids, styling, or custom chrome. The same target and operation capability has one surface owner.",
@@ -237,9 +240,10 @@ export const TOOLCRAFT_RUNTIME_COMPONENT_CONTRACTS = {
237
240
  "A concise property label such as Speed, Color, Size, or Opacity is allowed when the nearest visible section or group clearly names the affected product entity.",
238
241
  "When the section is generic, mixed, missing, or otherwise weak context, include the affected entity or role in the label: Pattern color, Background opacity, Wave speed, Stroke width.",
239
242
  "Acceptance validators suggest semantic replacement labels for weak generic labels; fix the schema label instead of relying on runtime fallback rewriting.",
240
- "Controls-panel sections should stay discrete: two to seven product controls is the normal size, and larger sections must split by product sub-entity or workflow stage.",
241
- "Generated product apps must export starterControlSectionInventory beside acceptance. Every product controls section declares title, exact targets, groupingReason, and entity or workflowStage; intentional splits of one target entity require workflowStage and splitReason on every split section.",
242
- "Section splitting must preserve dependency cohesion: a selector stays with the visibleWhen controls it gates when they share the same target entity or selected branch. Use internal spacing/dividers or a more specific section title before splitting dependent branch controls away.",
243
+ "One to seven controls is the normal section size. Eight to ten controls are allowed only for one cohesive entity and require semanticGroup on every control; ten is the hard maximum.",
244
+ "Every Control Section Inventory entry declares required entityId, entity, exact targets, and groupingReason. One entity with ten or fewer controls stays in one section regardless of control type, visual height, or target namespace.",
245
+ "An entity above ten controls splits into balanced workflow sections of two to ten controls. Every split section keeps the same entityId and entity and declares a unique workflowStage plus splitReason.",
246
+ "Section splitting must preserve dependency cohesion: a selector stays with the applicability-gated controls it owns when they share the same target entity or selected branch. Use internal spacing/dividers or a more specific section title before splitting dependent branch controls away.",
243
247
  "Every app-authored controls-panel body section must have a short meaningful visible title. Runtime-created Setup renders as the first visible headerless controls block with no title, reset action, collapse button, or collapsed state; sticky footer action sections use the technical title Export but render without a visible heading.",
244
248
  "Every visible app-authored controls-panel section title renders through the standard 36px collapsible header row with vertically centered text and the runtime collapse icon; generated apps must not hand-build section headers.",
245
249
  "Controls-panel section expand and collapse uses the standard runtime height/opacity animation; generated apps must not replace it with instant custom section visibility.",
@@ -318,13 +322,14 @@ export const TOOLCRAFT_RUNTIME_COMPONENT_CONTRACTS = {
318
322
  "Before choosing no timeline for any animated product, write an Animation Intent Inventory: product transport, editable keyframes, or autonomous decorative output, plus the user-facing time behaviors present or intentionally absent.",
319
323
  'User-requested product animation defaults to panels.timeline mode "playback" unless the spec explicitly declares autonomous decorative/self-running output with no play, pause, scrub, duration, loop, export-at-time behavior, or video export.',
320
324
  'Any product app with Export Video must enable the top Toolcraft timeline: use panels.timeline mode "playback" for product animation transport, or mode "keyframes" when exported animation is driven by keyframes.',
325
+ 'Export Video is valid only when productReadiness.exportIntent.video is "user-requested" with non-empty explicit user-request evidence; animation, playback, keyframes, and timeline presence never authorize video export.',
321
326
  'Use panels.timeline: { mode: "playback" } when the product needs user-facing play, pause, scrubbing, duration, loop, restart, time progress, export-at-time controls, or video export.',
322
327
  'When panels.timeline is enabled for a new Toolcraft app, appTransferMode.animationIntent must match it: mode "timeline-playback" for playback, or mode "timeline-keyframes" for keyframes.',
323
328
  "Playback renderers must consume runtime timeline state; pause freezes output, scrubbing renders a deterministic frame, and the full animation cycle maps to state.timeline.durationSeconds instead of a local fixed duration.",
324
329
  "When the product has a known loop duration, declare it as panels.timeline.defaultDurationSeconds; the runtime timeline duration starts from that loop duration instead of an unrelated 8s default.",
325
330
  'Timeline animation intent must declare loopDuration with source "reference", "user-request", or "product-derived", plus seconds and evidence; runtime/template fallback 8s is not a valid source.',
326
331
  "panels.timeline.defaultDurationSeconds must match animationIntent.loopDuration.seconds for playback/keyframe animation, so the initial timeline UI shows the declared product loop instead of a generic default.",
327
- "Video export renderers must render deterministic frames from runtime timeline timestamps and duration; do not treat autonomous wall-clock time or captureStream recording time as the source of product duration.",
332
+ "Video export frames are rendered deterministically from the runtime-owned 30 FPS timeline schedule; product code must not use autonomous wall-clock time, captureStream, or MediaRecorder as the source or fallback for product duration.",
328
333
  "Playback renderers may compute an initial loop duration default during app initialization or reset, but must not watch state.timeline.durationSeconds and dispatch timeline.setDuration back to a computed local duration. User-edited timeline duration becomes the loop duration source of truth after initialization or reset.",
329
334
  "Playback renderers should use getToolcraftTimelineLoopTime or getToolcraftTimelineLoopProgress to derive product loop phase from state.timeline.currentTimeSeconds and state.timeline.durationSeconds; do not hand-roll wall-clock, fixed-duration, mirror, yoyo, ping-pong, or reverse phase math.",
330
335
  "Product animation loop means a seamless forward-only cycle by default: motion advances in one direction, first and last frames stitch without a visible jump, and mirror/yoyo/ping-pong/reverse loops require explicit user request.",
@@ -26,6 +26,7 @@ describe("Toolcraft template component contracts: core", () => {
26
26
  "checkbox",
27
27
  "actions",
28
28
  "collectionActions",
29
+ "sourceCollection",
29
30
  "panelActions",
30
31
  "colorOpacity",
31
32
  "palette",
@@ -72,6 +73,7 @@ describe("Toolcraft template component contracts: core", () => {
72
73
  "palette",
73
74
  "actions",
74
75
  "collectionActions",
76
+ "sourceCollection",
75
77
  "panelActions",
76
78
  "customControl",
77
79
  ] as const) {
@@ -54,8 +54,10 @@ describe("Toolcraft template decision contract", () => {
54
54
 
55
55
  expect(rule?.level).toBe("invariant");
56
56
  expect(rule?.desiredBehavior).toMatch(/sceneBoundsProvider/i);
57
- expect(rule?.desiredBehavior).toMatch(/visible scene elements/i);
58
- expect(rule?.desiredBehavior).toMatch(/time-range envelope/i);
57
+ expect(rule?.desiredBehavior).toMatch(/useToolcraftProductSceneFrame/i);
58
+ expect(rule?.desiredBehavior).toMatch(/live infinite preview/i);
59
+ expect(rule?.desiredBehavior).toMatch(/one exact state/i);
60
+ expect(rule?.desiredBehavior).toMatch(/runtime-owned timestamp schedule/i);
59
61
  expect(rule?.desiredBehavior).toMatch(/finite canvas size/i);
60
62
  expect(rule?.enforcement).toContain("acceptance-validator");
61
63
  expect(rule?.enforcement).toContain("browser-helper");
@@ -116,6 +118,22 @@ describe("Toolcraft template decision contract", () => {
116
118
  );
117
119
  });
118
120
 
121
+ it("requires artifact delivery to correspond exactly to explicit product export intent", () => {
122
+ const rule = getToolcraftDecisionRule("output-export-required");
123
+
124
+ expect(rule?.level).toBe("invariant");
125
+ expect(rule?.desiredBehavior).toMatch(/productReadiness\.exportIntent/);
126
+ expect(rule?.desiredBehavior).toMatch(/image[^.]*Toolcraft default/i);
127
+ expect(rule?.desiredBehavior).toMatch(/video[^.]*explicit user request[^.]*evidence/i);
128
+ expect(rule?.desiredBehavior).toMatch(
129
+ /animation, playback, keyframes, or timeline[^.]*never/i,
130
+ );
131
+ expect(rule?.desiredBehavior).toMatch(/image[^.]*removed[^.]*explicit user[^.]*evidence/i);
132
+ expect(rule?.desiredBehavior).toMatch(
133
+ /intent[^.]*correspond exactly[^.]*schema actions, settings sections, and acceptance coverage/i,
134
+ );
135
+ });
136
+
119
137
  it("makes renderer technology a default with escape hatches", () => {
120
138
  const rule = getToolcraftDecisionRule("renderer-technique-inventory");
121
139
 
@@ -228,10 +246,28 @@ describe("Toolcraft template decision contract", () => {
228
246
  expect(rule?.desiredBehavior).toMatch(/performance-iteration/i);
229
247
  expect(rule?.desiredBehavior).toMatch(/bounded targeted iteration/i);
230
248
  expect(rule?.desiredBehavior).toMatch(
231
- /one complaint maps to one exact authority-selected targeted performance iteration over canonical affected path IDs/i,
249
+ /only a localized complaint or a post-clarification targeted choice creates performance-iteration intent and one bounded iteration over exact canonical affected paths/iu,
250
+ );
251
+ expect(rule?.desiredBehavior).toMatch(
252
+ /classifier output establishes complaint authority only and never path localization/iu,
232
253
  );
233
254
  expect(rule?.desiredBehavior).toMatch(
234
- /complaint wording and repetition never select the complete performance matrix/i,
255
+ /regardless of whether the classifier returned high-confidence performance-iteration or needs-agent-judgment, unresolved localization creates neither performance-iteration intent nor canonical path authority/iu,
256
+ );
257
+ expect(rule?.desiredBehavior).not.toMatch(
258
+ /one complaint maps to one exact authority-selected targeted performance iteration/iu,
259
+ );
260
+ expect(rule?.desiredBehavior).not.toMatch(
261
+ /direct and repeated performance complaints each start one bounded targeted iteration/iu,
262
+ );
263
+ expect(rule?.desiredBehavior).toMatch(
264
+ /localized complaint[\s\S]*without asking the user/iu,
265
+ );
266
+ expect(rule?.desiredBehavior).toMatch(
267
+ /ambiguous or unresolved localization[\s\S]*one user-facing clarification[\s\S]*targeted diagnosis or a complete review/iu,
268
+ );
269
+ expect(rule?.desiredBehavior).toMatch(
270
+ /repetition alone never expands scope or selects the complete performance matrix/i,
235
271
  );
236
272
  expect(rule?.desiredBehavior).toMatch(
237
273
  /explicit natural-language request for the complete audit/i,
@@ -244,6 +280,9 @@ describe("Toolcraft template decision contract", () => {
244
280
  expect(rule?.desiredBehavior).toMatch(
245
281
  /renderScaleCoverage "selected-backing-pixels" for interaction and steady, plus playback when timeline is enabled/i,
246
282
  );
283
+ expect(rule?.desiredBehavior).toMatch(
284
+ /every measured path[\s\S]*devicePixelRatio × 2[\s\S]*each measured phase/iu,
285
+ );
247
286
  expect(rule?.desiredBehavior).toMatch(
248
287
  /never downsample[\s\S]*clamp the selected render scale to pass a performance budget/i,
249
288
  );
@@ -58,7 +58,7 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
58
58
  area: "runtime-shell",
59
59
  currentConstraint: "Apps must assemble through defineToolcraft and ToolcraftApp.",
60
60
  desiredBehavior:
61
- "Generated apps use the Toolcraft runtime shell and keep product-specific rendering inside supported extension points: schema controls, canvasContent product output, controlRenderers for true custom controls, ToolcraftApp onPanelAction, and runtime commands. App-specific code must not import or render low-level runtime surfaces or built-in control components directly.",
61
+ "Generated apps use the Toolcraft runtime shell and keep product-specific rendering inside supported extension points: schema controls, bounded canvasContent product output, editor-only full-viewport infiniteCanvasContent product output, controlRenderers for true custom controls, one shared ToolcraftAppComposition.exportRenderer for typed image/video actions, ToolcraftApp onPanelAction for non-export product actions, and runtime commands. App-specific code must not import or render low-level runtime surfaces or built-in control components directly, instantiate encoders, or own artifact download mechanics.",
62
62
  enforcement: [
63
63
  "cli-integrity-check",
64
64
  "acceptance-validator",
@@ -87,7 +87,7 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
87
87
  currentConstraint:
88
88
  "The Toolcraft canvas shell owns the visible workspace backing behind product output.",
89
89
  desiredBehavior:
90
- "Generated apps preserve the runtime canvas surface and do not replace or hide it when product renderers customize their own background. In infinite mode the shell fills the complete viewport with the selected standard Background color, while bounded product background pixels remain suppressed.",
90
+ "Generated apps preserve the runtime canvas surface and do not replace or hide it when product renderers customize their own background. In infinite mode the shell fills the complete viewport with the selected standard Background color, while bounded product background pixels remain suppressed. Preview-only product environments may layer through infiniteCanvasContent across the complete viewport under the transformed world; that layer is pointer-transparent and excluded from scene bounds and export.",
91
91
  enforcement: ["browser-helper", "starter-agents", "spec-checklist"],
92
92
  id: "canvas-surface-preserved",
93
93
  level: "invariant",
@@ -99,7 +99,7 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
99
99
  currentConstraint:
100
100
  "Editable output can switch from a finite artboard to an unbounded workspace, but product renderers and export actions still need one canonical world-space frame contract.",
101
101
  desiredBehavior:
102
- "Editable-output apps expose one runtime-owned Infinity canvas mode. Enabling it removes finite artboard clipping and size controls while preserving the dormant finite canvas size for exact restoration. When the standard Background pair exists, its selected color fills the complete infinite viewport and Background off atomically resolves finite mode and disables Infinity. Runtime image and model assets retain explicit world-space frames. ToolcraftAppComposition.sceneBoundsProvider supplies product-renderer bounds through one direct composition boundary; image export crops to the union of visible scene elements and video export uses one provider time-range envelope for all rendered frames. Hidden layers, suppressed default consumers, and editor-only UI do not expand output bounds. Finite mode continues to export the full finite canvas size, while empty, unavailable, and unsafe oversized infinite exports return typed visible failures.",
102
+ "Editable-output apps expose one runtime-owned Infinity canvas mode. Enabling it removes finite artboard clipping and size controls while preserving the dormant finite canvas size for exact restoration. When the standard Background pair exists, its selected color fills the complete infinite viewport and Background off atomically resolves finite mode and disables Infinity. Runtime image and model assets retain explicit world-space frames. ToolcraftAppComposition.sceneBoundsProvider supplies product-renderer bounds for one exact state through one direct composition boundary. Runtime applies that same product frame to the live infinite preview, and custom raster/WebGL renderers consume useToolcraftProductSceneFrame for backing size and world-to-local translation instead of the dormant finite canvas size. ToolcraftAppComposition.infiniteCanvasContent is reserved for preview-only product environments that fill the complete Infinity viewport without world transforms, pointer input, scene-bounds participation, or export inclusion. Image export resolves the current frame and video export unions every state from the runtime-owned timestamp schedule. Hidden layers, suppressed default consumers, viewport environments, and editor-only UI do not expand preview or output bounds. Finite preview and export remain the full finite canvas and do not call the provider, while empty, unavailable, and unsafe oversized infinite exports return typed visible failures.",
103
103
  enforcement: [
104
104
  "acceptance-validator",
105
105
  "browser-helper",
@@ -203,9 +203,9 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
203
203
  {
204
204
  area: "controls",
205
205
  currentConstraint:
206
- "Every visible schema control must bind to runtime state, reset from schema defaults, and have acceptance and browser coverage.",
206
+ "Every product control explicitly declares always or conditional applicability; every visible finite sibling branch binds to runtime state, resets from schema defaults, and passes its accepted browser outcome.",
207
207
  desiredBehavior:
208
- "Controls are not decorative; each visible control proves its product responsibility through runtime and output observables.",
208
+ "Non-applicable controls are absent with values preserved, and no always or conditional control remains visible in a finite product branch where its value is ignored.",
209
209
  enforcement: ["acceptance-validator", "browser-helper"],
210
210
  id: "controls-product-coverage",
211
211
  level: "invariant",
@@ -215,13 +215,13 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
215
215
  {
216
216
  area: "controls",
217
217
  currentConstraint:
218
- "Product-output apps expose final output delivery through sticky footer panelActions.",
218
+ "Product artifact delivery must match the required productReadiness.exportIntent declaration.",
219
219
  desiredBehavior:
220
- 'Product apps include Export PNG plus an "Image Export" section for format and 2K/4K/8K resolution. Apps whose requested delivery includes animation also include Export Video plus "Video Export" settings; a playback or keyframe timeline may exist for interactive preview without inventing video delivery. Copy can be secondary, but it does not replace export. Product apps declare the standard background pair and runtime places it in Setup: Background beside Infinity canvas, Color below, and Timeline last. Standard helpers own runtime PNG transparency, live preview product-background visibility, infinite viewport color, the Background-to-Infinity finite fallback, and selected image dimensions or retina fallback, while video keeps the background.',
220
+ 'Every product declares productReadiness.exportIntent. Image export starts as the Toolcraft default and is removed only with explicit user-removal evidence; video export requires explicit user request evidence. Animation, playback, keyframes, or timeline presence never changes artifact delivery intent. Resolved intent capabilities correspond exactly to schema actions, settings sections, and acceptance coverage: image-only, image-plus-video, video-only, and explicit no-export products expose only the runtime-owned artifact surfaces their intent enables. Product apps declare the standard background pair and runtime places it in Setup: Background beside Infinity canvas, Color below, and Timeline last. One shared product exportRenderer draws deterministic scene-coordinate frames for enabled artifact types; runtime owns settings, scene crop, PNG/JPEG/video background semantics, visible runtime media/model composition, exact timestamped encoding, download, progress, and typed failures.',
221
221
  enforcement: ["acceptance-validator", "performance-validator", "browser-helper", "starter-agents"],
222
222
  id: "output-export-required",
223
223
  level: "invariant",
224
- title: "Product output always has export",
224
+ title: "Product output follows artifact export intent",
225
225
  verdict: "move-to-validator",
226
226
  },
227
227
  {
@@ -229,7 +229,7 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
229
229
  currentConstraint:
230
230
  "Every product controls section must be represented in the exported Control Section Inventory before schema authoring.",
231
231
  desiredBehavior:
232
- "Generated product apps publish a typed Control Section Inventory with section title, product entity or workflow stage, exact targets, and a concrete grouping reason so section ownership is machine-checkable without prescribing the final product layout.",
232
+ "Generated product apps publish a typed Control Section Inventory with stable entityId, human-readable entity, exact targets, and a concrete grouping reason. One entity stays in one section through ten controls; larger entities use balanced two-to-ten-control workflow stages with explicit split evidence, so section ownership and reset boundaries are machine-checkable without runtime rewriting.",
233
233
  enforcement: ["acceptance-validator", "docs", "starter-agents", "spec-checklist"],
234
234
  id: "controls-section-inventory-required",
235
235
  level: "invariant",
@@ -253,7 +253,7 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
253
253
  currentConstraint:
254
254
  "Labels, color placement, section grouping, selector order, and inline density need product-aware decisions.",
255
255
  desiredBehavior:
256
- "Given a valid Control Section Inventory, ordering, section cohesion, labels, color placement, and compact density remain product-aware recommendations. Validators report questionable grouping without rejecting a coherent product-specific choice solely because it differs from a preferred heuristic.",
256
+ "Given a valid Control Section Inventory, ordering, labels, color placement, and compact density remain product-aware recommendations. The ten-control maximum and entity cohesion are inventory invariants; heuristics may report questionable presentation without rejecting a coherent product-specific choice solely because it differs from a preferred layout.",
257
257
  enforcement: ["acceptance-validator", "schema-normalization", "docs", "starter-agents"],
258
258
  id: "controls-layout-heuristics",
259
259
  level: "heuristic",
@@ -26,6 +26,8 @@ describe("Toolcraft canonical performance verification policy", () => {
26
26
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY).toEqual({
27
27
  agentBrowser:
28
28
  "Agent-controlled browser checks are limited to diagnosis and targeted visual investigation; they do not mint durable performance baselines or receipts.",
29
+ complaintRouting:
30
+ "Performance authority and path localization are separate decisions. Classifier output establishes complaint authority only and never path localization. A localized complaint lets the agent select canonical affected paths and run one targeted iteration without asking the user. An ambiguous or unresolved localization requires one user-facing clarification naming visible operations and offering targeted diagnosis or a complete review; never ask the user for internal path ids. Regardless of whether the classifier returned high-confidence performance-iteration or needs-agent-judgment, unresolved localization creates neither performance-iteration intent nor canonical path authority. A broad or honestly unlocalizable problem may justify recommending the complete review immediately, but only the user's explicit request or acceptance authorizes it. A direct request for a complete review runs the full audit without another clarification.",
29
31
  deliveryBatch:
30
32
  "Classify and verify one coherent user-visible delivery batch instead of treating each internal implementation pass or steering correction as a separate aggregate verification boundary.",
31
33
  deliveryCommand:
@@ -37,13 +39,13 @@ describe("Toolcraft canonical performance verification policy", () => {
37
39
  fullAuditDiscovery:
38
40
  "The complete performance matrix remains outside conversational delivery. Complaints, repetition, tier, filename, and touched subsystem never launch it automatically. After two consecutive compatible protected performance iterations, the agent must offer a slower complete audit if the user remains unsatisfied; a demonstrably broad or unlocalizable cross-system problem may justify the same offer earlier. An explicit natural-language request for the complete audit, or explicit acceptance of the agent's offer, authorizes the agent acting as operator to run pnpm verify:perf; the user does not need to know the command name. After the audit, repair and rerun failed paths with focused checks, then run the complete matrix only once at the requested certification boundary.",
39
41
  performanceIterationDelivery:
40
- "The runtime request classifier is a tri-state guard, not a replacement for AI semantic judgment: high-confidence runtime complaints, responsiveness optimization, or excessive CPU, GPU, memory, or resource use classify as performance-iteration; high-confidence product motion, speed, freeze, terminology, or locally negated complaint commands classify as ordinary-product-work; ambiguous or unrecognized wording classifies as needs-agent-judgment and the AI decides from the actual user request. Ordinary work is rejected only for a high-confidence performance iteration. Every performance-iteration worklog intent quotes a nontrivial exact raw substring of Request; whitespace and Unicode code units must match before normalized tri-state classification. Invented, whitespace-collapsed, NFKC-equivalent, or mismatched evidence is rejected; high-confidence ordinary evidence is rejected; exact high-confidence performance-iteration or needs-agent-judgment evidence is accepted under agent ownership. Each performance iteration requires an exact reachable development fixture and fails with a configuration error instead of falling back to maximum. One complaint maps to one exact authority-selected targeted performance iteration over canonical affected path IDs. Direct and repeated performance complaints each start one bounded targeted iteration over affected paths. Complaint wording and repetition never select the complete performance matrix, create or refresh a durable baseline, or grant full-performance certification authority.",
42
+ "The runtime request classifier is a tri-state guard, not a replacement for AI semantic judgment: high-confidence runtime complaints, responsiveness optimization, or excessive CPU, GPU, memory, or resource use classify as performance-iteration; high-confidence product motion, speed, freeze, terminology, or locally negated complaint commands classify as ordinary-product-work; ambiguous or unrecognized wording classifies as needs-agent-judgment and the AI decides from the actual user request. Ordinary work is rejected only for a high-confidence performance iteration. Every performance-iteration worklog intent quotes a nontrivial exact raw substring of Request; whitespace and Unicode code units must match before normalized tri-state classification. Invented, whitespace-collapsed, NFKC-equivalent, or mismatched evidence is rejected; high-confidence ordinary evidence is rejected; exact high-confidence performance-iteration evidence, or needs-agent-judgment evidence after semantic resolution, is accepted under agent ownership. Classifier output establishes complaint authority only and never path localization. Each performance iteration requires an exact reachable development fixture and fails with a configuration error instead of falling back to maximum. Only a localized complaint or a post-clarification targeted choice creates performance-iteration intent and one bounded iteration over exact canonical affected paths. Regardless of whether the classifier returned high-confidence performance-iteration or needs-agent-judgment, unresolved localization creates neither performance-iteration intent nor canonical path authority. Each repeated localized request may create one new bounded targeted iteration over its affected paths; repetition alone never expands scope or selects the complete performance matrix, creates or refreshes a durable baseline, or grants full-performance certification authority.",
41
43
  functionalInitialDelivery:
42
44
  'A functional delivery plan with basis "initial" runs complete functional proof and no measured performance. It preserves any independent full-performance baseline and cannot claim targeted or full performance evidence.',
43
45
  functionalTargetedDelivery:
44
46
  'A functional delivery plan with basis "changed" runs exact affected ownership-derived functional proof and no measured performance. Functional delivery cannot silently invoke targeted performance, the full performance matrix, or create or refresh a performance baseline.',
45
47
  renderScaleFidelity:
46
- 'canvas.renderScale is visible functional fidelity. Raster products declare typed renderScaleCoverage "selected-backing-pixels" for interaction and steady, plus playback when timeline is enabled, and prove real canvas backing pixels in every declared state. Never downsample, stretch a lower-resolution backing canvas, blur output, or clamp the selected render scale to pass a performance budget.',
48
+ 'canvas.renderScale is visible functional fidelity. Raster products declare typed renderScaleCoverage "selected-backing-pixels" for interaction and steady, plus playback when timeline is enabled, and prove real canvas backing pixels in every declared state. Every measured path in a render-scale-enabled raster product proves actual CSS × devicePixelRatio × 2 backing after each measured phase. Never downsample, stretch a lower-resolution backing canvas, blur output, or clamp the selected render scale to pass a performance budget.',
47
49
  });
48
50
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_LIFECYCLE).toEqual({
49
51
  delivery: {
@@ -87,7 +89,7 @@ describe("Toolcraft canonical performance verification policy", () => {
87
89
  /ambiguous or unrecognized wording classifies as needs-agent-judgment/i,
88
90
  );
89
91
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
90
- /high-confidence performance-iteration or needs-agent-judgment evidence is accepted under agent ownership/i,
92
+ /exact high-confidence performance-iteration evidence, or needs-agent-judgment evidence after semantic resolution, is accepted under agent ownership/i,
91
93
  );
92
94
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
93
95
  /whitespace and Unicode code units must match/i,
@@ -96,13 +98,31 @@ describe("Toolcraft canonical performance verification policy", () => {
96
98
  /performance iteration requires an exact reachable development fixture/i,
97
99
  );
98
100
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
99
- /direct and repeated performance complaints each start one bounded targeted iteration/i,
101
+ /only a localized complaint or a post-clarification targeted choice creates performance-iteration intent and one bounded iteration over exact canonical affected paths/iu,
100
102
  );
101
103
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
102
- /one complaint maps to one exact authority-selected targeted performance iteration over canonical affected path IDs/i,
104
+ /regardless of whether the classifier returned high-confidence performance-iteration or needs-agent-judgment, unresolved localization creates neither performance-iteration intent nor canonical path authority/iu,
103
105
  );
104
106
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
105
- /complaint wording and repetition never select the complete performance matrix/i,
107
+ /each repeated localized request may create one new bounded targeted iteration/iu,
108
+ );
109
+ expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).not.toMatch(
110
+ /one complaint maps to one exact authority-selected targeted performance iteration/iu,
111
+ );
112
+ expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).not.toMatch(
113
+ /direct and repeated performance complaints each start one bounded targeted iteration/iu,
114
+ );
115
+ expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
116
+ /localized complaint[\s\S]*without asking the user/iu,
117
+ );
118
+ expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
119
+ /ambiguous or unresolved localization[\s\S]*one user-facing clarification[\s\S]*targeted diagnosis or a complete review/iu,
120
+ );
121
+ expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
122
+ /direct request for a complete review[\s\S]*without another clarification/iu,
123
+ );
124
+ expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
125
+ /repetition alone never expands scope or selects the complete performance matrix/i,
106
126
  );
107
127
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
108
128
  /two consecutive compatible protected performance iterations[\s\S]*must offer/i,
@@ -122,8 +142,41 @@ describe("Toolcraft canonical performance verification policy", () => {
122
142
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
123
143
  /typed renderScaleCoverage "selected-backing-pixels" for interaction and steady, plus playback when timeline is enabled/i,
124
144
  );
145
+ expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
146
+ /every measured path[\s\S]*devicePixelRatio × 2[\s\S]*each measured phase/iu,
147
+ );
125
148
  expect(TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT).toMatch(
126
149
  /never downsample[\s\S]*clamp the selected render scale to pass a performance budget/i,
127
150
  );
128
151
  });
152
+
153
+ it.each([
154
+ {
155
+ axis: "high-confidence but unlocalized complaint",
156
+ expected:
157
+ /classifier output establishes complaint authority only and never path localization[\s\S]*regardless of whether the classifier returned high-confidence performance-iteration[\s\S]*unresolved localization creates neither performance-iteration intent nor canonical path authority/iu,
158
+ },
159
+ {
160
+ axis: "localized complaint",
161
+ expected:
162
+ /localized complaint[\s\S]*one targeted iteration without asking the user/iu,
163
+ },
164
+ {
165
+ axis: "needs-agent-judgment with unresolved localization",
166
+ expected:
167
+ /regardless of whether the classifier returned[\s\S]*needs-agent-judgment[\s\S]*unresolved localization creates neither performance-iteration intent nor canonical path authority/iu,
168
+ },
169
+ {
170
+ axis: "post-clarification targeted choice",
171
+ expected:
172
+ /post-clarification targeted choice creates performance-iteration intent and one bounded iteration over exact canonical affected paths/iu,
173
+ },
174
+ ])("routes $axis without coupling authority to localization", ({ expected }) => {
175
+ const canonicalRoutingPolicy = [
176
+ TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY.complaintRouting,
177
+ TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY.performanceIterationDelivery,
178
+ ].join(" ");
179
+
180
+ expect(canonicalRoutingPolicy).toMatch(expected);
181
+ });
129
182
  });
@@ -3,6 +3,8 @@ export * from "./performance-request-classifier";
3
3
  export const TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY = {
4
4
  agentBrowser:
5
5
  "Agent-controlled browser checks are limited to diagnosis and targeted visual investigation; they do not mint durable performance baselines or receipts.",
6
+ complaintRouting:
7
+ "Performance authority and path localization are separate decisions. Classifier output establishes complaint authority only and never path localization. A localized complaint lets the agent select canonical affected paths and run one targeted iteration without asking the user. An ambiguous or unresolved localization requires one user-facing clarification naming visible operations and offering targeted diagnosis or a complete review; never ask the user for internal path ids. Regardless of whether the classifier returned high-confidence performance-iteration or needs-agent-judgment, unresolved localization creates neither performance-iteration intent nor canonical path authority. A broad or honestly unlocalizable problem may justify recommending the complete review immediately, but only the user's explicit request or acceptance authorizes it. A direct request for a complete review runs the full audit without another clarification.",
6
8
  deliveryBatch:
7
9
  "Classify and verify one coherent user-visible delivery batch instead of treating each internal implementation pass or steering correction as a separate aggregate verification boundary.",
8
10
  deliveryCommand:
@@ -14,13 +16,13 @@ export const TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY = {
14
16
  fullAuditDiscovery:
15
17
  "The complete performance matrix remains outside conversational delivery. Complaints, repetition, tier, filename, and touched subsystem never launch it automatically. After two consecutive compatible protected performance iterations, the agent must offer a slower complete audit if the user remains unsatisfied; a demonstrably broad or unlocalizable cross-system problem may justify the same offer earlier. An explicit natural-language request for the complete audit, or explicit acceptance of the agent's offer, authorizes the agent acting as operator to run pnpm verify:perf; the user does not need to know the command name. After the audit, repair and rerun failed paths with focused checks, then run the complete matrix only once at the requested certification boundary.",
16
18
  performanceIterationDelivery:
17
- "The runtime request classifier is a tri-state guard, not a replacement for AI semantic judgment: high-confidence runtime complaints, responsiveness optimization, or excessive CPU, GPU, memory, or resource use classify as performance-iteration; high-confidence product motion, speed, freeze, terminology, or locally negated complaint commands classify as ordinary-product-work; ambiguous or unrecognized wording classifies as needs-agent-judgment and the AI decides from the actual user request. Ordinary work is rejected only for a high-confidence performance iteration. Every performance-iteration worklog intent quotes a nontrivial exact raw substring of Request; whitespace and Unicode code units must match before normalized tri-state classification. Invented, whitespace-collapsed, NFKC-equivalent, or mismatched evidence is rejected; high-confidence ordinary evidence is rejected; exact high-confidence performance-iteration or needs-agent-judgment evidence is accepted under agent ownership. Each performance iteration requires an exact reachable development fixture and fails with a configuration error instead of falling back to maximum. One complaint maps to one exact authority-selected targeted performance iteration over canonical affected path IDs. Direct and repeated performance complaints each start one bounded targeted iteration over affected paths. Complaint wording and repetition never select the complete performance matrix, create or refresh a durable baseline, or grant full-performance certification authority.",
19
+ "The runtime request classifier is a tri-state guard, not a replacement for AI semantic judgment: high-confidence runtime complaints, responsiveness optimization, or excessive CPU, GPU, memory, or resource use classify as performance-iteration; high-confidence product motion, speed, freeze, terminology, or locally negated complaint commands classify as ordinary-product-work; ambiguous or unrecognized wording classifies as needs-agent-judgment and the AI decides from the actual user request. Ordinary work is rejected only for a high-confidence performance iteration. Every performance-iteration worklog intent quotes a nontrivial exact raw substring of Request; whitespace and Unicode code units must match before normalized tri-state classification. Invented, whitespace-collapsed, NFKC-equivalent, or mismatched evidence is rejected; high-confidence ordinary evidence is rejected; exact high-confidence performance-iteration evidence, or needs-agent-judgment evidence after semantic resolution, is accepted under agent ownership. Classifier output establishes complaint authority only and never path localization. Each performance iteration requires an exact reachable development fixture and fails with a configuration error instead of falling back to maximum. Only a localized complaint or a post-clarification targeted choice creates performance-iteration intent and one bounded iteration over exact canonical affected paths. Regardless of whether the classifier returned high-confidence performance-iteration or needs-agent-judgment, unresolved localization creates neither performance-iteration intent nor canonical path authority. Each repeated localized request may create one new bounded targeted iteration over its affected paths; repetition alone never expands scope or selects the complete performance matrix, creates or refreshes a durable baseline, or grants full-performance certification authority.",
18
20
  functionalInitialDelivery:
19
21
  'A functional delivery plan with basis "initial" runs complete functional proof and no measured performance. It preserves any independent full-performance baseline and cannot claim targeted or full performance evidence.',
20
22
  functionalTargetedDelivery:
21
23
  'A functional delivery plan with basis "changed" runs exact affected ownership-derived functional proof and no measured performance. Functional delivery cannot silently invoke targeted performance, the full performance matrix, or create or refresh a performance baseline.',
22
24
  renderScaleFidelity:
23
- 'canvas.renderScale is visible functional fidelity. Raster products declare typed renderScaleCoverage "selected-backing-pixels" for interaction and steady, plus playback when timeline is enabled, and prove real canvas backing pixels in every declared state. Never downsample, stretch a lower-resolution backing canvas, blur output, or clamp the selected render scale to pass a performance budget.',
25
+ 'canvas.renderScale is visible functional fidelity. Raster products declare typed renderScaleCoverage "selected-backing-pixels" for interaction and steady, plus playback when timeline is enabled, and prove real canvas backing pixels in every declared state. Every measured path in a render-scale-enabled raster product proves actual CSS × devicePixelRatio × 2 backing after each measured phase. Never downsample, stretch a lower-resolution backing canvas, blur output, or clamp the selected render scale to pass a performance budget.',
24
26
  } as const;
25
27
 
26
28
  export const TOOLCRAFT_PERFORMANCE_VERIFICATION_LIFECYCLE = {
@@ -37,6 +39,7 @@ export const TOOLCRAFT_PERFORMANCE_VERIFICATION_LIFECYCLE = {
37
39
 
38
40
  export const TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY_TEXT = [
39
41
  TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY.agentBrowser,
42
+ TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY.complaintRouting,
40
43
  TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY.deliveryBatch,
41
44
  TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY.deliveryCommand,
42
45
  TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY.demandOnlyPerformance,
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { downloadToolcraftArtifact } from "./artifact-download";
4
+
5
+ describe("downloadToolcraftArtifact", () => {
6
+ it("sanitizes the filename and revokes the object URL after the click", () => {
7
+ const events: string[] = [];
8
+ const anchor = {
9
+ click: vi.fn(() => events.push("click")),
10
+ download: "",
11
+ href: "",
12
+ remove: vi.fn(() => events.push("remove")),
13
+ };
14
+ const schedule = vi.fn((callback: () => void) => {
15
+ events.push("schedule-revoke");
16
+ callback();
17
+ return 1;
18
+ });
19
+
20
+ const result = downloadToolcraftArtifact({
21
+ appendAnchor: () => events.push("append"),
22
+ blob: new Blob(["png"], { type: "image/png" }),
23
+ createAnchor: () => anchor,
24
+ createObjectUrl: () => "blob:artifact",
25
+ extension: ".png",
26
+ rawBaseFileName: " Unsafe / name ",
27
+ revokeObjectUrl: () => events.push("revoke"),
28
+ schedule,
29
+ });
30
+
31
+ expect(result.fileName).toBe("Unsafe-name.png");
32
+ expect(anchor.download).toBe("Unsafe-name.png");
33
+ expect(events).toEqual([
34
+ "append",
35
+ "click",
36
+ "remove",
37
+ "schedule-revoke",
38
+ "revoke",
39
+ ]);
40
+ });
41
+
42
+ it("types click failures and still removes and revokes", () => {
43
+ const remove = vi.fn();
44
+ const revoke = vi.fn();
45
+
46
+ expect(() =>
47
+ downloadToolcraftArtifact({
48
+ appendAnchor: vi.fn(),
49
+ blob: new Blob(["png"]),
50
+ createAnchor: () => ({
51
+ click: () => {
52
+ throw new Error("blocked");
53
+ },
54
+ download: "",
55
+ href: "",
56
+ remove,
57
+ }),
58
+ createObjectUrl: () => "blob:artifact",
59
+ extension: ".png",
60
+ rawBaseFileName: "",
61
+ revokeObjectUrl: revoke,
62
+ schedule: (callback) => {
63
+ callback();
64
+ return 1;
65
+ },
66
+ }),
67
+ ).toThrow("could not start the artifact download");
68
+ expect(remove).toHaveBeenCalledOnce();
69
+ expect(revoke).toHaveBeenCalledWith("blob:artifact");
70
+ });
71
+ });
@@ -0,0 +1,80 @@
1
+ import {
2
+ normalizeToolcraftExportError,
3
+ type ToolcraftArtifactExportFailure,
4
+ } from "./export-error";
5
+
6
+ export type ToolcraftArtifactFileExtension =
7
+ | ".jpg"
8
+ | ".mp4"
9
+ | ".png"
10
+ | ".webm";
11
+
12
+ type ToolcraftDownloadAnchor = {
13
+ click: () => void;
14
+ download: string;
15
+ href: string;
16
+ remove: () => void;
17
+ };
18
+
19
+ export type ToolcraftArtifactDownloadRequest = Readonly<{
20
+ appendAnchor?: (anchor: ToolcraftDownloadAnchor) => void;
21
+ blob: Blob;
22
+ createAnchor?: () => ToolcraftDownloadAnchor;
23
+ createObjectUrl?: (blob: Blob) => string;
24
+ extension: ToolcraftArtifactFileExtension;
25
+ rawBaseFileName: string;
26
+ revokeObjectUrl?: (url: string) => void;
27
+ schedule?: (callback: () => void, delay: number) => unknown;
28
+ }>;
29
+
30
+ export type ToolcraftArtifactDownloadResult = Readonly<{ fileName: string }>;
31
+
32
+ export function sanitizeToolcraftArtifactBaseFileName(value: string): string {
33
+ const sanitized = value
34
+ .trim()
35
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
36
+ .replace(/^[.-]+|[.-]+$/g, "");
37
+
38
+ return sanitized || "toolcraft-export";
39
+ }
40
+
41
+ const downloadFailure: ToolcraftArtifactExportFailure = Object.freeze({
42
+ code: "artifact-download-failed",
43
+ message: "Toolcraft could not start the artifact download.",
44
+ });
45
+
46
+ export function downloadToolcraftArtifact(
47
+ request: ToolcraftArtifactDownloadRequest,
48
+ ): ToolcraftArtifactDownloadResult {
49
+ const createAnchor =
50
+ request.createAnchor ?? (() => document.createElement("a"));
51
+ const appendAnchor =
52
+ request.appendAnchor ??
53
+ ((anchor) => document.body.append(anchor as HTMLAnchorElement));
54
+ const createObjectUrl = request.createObjectUrl ?? URL.createObjectURL;
55
+ const revokeObjectUrl = request.revokeObjectUrl ?? URL.revokeObjectURL;
56
+ const schedule = request.schedule ?? globalThis.setTimeout;
57
+ const fileName = `${sanitizeToolcraftArtifactBaseFileName(
58
+ request.rawBaseFileName,
59
+ )}${request.extension}`;
60
+ let anchor: ToolcraftDownloadAnchor | null = null;
61
+ let objectUrl: string | null = null;
62
+
63
+ try {
64
+ objectUrl = createObjectUrl(request.blob);
65
+ anchor = createAnchor();
66
+ anchor.download = fileName;
67
+ anchor.href = objectUrl;
68
+ appendAnchor(anchor);
69
+ anchor.click();
70
+ return { fileName };
71
+ } catch (error) {
72
+ throw normalizeToolcraftExportError(error, downloadFailure);
73
+ } finally {
74
+ anchor?.remove();
75
+ if (objectUrl) {
76
+ const urlToRevoke = objectUrl;
77
+ schedule(() => revokeObjectUrl(urlToRevoke), 0);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,23 @@
1
+ import type { ToolcraftRendererPipelineClient } from "../rendering";
2
+ import type {
3
+ ToolcraftProductSceneBoundsProvider,
4
+ ToolcraftRuntimeSceneVisibility,
5
+ } from "../scene";
6
+ import type { ToolcraftState } from "../state/types";
7
+ import type { ToolcraftExportFrame } from "./export-frame";
8
+ import type { ToolcraftProductExportRenderer } from "./product-export-renderer";
9
+
10
+ export type ToolcraftArtifactExportRequest = Readonly<{
11
+ boundsProvider: ToolcraftProductSceneBoundsProvider | undefined;
12
+ exportRenderer: ToolcraftProductExportRenderer | undefined;
13
+ productSceneRequired: boolean;
14
+ renderRuntimeScene: (
15
+ canvas: HTMLCanvasElement,
16
+ frame: ToolcraftExportFrame,
17
+ state: ToolcraftState,
18
+ ) => Promise<unknown>;
19
+ rendererPipeline: ToolcraftRendererPipelineClient | null;
20
+ reportProgress: (progress: number) => void;
21
+ state: ToolcraftState;
22
+ visibility: ToolcraftRuntimeSceneVisibility;
23
+ }>;