@pixel-point/toolcraft 0.0.14 → 0.0.15

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 (577) hide show
  1. package/README.md +13 -2
  2. package/package.json +1 -1
  3. package/scripts/prepare-pack.mjs +2 -28
  4. package/src/cli-io.mjs +174 -0
  5. package/src/cli-package-manager-flow.test.mjs +206 -0
  6. package/src/cli.mjs +7 -495
  7. package/src/cli.test.mjs +34 -177
  8. package/src/create-command.mjs +152 -0
  9. package/src/create-options.mjs +194 -0
  10. package/src/generate-test-assertions.mjs +27 -0
  11. package/src/generate-test-doc-assertions.mjs +162 -0
  12. package/src/generate-test-file-assertions.mjs +251 -0
  13. package/src/generate-test-fs-utils.mjs +59 -0
  14. package/src/generate-test-integrity-assertions.mjs +30 -0
  15. package/src/generate-test-integrity-framework-assertions.mjs +222 -0
  16. package/src/generate-test-integrity-manifest-assertions.mjs +150 -0
  17. package/src/generate-test-integrity-ownership-assertions.mjs +127 -0
  18. package/src/generate-test-integrity-test-utils.mjs +102 -0
  19. package/src/generate-test-package-assertions.mjs +75 -0
  20. package/src/generate-test-product-fixture.mjs +84 -0
  21. package/src/generate-test-product-fixture.test.mjs +73 -0
  22. package/src/generate-test-standalone-assertions.mjs +209 -0
  23. package/src/generate-transactional.test.mjs +151 -0
  24. package/src/generate.mjs +182 -102
  25. package/src/generate.test.mjs +242 -338
  26. package/src/generated-integrity-manifest.mjs +127 -0
  27. package/src/generated-integrity-manifest.test.mjs +151 -0
  28. package/src/generated-source-policy.mjs +22 -0
  29. package/src/generated-source-policy.test.mjs +35 -0
  30. package/src/generated-target-policy.mjs +55 -0
  31. package/src/generation-transaction.mjs +135 -0
  32. package/src/generation-transaction.test.mjs +74 -0
  33. package/src/package-json.mjs +18 -1
  34. package/src/package-json.test.mjs +38 -5
  35. package/src/package-manager.mjs +2 -0
  36. package/src/package-manager.test.mjs +5 -1
  37. package/src/packaged-template-fallback.test.mjs +126 -0
  38. package/src/prepare-pack-templates.mjs +34 -0
  39. package/templates/runtime/contracts/component-contract-builders.ts +95 -0
  40. package/templates/runtime/contracts/component-contracts.choices.test.ts +293 -0
  41. package/templates/runtime/contracts/component-contracts.choices.ts +322 -0
  42. package/templates/runtime/contracts/component-contracts.inputs.test.ts +210 -0
  43. package/templates/runtime/contracts/component-contracts.inputs.ts +252 -0
  44. package/templates/runtime/contracts/component-contracts.media-custom.test.ts +116 -0
  45. package/templates/runtime/contracts/component-contracts.media-custom.ts +144 -0
  46. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +229 -0
  47. package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +76 -0
  48. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +135 -0
  49. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +148 -0
  50. package/templates/runtime/contracts/component-contracts.runtime.ts +361 -0
  51. package/templates/runtime/contracts/component-contracts.test.ts +39 -1458
  52. package/templates/runtime/contracts/component-contracts.ts +44 -1505
  53. package/templates/runtime/contracts/component-contracts.visual.test.ts +316 -0
  54. package/templates/runtime/contracts/component-contracts.visual.ts +385 -0
  55. package/templates/runtime/contracts/contract-manifest.test.ts +64 -0
  56. package/templates/runtime/contracts/decision-contracts.test.ts +29 -4
  57. package/templates/runtime/contracts/decision-contracts.ts +38 -1
  58. package/templates/runtime/index.ts +1 -0
  59. package/templates/runtime/react/{runtime-public-api.test.tsx → app-shell/runtime-public-api.test.tsx} +29 -7
  60. package/templates/runtime/react/{settings-transfer.test.ts → app-shell/settings-transfer.test.ts} +4 -4
  61. package/templates/runtime/react/{settings-transfer.ts → app-shell/settings-transfer.ts} +3 -3
  62. package/templates/runtime/react/{toolbar-panel.test.tsx → app-shell/toolbar-panel.test.tsx} +2 -2
  63. package/templates/runtime/react/{toolbar-panel.tsx → app-shell/toolbar-panel.tsx} +3 -3
  64. package/templates/runtime/react/{toolcraft-app.integration.test.tsx → app-shell/toolcraft-app.integration.test.tsx} +6 -6
  65. package/templates/runtime/react/{toolcraft-app.test.tsx → app-shell/toolcraft-app.test.tsx} +3 -3
  66. package/templates/runtime/react/{toolcraft-app.tsx → app-shell/toolcraft-app.tsx} +11 -8
  67. package/templates/runtime/react/{toolcraft-root.test.tsx → app-shell/toolcraft-root.test.tsx} +1 -1
  68. package/templates/runtime/react/{toolcraft-root.tsx → app-shell/toolcraft-root.tsx} +5 -5
  69. package/templates/runtime/react/{use-toolcraft.ts → app-shell/use-toolcraft.ts} +1 -1
  70. package/templates/runtime/react/canvas/canvas-default-media-layer.tsx +81 -0
  71. package/templates/runtime/react/canvas/canvas-media-transform.ts +33 -0
  72. package/templates/runtime/react/canvas/canvas-shell-interactions.test.tsx +156 -0
  73. package/templates/runtime/react/canvas/canvas-shell-media-rendering.test.tsx +87 -0
  74. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +102 -0
  75. package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +108 -0
  76. package/templates/runtime/react/canvas/canvas-shell-upload.test.tsx +106 -0
  77. package/templates/runtime/react/canvas/canvas-shell.tsx +146 -0
  78. package/templates/runtime/react/canvas/canvas-upload-routing.ts +84 -0
  79. package/templates/runtime/react/{media-file.ts → canvas/media-file.ts} +1 -1
  80. package/templates/runtime/react/canvas/use-canvas-drop-import.ts +145 -0
  81. package/templates/runtime/react/canvas/use-canvas-viewport-interactions.ts +227 -0
  82. package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +89 -0
  83. package/templates/runtime/react/{controls-panel-filedrop-reorder.test.tsx → controls-panel/__tests__/controls-panel-filedrop-reorder.test.tsx} +4 -4
  84. package/templates/runtime/react/controls-panel/__tests__/controls-panel.actions-layout.test.tsx +91 -0
  85. package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +213 -0
  86. package/templates/runtime/react/controls-panel/__tests__/controls-panel.color-layout.test.tsx +366 -0
  87. package/templates/runtime/react/controls-panel/__tests__/controls-panel.color-picker-models.test.tsx +169 -0
  88. package/templates/runtime/react/controls-panel/__tests__/controls-panel.compound-layout.test.tsx +160 -0
  89. package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +352 -0
  90. package/templates/runtime/react/controls-panel/__tests__/controls-panel.curves.test.tsx +226 -0
  91. package/templates/runtime/react/controls-panel/__tests__/controls-panel.floating-shell.test.tsx +13 -0
  92. package/templates/runtime/react/controls-panel/__tests__/controls-panel.font-picker.test.tsx +302 -0
  93. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +401 -0
  94. package/templates/runtime/react/controls-panel/__tests__/controls-panel.gradient-control.test.tsx +139 -0
  95. package/templates/runtime/react/controls-panel/__tests__/controls-panel.help-policy.test.tsx +103 -0
  96. package/templates/runtime/react/controls-panel/__tests__/controls-panel.image-picker.test.tsx +16 -0
  97. package/templates/runtime/react/controls-panel/__tests__/controls-panel.inline-layout.test.tsx +290 -0
  98. package/templates/runtime/react/controls-panel/__tests__/controls-panel.keyframes.test.tsx +175 -0
  99. package/templates/runtime/react/controls-panel/__tests__/controls-panel.media-upload-files.test.tsx +121 -0
  100. package/templates/runtime/react/controls-panel/__tests__/controls-panel.media-upload-image.test.tsx +363 -0
  101. package/templates/runtime/react/controls-panel/__tests__/controls-panel.media-upload-multi-image.test.tsx +105 -0
  102. package/templates/runtime/react/controls-panel/__tests__/controls-panel.range-slider-editing.test.tsx +149 -0
  103. package/templates/runtime/react/controls-panel/__tests__/controls-panel.section-shell.test.tsx +138 -0
  104. package/templates/runtime/react/controls-panel/__tests__/controls-panel.select-layout.test.tsx +148 -0
  105. package/templates/runtime/react/controls-panel/__tests__/controls-panel.setup-controls.test.tsx +189 -0
  106. package/templates/runtime/react/controls-panel/__tests__/controls-panel.slider-rendering.test.tsx +317 -0
  107. package/templates/runtime/react/controls-panel/__tests__/controls-panel.state-reset.test.tsx +137 -0
  108. package/templates/runtime/react/controls-panel/__tests__/controls-panel.structure.test.tsx +66 -0
  109. package/templates/runtime/react/controls-panel/__tests__/controls-panel.text-input.test.tsx +205 -0
  110. package/templates/runtime/react/controls-panel/__tests__/controls-panel.vector-interaction.test.tsx +388 -0
  111. package/templates/runtime/react/controls-panel/__tests__/controls-panel.vector-layout.test.tsx +155 -0
  112. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +172 -0
  113. package/templates/runtime/react/{control-conditions.ts → controls-panel/conditions/control-conditions.ts} +26 -3
  114. package/templates/runtime/react/{control-renderers.ts → controls-panel/control-renderers.ts} +2 -2
  115. package/templates/runtime/react/controls-panel/controls-panel.tsx +250 -0
  116. package/templates/runtime/react/controls-panel/keyframes/controls-panel-keyframes.tsx +265 -0
  117. package/templates/runtime/react/controls-panel/layout/controls-panel-collapse-storage.ts +131 -0
  118. package/templates/runtime/react/controls-panel/layout/controls-panel-help.tsx +132 -0
  119. package/templates/runtime/react/controls-panel/layout/controls-panel-inline-layout.tsx +311 -0
  120. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +216 -0
  121. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +401 -0
  122. package/templates/runtime/react/controls-panel/renderers/controls-panel-action-renderer.tsx +137 -0
  123. package/templates/runtime/react/controls-panel/renderers/controls-panel-basic-renderers.tsx +421 -0
  124. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +286 -0
  125. package/templates/runtime/react/controls-panel/renderers/controls-panel-compound-renderers.tsx +366 -0
  126. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +173 -0
  127. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +23 -0
  128. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +55 -0
  129. package/templates/runtime/react/controls-panel/renderers/controls-panel-settings-transfer-renderer.tsx +45 -0
  130. package/templates/runtime/react/controls-panel/testing/controls-panel-test-utils.tsx +300 -0
  131. package/templates/runtime/react/controls-panel/values/controls-panel-aspect-ratio-values.ts +94 -0
  132. package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +87 -0
  133. package/templates/runtime/react/controls-panel/values/controls-panel-color-values.ts +82 -0
  134. package/templates/runtime/react/controls-panel/values/controls-panel-font-values.ts +69 -0
  135. package/templates/runtime/react/controls-panel/values/controls-panel-spatial-values.ts +64 -0
  136. package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +93 -0
  137. package/templates/runtime/react/controls-panel/values/controls-panel-value-primitives.ts +30 -0
  138. package/templates/runtime/react/controls-panel/values/controls-panel-values.ts +7 -0
  139. package/templates/runtime/react/index.ts +13 -13
  140. package/templates/runtime/react/{layer-tree.ts → layers/layer-tree.ts} +1 -1
  141. package/templates/runtime/react/layers/layers-panel-drag-drop.test.tsx +190 -0
  142. package/templates/runtime/react/layers/layers-panel-editing.test.tsx +41 -0
  143. package/templates/runtime/react/layers/layers-panel-insert-targets.test.ts +87 -0
  144. package/templates/runtime/react/layers/layers-panel-insert-targets.ts +254 -0
  145. package/templates/runtime/react/layers/layers-panel-model.ts +15 -0
  146. package/templates/runtime/react/layers/layers-panel-rendering.test.tsx +43 -0
  147. package/templates/runtime/react/layers/layers-panel-reorder.ts +148 -0
  148. package/templates/runtime/react/layers/layers-panel-row.tsx +475 -0
  149. package/templates/runtime/react/layers/layers-panel-test-utils.tsx +135 -0
  150. package/templates/runtime/react/layers/layers-panel-visibility.test.tsx +126 -0
  151. package/templates/runtime/react/layers/layers-panel.tsx +349 -0
  152. package/templates/runtime/react/layers/use-layers-panel-drag-controller.ts +327 -0
  153. package/templates/runtime/react/{panel-host-types.ts → panel-host/panel-host-types.ts} +2 -2
  154. package/templates/runtime/react/{panel-host.test.tsx → panel-host/panel-host.test.tsx} +3 -3
  155. package/templates/runtime/react/{panel-host.tsx → panel-host/panel-host.tsx} +1 -1
  156. package/templates/runtime/react/timeline/timeline-easing-editor.tsx +409 -0
  157. package/templates/runtime/react/timeline/timeline-easing-geometry.ts +42 -0
  158. package/templates/runtime/react/timeline/timeline-easing-icons.tsx +135 -0
  159. package/templates/runtime/react/timeline/timeline-easing-model.test.ts +87 -0
  160. package/templates/runtime/react/timeline/timeline-easing-model.ts +271 -0
  161. package/templates/runtime/react/timeline/timeline-easing-popover-content.tsx +227 -0
  162. package/templates/runtime/react/timeline/timeline-easing-popover-layout.ts +69 -0
  163. package/templates/runtime/react/timeline/timeline-easing-popover.tsx +60 -0
  164. package/templates/runtime/react/timeline/timeline-event-targets.ts +19 -0
  165. package/templates/runtime/react/timeline/timeline-expanded-content.tsx +299 -0
  166. package/templates/runtime/react/timeline/timeline-icon-button.tsx +51 -0
  167. package/templates/runtime/react/timeline/timeline-keyframe-row.tsx +360 -0
  168. package/templates/runtime/react/timeline/timeline-keyframes.ts +23 -0
  169. package/templates/runtime/react/timeline/timeline-panel-floating-layout.test.tsx +328 -0
  170. package/templates/runtime/react/timeline/timeline-panel-header.tsx +404 -0
  171. package/templates/runtime/react/timeline/timeline-panel-keyframes.test.tsx +90 -0
  172. package/templates/runtime/react/timeline/timeline-panel-layout.ts +30 -0
  173. package/templates/runtime/react/timeline/timeline-panel-responsive-layout.ts +182 -0
  174. package/templates/runtime/react/timeline/timeline-panel-test-utils.tsx +88 -0
  175. package/templates/runtime/react/timeline/timeline-panel-transport.test.tsx +202 -0
  176. package/templates/runtime/react/timeline/timeline-panel-upload.test.tsx +53 -0
  177. package/templates/runtime/react/timeline/timeline-panel.tsx +449 -0
  178. package/templates/runtime/react/timeline/timeline-playback-hooks.ts +254 -0
  179. package/templates/runtime/schema/assembly-contract.ts +271 -0
  180. package/templates/runtime/schema/control-schema-normalization.ts +55 -0
  181. package/templates/runtime/schema/controls-panel-actions.ts +141 -0
  182. package/templates/runtime/schema/controls-panel-layout-groups.ts +144 -0
  183. package/templates/runtime/schema/controls-panel-normalization.ts +19 -0
  184. package/templates/runtime/schema/controls-panel-section-layout.ts +140 -0
  185. package/templates/runtime/schema/controls-panel-section-titles.ts +161 -0
  186. package/templates/runtime/schema/define-toolcraft-test-utils.ts +18 -0
  187. package/templates/runtime/schema/define-toolcraft.actions.test.ts +182 -0
  188. package/templates/runtime/schema/define-toolcraft.assembly.test.ts +246 -0
  189. package/templates/runtime/schema/define-toolcraft.canvas.test.ts +150 -0
  190. package/templates/runtime/schema/define-toolcraft.control-layout.test.ts +345 -0
  191. package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +304 -0
  192. package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +69 -0
  193. package/templates/runtime/schema/define-toolcraft.setup-persistence.test.ts +66 -0
  194. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +116 -0
  195. package/templates/runtime/schema/define-toolcraft.setup-settings-transfer.test.ts +291 -0
  196. package/templates/runtime/schema/define-toolcraft.ts +24 -1449
  197. package/templates/runtime/schema/panels-schema-normalization.ts +49 -0
  198. package/templates/runtime/schema/runtime-section-titles.ts +1 -0
  199. package/templates/runtime/schema/runtime-setup-section.ts +282 -0
  200. package/templates/runtime/schema/schema-resolvers.ts +168 -0
  201. package/templates/runtime/schema/types.ts +17 -0
  202. package/templates/runtime/state/canvas-reducer.ts +106 -0
  203. package/templates/runtime/state/canvas-state.ts +209 -0
  204. package/templates/runtime/state/controls-reducer.ts +277 -0
  205. package/templates/runtime/state/history-patches.ts +174 -0
  206. package/templates/runtime/state/layer-state.ts +12 -0
  207. package/templates/runtime/state/layers-reducer.ts +318 -0
  208. package/templates/runtime/state/media-reducer.ts +283 -0
  209. package/templates/runtime/state/media-state.ts +105 -0
  210. package/templates/runtime/state/panels-reducer.ts +58 -0
  211. package/templates/runtime/state/persistence-merge.ts +53 -0
  212. package/templates/runtime/state/persistence-reader-canvas.ts +27 -0
  213. package/templates/runtime/state/persistence-reader-layers.ts +73 -0
  214. package/templates/runtime/state/persistence-reader-media.ts +82 -0
  215. package/templates/runtime/state/persistence-reader-panels.ts +56 -0
  216. package/templates/runtime/state/persistence-reader-primitives.ts +27 -0
  217. package/templates/runtime/state/persistence-reader-timeline.ts +145 -0
  218. package/templates/runtime/state/persistence-reader-values.ts +40 -0
  219. package/templates/runtime/state/persistence-readers.ts +72 -0
  220. package/templates/runtime/state/persistence-shared.ts +23 -0
  221. package/templates/runtime/state/persistence-snapshot.ts +57 -0
  222. package/templates/runtime/state/persistence.ts +16 -581
  223. package/templates/runtime/state/reducer-canvas-panels.test.ts +324 -0
  224. package/templates/runtime/state/reducer-controls.test.ts +158 -0
  225. package/templates/runtime/state/reducer-layers.test.ts +306 -0
  226. package/templates/runtime/state/reducer-media-import-sizing.test.ts +197 -0
  227. package/templates/runtime/state/reducer-media-reorder-history.test.ts +116 -0
  228. package/templates/runtime/state/reducer-media-reset.test.ts +249 -0
  229. package/templates/runtime/state/reducer-media-test-utils.ts +40 -0
  230. package/templates/runtime/state/reducer-media-transform.test.ts +52 -0
  231. package/templates/runtime/state/reducer-test-utils.ts +30 -0
  232. package/templates/runtime/state/reducer-timeline.test.ts +242 -0
  233. package/templates/runtime/state/reducer.ts +57 -1785
  234. package/templates/runtime/state/timeline-reducer.ts +412 -0
  235. package/templates/runtime/state/timeline-values.test.ts +45 -0
  236. package/templates/runtime/state/timeline-values.ts +45 -0
  237. package/templates/runtime/testing/performance-browser-policy.test.ts +81 -0
  238. package/templates/runtime/testing/performance-browser-policy.ts +102 -0
  239. package/templates/runtime/testing/performance-budget-validation.ts +84 -0
  240. package/templates/runtime/testing/performance-control-classification.test.ts +106 -0
  241. package/templates/runtime/testing/performance-control-classification.ts +101 -0
  242. package/templates/runtime/testing/performance-coverage-context.ts +44 -0
  243. package/templates/runtime/testing/performance-coverage-renderer-policy.ts +221 -0
  244. package/templates/runtime/testing/performance-coverage-scenario-policy.ts +147 -0
  245. package/templates/runtime/testing/performance-coverage-target-policy.ts +121 -0
  246. package/templates/runtime/testing/performance-coverage-validator.ts +34 -0
  247. package/templates/runtime/testing/performance-export-coverage.test.ts +234 -0
  248. package/templates/runtime/testing/performance-fixture-load-profile.test.ts +360 -0
  249. package/templates/runtime/testing/performance-fixture-media.test.ts +80 -0
  250. package/templates/runtime/testing/performance-fixture-stress.test.ts +314 -0
  251. package/templates/runtime/testing/performance-fixture-validation.ts +255 -0
  252. package/templates/runtime/testing/performance-fixture-values.ts +275 -0
  253. package/templates/runtime/testing/performance-fixture-workload.test.ts +140 -0
  254. package/templates/runtime/testing/performance-load-profile-validation.ts +467 -0
  255. package/templates/runtime/testing/performance-renderer-coverage.test.ts +325 -0
  256. package/templates/runtime/testing/performance-renderer-pipeline-coverage.test.ts +115 -0
  257. package/templates/runtime/testing/performance-renderer-pipeline-interactions.test.ts +201 -0
  258. package/templates/runtime/testing/performance-renderer-pipeline-passes.test.ts +247 -0
  259. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +360 -0
  260. package/templates/runtime/testing/performance-renderer-technique-validation.ts +329 -0
  261. package/templates/runtime/testing/performance-renderer-technique.test.ts +275 -0
  262. package/templates/runtime/testing/performance-renderer-validation.ts +2 -0
  263. package/templates/runtime/testing/performance-scenario-coverage.test.ts +303 -0
  264. package/templates/runtime/testing/performance-scenario-validation.ts +49 -0
  265. package/templates/runtime/testing/performance-schema-derived-workloads.test.ts +231 -0
  266. package/templates/runtime/testing/performance-schema-queries.ts +181 -0
  267. package/templates/runtime/testing/performance-test-fixtures/index.ts +4 -0
  268. package/templates/runtime/testing/performance-test-fixtures/renderer-pipelines.ts +322 -0
  269. package/templates/runtime/testing/performance-test-fixtures/scenarios.ts +172 -0
  270. package/templates/runtime/testing/performance-test-fixtures/schemas.ts +151 -0
  271. package/templates/runtime/testing/performance-test-fixtures/stress-fixtures.ts +133 -0
  272. package/templates/runtime/testing/performance-test-fixtures.ts +1 -0
  273. package/templates/runtime/testing/performance-types.ts +338 -0
  274. package/templates/runtime/testing/performance.ts +15 -2485
  275. package/templates/starter/AGENTS.md +41 -26
  276. package/templates/starter/LICENSE.md +18 -95
  277. package/templates/starter/NOTICE.md +3 -3
  278. package/templates/starter/docs/toolcraft/README.md +5 -5
  279. package/templates/starter/docs/toolcraft/acceptance-testing.md +36 -8
  280. package/templates/starter/docs/toolcraft/assembly-workflow.md +12 -15
  281. package/templates/starter/docs/toolcraft/component-rules.md +6 -2
  282. package/templates/starter/docs/toolcraft/core/layout.md +1 -0
  283. package/templates/starter/docs/toolcraft/core/performance.md +7 -4
  284. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +11 -4
  285. package/templates/starter/docs/toolcraft/core/timeline-animation.md +1 -0
  286. package/templates/starter/docs/toolcraft/custom-controls.md +8 -1
  287. package/templates/starter/docs/toolcraft/decision-contract.md +26 -22
  288. package/templates/starter/docs/toolcraft/performance.md +42 -16
  289. package/templates/starter/docs/toolcraft/schema-reference.md +34 -23
  290. package/templates/starter/docs/toolcraft/workflow.md +25 -17
  291. package/templates/starter/e2e/app-browser-acceptance-evidence.spec.ts +111 -0
  292. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +118 -0
  293. package/templates/starter/e2e/app-browser-performance-evidence.spec.ts +217 -0
  294. package/templates/starter/e2e/app-browser-performance-probe.spec.ts +322 -0
  295. package/templates/starter/e2e/app-browser-proof-session.spec.ts +196 -0
  296. package/templates/starter/e2e/app-browser-runtime-evidence.spec.ts +9 -0
  297. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +240 -0
  298. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +180 -0
  299. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +310 -0
  300. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +432 -0
  301. package/templates/starter/e2e/app-browser-shell.spec.ts +171 -0
  302. package/templates/starter/e2e/app-browser-timeline-layers-reference.spec.ts +70 -0
  303. package/templates/starter/e2e/app-performance.spec.ts +0 -491
  304. package/templates/starter/e2e/browser-acceptance-outcome-helpers.ts +97 -0
  305. package/templates/starter/e2e/browser-acceptance-transition-helpers.ts +74 -0
  306. package/templates/starter/e2e/browser-conditional-output-evidence-helpers.ts +230 -0
  307. package/templates/starter/e2e/browser-control-target-helpers.ts +109 -0
  308. package/templates/starter/e2e/browser-layer-evidence-helpers.ts +229 -0
  309. package/templates/starter/e2e/browser-proof-session.ts +392 -0
  310. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +136 -0
  311. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +290 -0
  312. package/templates/starter/e2e/browser-runtime-evidence.ts +27 -0
  313. package/templates/starter/e2e/browser-state-evidence-helpers.ts +205 -0
  314. package/templates/starter/e2e/browser-timeline-evidence-helpers.ts +319 -0
  315. package/templates/starter/e2e/canvas-handle-helpers.ts +86 -4
  316. package/templates/starter/e2e/export-artifact-helpers.ts +132 -0
  317. package/templates/starter/e2e/performance-budget-helpers.ts +159 -0
  318. package/templates/starter/e2e/performance-canvas-helpers.ts +123 -0
  319. package/templates/starter/e2e/performance-control-helpers.ts +3 -0
  320. package/templates/starter/e2e/performance-control-layout-helpers.ts +161 -0
  321. package/templates/starter/e2e/performance-fixture-helpers.ts +217 -0
  322. package/templates/starter/e2e/performance-frame-probe.ts +247 -0
  323. package/templates/starter/e2e/performance-helpers.ts +6 -854
  324. package/templates/starter/e2e/performance-interaction-measurement.ts +251 -0
  325. package/templates/starter/e2e/performance-measurement-evidence.ts +72 -0
  326. package/templates/starter/e2e/performance-output-action-helpers.spec.ts +43 -0
  327. package/templates/starter/e2e/performance-output-action-helpers.ts +109 -0
  328. package/templates/starter/e2e/performance-probe-helpers.ts +12 -0
  329. package/templates/starter/e2e/performance-render-scale-helpers.ts +55 -0
  330. package/templates/starter/e2e/performance-slider-helpers.ts +271 -0
  331. package/templates/starter/e2e/product-observable-helpers.ts +72 -17
  332. package/templates/starter/e2e/stable-outcome-helpers.ts +208 -0
  333. package/templates/starter/package.json +10 -5
  334. package/templates/starter/playwright.config.ts +5 -2
  335. package/templates/starter/scripts/check-ai-skills.mjs +14 -8
  336. package/templates/starter/scripts/check-ai-skills.test.mjs +64 -0
  337. package/templates/starter/scripts/check-toolcraft-code-health.mjs +108 -0
  338. package/templates/starter/scripts/check-toolcraft-code-health.test.mjs +213 -0
  339. package/templates/starter/scripts/check-toolcraft-docs.mjs +47 -34
  340. package/templates/starter/scripts/check-toolcraft-integrity.mjs +141 -144
  341. package/templates/starter/scripts/run-browser-performance.mjs +118 -0
  342. package/templates/starter/scripts/run-browser-performance.test.mjs +53 -0
  343. package/templates/starter/scripts/toolcraft-code-health-core.mjs +297 -0
  344. package/templates/starter/scripts/toolcraft-contract-doc-integrity.mjs +77 -0
  345. package/templates/starter/scripts/toolcraft-contract-doc-integrity.test.mjs +81 -0
  346. package/templates/starter/scripts/toolcraft-contract-manifest.json +113 -0
  347. package/templates/starter/scripts/toolcraft-contract-manifest.mjs +128 -0
  348. package/templates/starter/scripts/toolcraft-contract-manifest.test.mjs +72 -0
  349. package/templates/starter/scripts/toolcraft-integrity-manifest.mjs +172 -0
  350. package/templates/starter/scripts/toolcraft-integrity-policy.mjs +60 -0
  351. package/templates/starter/scripts/toolcraft-product-boundary-ast-utils.mjs +23 -0
  352. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +264 -0
  353. package/templates/starter/scripts/toolcraft-product-boundary-test-fixtures.mjs +16 -0
  354. package/templates/starter/scripts/toolcraft-product-boundary.mjs +243 -0
  355. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +289 -0
  356. package/templates/starter/scripts/toolcraft-product-dependency-graph.mjs +176 -0
  357. package/templates/starter/scripts/toolcraft-product-dependency-graph.test.mjs +196 -0
  358. package/templates/starter/scripts/toolcraft-product-dependency-resolution.mjs +279 -0
  359. package/templates/starter/scripts/toolcraft-product-evidence-boundary-ast.mjs +91 -0
  360. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +138 -0
  361. package/templates/starter/scripts/toolcraft-product-style-boundary.mjs +219 -0
  362. package/templates/starter/scripts/toolcraft-source-inventory.d.mts +46 -0
  363. package/templates/starter/scripts/toolcraft-source-inventory.mjs +187 -0
  364. package/templates/starter/scripts/toolcraft-source-inventory.test.mjs +132 -0
  365. package/templates/starter/scripts/toolcraft-source-ownership.mjs +167 -0
  366. package/templates/starter/scripts/toolcraft-source-ownership.test.mjs +113 -0
  367. package/templates/starter/scripts/toolcraft-static-string.mjs +104 -0
  368. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +123 -0
  369. package/templates/starter/scripts/toolcraft-verification-receipt.d.mts +19 -0
  370. package/templates/starter/scripts/toolcraft-verification-receipt.mjs +284 -0
  371. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +282 -0
  372. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.d.mts +32 -0
  373. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.mjs +88 -0
  374. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.test.mjs +87 -0
  375. package/templates/starter/scripts/toolcraft-vitest-runtime-evidence-reporter.mjs +109 -0
  376. package/templates/starter/scripts/toolcraft-vitest-runtime-evidence-reporter.test.mjs +111 -0
  377. package/templates/starter/scripts/toolcraft-workflow-routes.mjs +314 -0
  378. package/templates/starter/scripts/toolcraft-workflow-routes.test.mjs +204 -0
  379. package/templates/starter/src/app/acceptance/actions.ts +30 -0
  380. package/templates/starter/src/app/acceptance/animation-intent.ts +184 -0
  381. package/templates/starter/src/app/acceptance/canvas-handle-acceptance.ts +70 -0
  382. package/templates/starter/src/app/acceptance/color-bank-labels.ts +164 -0
  383. package/templates/starter/src/app/acceptance/conditions.ts +80 -0
  384. package/templates/starter/src/app/acceptance/control-acceptance-context.ts +70 -0
  385. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +271 -0
  386. package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +149 -0
  387. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +189 -0
  388. package/templates/starter/src/app/acceptance/control-acceptance.ts +70 -0
  389. package/templates/starter/src/app/acceptance/control-component-rules.ts +277 -0
  390. package/templates/starter/src/app/acceptance/control-labels.ts +223 -0
  391. package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +107 -0
  392. package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +104 -0
  393. package/templates/starter/src/app/acceptance/control-layout-model.ts +159 -0
  394. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +164 -0
  395. package/templates/starter/src/app/acceptance/control-layout.ts +45 -0
  396. package/templates/starter/src/app/acceptance/control-order.ts +123 -0
  397. package/templates/starter/src/app/acceptance/control-parts.ts +47 -0
  398. package/templates/starter/src/app/acceptance/control-section-inventory.ts +183 -0
  399. package/templates/starter/src/app/acceptance/controls.ts +27 -0
  400. package/templates/starter/src/app/acceptance/coverage.ts +81 -0
  401. package/templates/starter/src/app/acceptance/custom-controls.ts +140 -0
  402. package/templates/starter/src/app/acceptance/inline-layout.ts +192 -0
  403. package/templates/starter/src/app/acceptance/media-upload.ts +55 -0
  404. package/templates/starter/src/app/acceptance/output-background-rules.ts +135 -0
  405. package/templates/starter/src/app/acceptance/output-export-actions.test.ts +76 -0
  406. package/templates/starter/src/app/acceptance/output-export-actions.ts +34 -0
  407. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +46 -0
  408. package/templates/starter/src/app/acceptance/output-export-model.ts +146 -0
  409. package/templates/starter/src/app/acceptance/output-export.ts +45 -0
  410. package/templates/starter/src/app/acceptance/output-image-export-rules.ts +87 -0
  411. package/templates/starter/src/app/acceptance/reference-runtime-coverage.ts +96 -0
  412. package/templates/starter/src/app/acceptance/reference-runtime-feature-inventory.ts +171 -0
  413. package/templates/starter/src/app/acceptance/reference-runtime-study.ts +73 -0
  414. package/templates/starter/src/app/acceptance/reference-runtime-timeline.ts +291 -0
  415. package/templates/starter/src/app/acceptance/reference-runtime.ts +99 -0
  416. package/templates/starter/src/app/acceptance/runtime-coverage.ts +290 -0
  417. package/templates/starter/src/app/acceptance/runtime-setup.ts +117 -0
  418. package/templates/starter/src/app/acceptance/section-title-rules.ts +8 -0
  419. package/templates/starter/src/app/acceptance/sections.ts +6 -0
  420. package/templates/starter/src/app/acceptance/semantic.ts +63 -0
  421. package/templates/starter/src/app/acceptance/timeline-loop.ts +21 -0
  422. package/templates/starter/src/app/acceptance/types.ts +363 -0
  423. package/templates/starter/src/app/acceptance/validate-coverage.ts +217 -0
  424. package/templates/starter/src/app/acceptance/validation-pipeline.test.ts +56 -0
  425. package/templates/starter/src/app/acceptance/validation-pipeline.ts +60 -0
  426. package/templates/starter/src/app/acceptance/video-reference-study.ts +217 -0
  427. package/templates/starter/src/app/starter-acceptance-data.ts +21 -0
  428. package/templates/starter/src/app/starter-acceptance.animation-intent.test.ts +383 -0
  429. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +349 -0
  430. package/templates/starter/src/app/starter-acceptance.base-coverage.test.ts +47 -0
  431. package/templates/starter/src/app/starter-acceptance.canvas-handles.test.ts +71 -0
  432. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +199 -0
  433. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +244 -0
  434. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +63 -0
  435. package/templates/starter/src/app/starter-acceptance.control-label-context.test.ts +172 -0
  436. package/templates/starter/src/app/starter-acceptance.control-labels.test.ts +387 -0
  437. package/templates/starter/src/app/starter-acceptance.control-naming-rules.test.ts +215 -0
  438. package/templates/starter/src/app/starter-acceptance.control-order.test.ts +274 -0
  439. package/templates/starter/src/app/starter-acceptance.control-state.test.ts +275 -0
  440. package/templates/starter/src/app/starter-acceptance.curves-rules.test.ts +184 -0
  441. package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +200 -0
  442. package/templates/starter/src/app/starter-acceptance.custom-control-coverage.test.ts +111 -0
  443. package/templates/starter/src/app/starter-acceptance.custom-control-fit-check.test.ts +135 -0
  444. package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +74 -0
  445. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +107 -0
  446. package/templates/starter/src/app/starter-acceptance.framework-boundary.test.ts +327 -0
  447. package/templates/starter/src/app/starter-acceptance.image-video-export-settings.test.ts +328 -0
  448. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +242 -0
  449. package/templates/starter/src/app/starter-acceptance.layers.test.ts +203 -0
  450. package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +110 -0
  451. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +153 -0
  452. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +97 -0
  453. package/templates/starter/src/app/starter-acceptance.persistence-coverage.test.ts +134 -0
  454. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +202 -0
  455. package/templates/starter/src/app/starter-acceptance.product-readiness-rules.test.ts +14 -0
  456. package/templates/starter/src/app/starter-acceptance.product-readiness-test-utils.ts +25 -0
  457. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +50 -0
  458. package/templates/starter/src/app/starter-acceptance.product-worklog.test.ts +27 -0
  459. package/templates/starter/src/app/starter-acceptance.range-slider-rules.test.ts +147 -0
  460. package/templates/starter/src/app/starter-acceptance.reference-custom-timeline.test.ts +197 -0
  461. package/templates/starter/src/app/starter-acceptance.reference-foundation.test.ts +304 -0
  462. package/templates/starter/src/app/starter-acceptance.reference-test-utils.ts +102 -0
  463. package/templates/starter/src/app/starter-acceptance.reference-timeline.test.ts +267 -0
  464. package/templates/starter/src/app/starter-acceptance.schema-test-utils.ts +116 -0
  465. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +373 -0
  466. package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +260 -0
  467. package/templates/starter/src/app/starter-acceptance.section-titles.test.ts +180 -0
  468. package/templates/starter/src/app/starter-acceptance.setup-readiness.test.ts +205 -0
  469. package/templates/starter/src/app/starter-acceptance.slider-rules.test.ts +470 -0
  470. package/templates/starter/src/app/starter-acceptance.slider-unit-rules.test.ts +61 -0
  471. package/templates/starter/src/app/starter-acceptance.source-test-utils.ts +43 -0
  472. package/templates/starter/src/app/starter-acceptance.test-utils.ts +21 -0
  473. package/templates/starter/src/app/starter-acceptance.text-control-kind.test.ts +89 -0
  474. package/templates/starter/src/app/starter-acceptance.timeline-playback.test.ts +264 -0
  475. package/templates/starter/src/app/starter-acceptance.timeline-test-utils.ts +56 -0
  476. package/templates/starter/src/app/starter-acceptance.toggle-pair-layout.test.ts +200 -0
  477. package/templates/starter/src/app/starter-acceptance.toggle-parameter-layout.test.ts +277 -0
  478. package/templates/starter/src/app/starter-acceptance.ts +151 -4576
  479. package/templates/starter/src/app/starter-acceptance.video-export-test-utils.ts +37 -0
  480. package/templates/starter/src/app/starter-acceptance.video-reference-study.test.ts +262 -0
  481. package/templates/starter/src/app/starter-acceptance.worklog-test-utils.ts +428 -0
  482. package/templates/starter/src/app/starter-acceptance.worklog.test.ts +283 -0
  483. package/templates/starter/src/app/starter-automated-runtime-evidence.test.ts +34 -0
  484. package/templates/starter/src/app/starter-composition.tsx +7 -0
  485. package/templates/starter/src/app/starter-performance-test-utils.ts +275 -0
  486. package/templates/starter/src/app/starter-performance.budgets.test.ts +374 -0
  487. package/templates/starter/src/app/starter-performance.gates.test.ts +128 -0
  488. package/templates/starter/src/app/starter-performance.renderer-source.test.ts +136 -0
  489. package/templates/starter/src/app/starter-performance.renderer-technique.test.ts +291 -0
  490. package/templates/starter/src/app/starter-performance.scenarios.test.ts +355 -0
  491. package/templates/starter/src/app/test-evidence/browser-runtime-contract.test.ts +253 -0
  492. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +243 -0
  493. package/templates/starter/src/routes/index.tsx +11 -2
  494. package/templates/starter/vite.config.ts +8 -0
  495. package/templates/ui/components/controls/color/color-control-types.ts +38 -0
  496. package/templates/ui/components/controls/color/color-control.tsx +19 -437
  497. package/templates/ui/components/controls/color/color-footer.tsx +272 -0
  498. package/templates/ui/components/controls/color/color-model-slider.tsx +271 -0
  499. package/templates/ui/components/controls/color/color-opacity-input.tsx +94 -0
  500. package/templates/ui/components/controls/color/color-surface.tsx +194 -0
  501. package/templates/ui/components/controls/color/color-value-control.tsx +159 -0
  502. package/templates/ui/components/controls/color/color-value-utils.ts +170 -0
  503. package/templates/ui/components/controls/color/index.ts +10 -3
  504. package/templates/ui/components/controls/color/palette-control-controller.ts +407 -0
  505. package/templates/ui/components/controls/color/palette-control-layout.ts +14 -0
  506. package/templates/ui/components/controls/color/palette-control-types.ts +50 -0
  507. package/templates/ui/components/controls/color/palette-control-view.tsx +174 -0
  508. package/templates/ui/components/controls/color/palette-control.tsx +6 -544
  509. package/templates/ui/components/controls/color/style-guide-color-picker-color-utils.ts +39 -0
  510. package/templates/ui/components/controls/color/style-guide-color-picker-controller.ts +370 -0
  511. package/templates/ui/components/controls/color/style-guide-color-picker-hue-handlers.ts +66 -0
  512. package/templates/ui/components/controls/color/style-guide-color-picker-interaction-state.ts +49 -0
  513. package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +2 -2
  514. package/templates/ui/components/controls/color/style-guide-color-picker-model.ts +125 -0
  515. package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +10 -707
  516. package/templates/ui/components/controls/color/style-guide-color-picker-surface-drag.ts +93 -0
  517. package/templates/ui/components/controls/color/style-guide-color-picker-surface-geometry.ts +114 -0
  518. package/templates/ui/components/controls/color/style-guide-color-picker-surface-preview.ts +45 -0
  519. package/templates/ui/components/controls/color/style-guide-color-picker-types.ts +84 -0
  520. package/templates/ui/components/controls/color/style-guide-color-picker-view.tsx +69 -0
  521. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +6 -495
  522. package/templates/ui/components/controls/curves/curves-control.tsx +4 -3
  523. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +39 -515
  524. package/templates/ui/components/controls/file-drop/file-drop-empty-state.tsx +35 -0
  525. package/templates/ui/components/controls/file-drop/file-drop-file-list.tsx +160 -0
  526. package/templates/ui/components/controls/file-drop/file-drop-image-grid.tsx +175 -0
  527. package/templates/ui/components/controls/file-drop/file-drop-model.tsx +170 -0
  528. package/templates/ui/components/controls/file-drop/file-drop-single-preview.tsx +63 -0
  529. package/templates/ui/components/controls/file-drop/file-drop-types.ts +48 -0
  530. package/templates/ui/components/controls/file-drop/index.ts +2 -2
  531. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +133 -906
  532. package/templates/ui/components/controls/font-picker/font-picker-footer.tsx +116 -0
  533. package/templates/ui/components/controls/font-picker/font-picker-list.tsx +194 -0
  534. package/templates/ui/components/controls/font-picker/font-picker-popover-content.tsx +191 -0
  535. package/templates/ui/components/controls/font-picker/font-picker-value.ts +222 -0
  536. package/templates/ui/components/controls/font-picker/index.ts +4 -1
  537. package/templates/ui/components/controls/font-picker/use-font-picker-preview-pipeline.ts +163 -0
  538. package/templates/ui/components/controls/font-picker/use-font-picker-virtual-list.ts +293 -0
  539. package/templates/ui/components/controls/gradient/gradient-control-utils.ts +2 -25
  540. package/templates/ui/components/controls/gradient/gradient-control.tsx +4 -464
  541. package/templates/ui/components/controls/gradient/gradient-stops-controller.ts +332 -0
  542. package/templates/ui/components/controls/gradient/gradient-stops-track.tsx +159 -0
  543. package/templates/ui/components/controls/vector/vector-control-types.ts +26 -0
  544. package/templates/ui/components/controls/vector/vector-control.tsx +10 -486
  545. package/templates/ui/components/controls/vector/vector-pad-field.tsx +211 -0
  546. package/templates/ui/components/controls/vector/vector-pad-geometry.ts +61 -0
  547. package/templates/ui/components/controls/vector/vector-pad-parts.tsx +72 -0
  548. package/templates/ui/components/controls/vector/vector-pad-variants.ts +20 -0
  549. package/templates/ui/components/controls/vector/vector-size-field.tsx +104 -0
  550. package/templates/ui/components/controls/vector/vector-value.ts +46 -0
  551. package/templates/ui/components/primitives/slider/slider-discrete-state.ts +181 -0
  552. package/templates/ui/components/primitives/slider/slider-marker-policy.ts +52 -0
  553. package/templates/ui/components/primitives/slider/slider-pointer-dragging.ts +87 -0
  554. package/templates/ui/components/primitives/slider/slider-runtime.ts +109 -0
  555. package/templates/ui/components/primitives/slider/slider-types.ts +21 -0
  556. package/templates/ui/components/primitives/slider/slider.tsx +2 -403
  557. package/templates/runtime/react/canvas-shell.test.tsx +0 -554
  558. package/templates/runtime/react/canvas-shell.tsx +0 -585
  559. package/templates/runtime/react/controls-panel.test.tsx +0 -5446
  560. package/templates/runtime/react/controls-panel.tsx +0 -3220
  561. package/templates/runtime/react/layers-panel.test.tsx +0 -487
  562. package/templates/runtime/react/layers-panel.tsx +0 -1348
  563. package/templates/runtime/react/timeline-panel.test.tsx +0 -751
  564. package/templates/runtime/react/timeline-panel.tsx +0 -3078
  565. package/templates/runtime/schema/define-toolcraft.test.ts +0 -1742
  566. package/templates/runtime/state/reducer.test.ts +0 -1703
  567. package/templates/runtime/testing/performance.test.ts +0 -3081
  568. package/templates/starter/e2e/app-browser-acceptance.spec.ts +0 -785
  569. package/templates/starter/src/app/starter-acceptance.test.ts +0 -10068
  570. package/templates/starter/src/app/starter-performance.test.ts +0 -1583
  571. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +0 -490
  572. /package/templates/runtime/react/{storage-key-migration.ts → app-shell/storage-key-migration.ts} +0 -0
  573. /package/templates/runtime/react/{theme-runtime.tsx → app-shell/theme-runtime.tsx} +0 -0
  574. /package/templates/runtime/react/{curve-geometry.test.ts → controls-panel/__tests__/curve-geometry.test.ts} +0 -0
  575. /package/templates/runtime/react/{panel-host-config.ts → panel-host/panel-host-config.ts} +0 -0
  576. /package/templates/runtime/react/{panel-host-geometry.test.ts → panel-host/panel-host-geometry.test.ts} +0 -0
  577. /package/templates/runtime/react/{panel-host-geometry.ts → panel-host/panel-host-geometry.ts} +0 -0
@@ -1,2485 +1,15 @@
1
- import type {
2
- ToolcraftControlSchema,
3
- ResolvedToolcraftAppSchema,
4
- } from "../schema/types";
5
- import { isToolcraftRuntimeOwnedTarget } from "../schema/runtime-targets";
6
-
7
- export type ToolcraftPerformanceInteraction =
8
- | "animation-frame"
9
- | "animation-viewport-drag"
10
- | "control-change"
11
- | "control-drag"
12
- | "export-copy"
13
- | "mask-drag"
14
- | "media-import"
15
- | "preview-render"
16
- | "timeline-playback"
17
- | "timeline-scrub"
18
- | "viewport-zoom-stress"
19
- | "viewport-stability";
20
-
21
- export type ToolcraftPerformanceCoverage = {
22
- automated: boolean;
23
- automatedTestName: string;
24
- browser: boolean;
25
- browserTestName: string;
26
- };
27
-
28
- export type ToolcraftBrowserCheckPreferredRunner = "agent-browser";
29
-
30
- export type ToolcraftBrowserCheckFallbackRunner = "playwright";
31
-
32
- export type ToolcraftBrowserCheckFallbackCondition =
33
- | "agent-browser-unavailable"
34
- | "ci";
35
-
36
- export type ToolcraftBrowserCheckPolicy = {
37
- fallbackRunner: ToolcraftBrowserCheckFallbackRunner;
38
- fallbackWhen: readonly ToolcraftBrowserCheckFallbackCondition[];
39
- preferredRunner: ToolcraftBrowserCheckPreferredRunner;
40
- };
41
-
42
- export type ToolcraftPerformanceBudget = {
43
- maxExportMs?: number;
44
- maxFrameGapMs?: number;
45
- maxInteractionMs?: number;
46
- maxLongTaskMs?: number;
47
- maxPreviewMs?: number;
48
- maxRenderMs?: number;
49
- };
50
-
51
- export type ToolcraftPerformanceValueSet = {
52
- default: unknown;
53
- max?: unknown;
54
- min?: unknown;
55
- };
56
-
57
- export type ToolcraftPerformanceFixtureKind =
58
- | "custom"
59
- | "high-density"
60
- | "large-canvas"
61
- | "large-text"
62
- | "many-items"
63
- | "max-value"
64
- | "media";
65
-
66
- export type ToolcraftPerformanceLoadMetric =
67
- | "canvas-area"
68
- | "count"
69
- | "custom"
70
- | "media-area"
71
- | "numeric-max"
72
- | "numeric-min"
73
- | "text-length";
74
-
75
- export type ToolcraftPerformanceUserFacingRange =
76
- | "experimental-above-smooth"
77
- | "fully-guaranteed";
78
-
79
- export type ToolcraftPerformanceLoadEvidence = {
80
- attemptedTarget: unknown;
81
- decision: string;
82
- measuredResult: string;
83
- optimizationAttempted?: string;
84
- result: "failed" | "passed";
85
- scenarioId: string;
86
- };
87
-
88
- export type ToolcraftPerformanceLoadProfile = {
89
- degradationStepPercent?: 10;
90
- evidence?: readonly ToolcraftPerformanceLoadEvidence[];
91
- hardLimit: unknown;
92
- metric: ToolcraftPerformanceLoadMetric;
93
- smoothTarget: unknown;
94
- smoothTargetRatio: number;
95
- target: string;
96
- userFacingRange: ToolcraftPerformanceUserFacingRange;
97
- };
98
-
99
- export type ToolcraftPerformanceFixture = {
100
- kind: ToolcraftPerformanceFixtureKind;
101
- loadProfile?: ToolcraftPerformanceLoadProfile;
102
- minChars?: number;
103
- minCount?: number;
104
- minLines?: number;
105
- reason: string;
106
- value?: unknown;
107
- };
108
-
109
- export type ToolcraftPerformanceStressFixtureKind = ToolcraftPerformanceFixtureKind;
110
- export type ToolcraftPerformanceStressFixture = ToolcraftPerformanceFixture;
111
- export type ToolcraftPerformanceWorkloadFixture = ToolcraftPerformanceFixture;
112
-
113
- export type ToolcraftPerformanceScenario = ToolcraftPerformanceCoverage & {
114
- budget: ToolcraftPerformanceBudget;
115
- controlLabel?: string;
116
- expectedObservable: string;
117
- fixture: string;
118
- id: string;
119
- interaction: ToolcraftPerformanceInteraction;
120
- target?: string;
121
- stress?: boolean;
122
- stressFixture?: ToolcraftPerformanceStressFixture;
123
- workloadFixture?: ToolcraftPerformanceWorkloadFixture;
124
- uiSelector?: string;
125
- values?: ToolcraftPerformanceValueSet;
126
- workload: boolean;
127
- };
128
-
129
- export type ToolcraftRendererStrategy =
130
- | "none"
131
- | "dom"
132
- | "svg"
133
- | "canvas-2d"
134
- | "webgl"
135
- | "webgpu";
136
-
137
- export type ToolcraftRendererWorkload =
138
- | "none"
139
- | "simple-composition"
140
- | "text-output"
141
- | "vector-output"
142
- | "pixel-output";
143
-
144
- export type ToolcraftSourceRepresentation =
145
- | "reference-runtime"
146
- | "dom-text"
147
- | "svg"
148
- | "canvas-2d"
149
- | "webgl-texture"
150
- | "webgpu-texture"
151
- | "image-media"
152
- | "video-media"
153
- | "procedural-data"
154
- | "mixed";
155
-
156
- export type ToolcraftProductRepresentation =
157
- | "text"
158
- | "vector"
159
- | "pixel"
160
- | "video"
161
- | "mixed";
162
-
163
- export type ToolcraftPreviewRenderer =
164
- | "dom"
165
- | "svg"
166
- | "canvas-2d"
167
- | "webgl"
168
- | "webgpu";
169
-
170
- export type ToolcraftExportRenderer =
171
- | "none"
172
- | "dom"
173
- | "svg"
174
- | "canvas-2d"
175
- | "webgl"
176
- | "webgpu"
177
- | "media-recorder"
178
- | "webcodecs";
179
-
180
- export type ToolcraftRendererLayerKind =
181
- | "background"
182
- | "product-foreground"
183
- | "editing-handles"
184
- | "export-composite";
185
-
186
- export type ToolcraftRendererLayerContent =
187
- | "bitmap-media"
188
- | "composite"
189
- | "dense-pattern"
190
- | "geometry"
191
- | "handles"
192
- | "noise"
193
- | "shader"
194
- | "text";
195
-
196
- export type ToolcraftRendererLayerPrimitiveCount = "low" | "medium" | "high";
197
-
198
- export type ToolcraftRendererLayerExportMode =
199
- | "included"
200
- | "excluded"
201
- | "composited";
202
-
203
- export type ToolcraftRendererLayer = {
204
- content: readonly ToolcraftRendererLayerContent[];
205
- exportMode: ToolcraftRendererLayerExportMode;
206
- id: string;
207
- intentionalRasterizationReason?: string;
208
- kind: ToolcraftRendererLayerKind;
209
- primitiveCount: ToolcraftRendererLayerPrimitiveCount;
210
- renderer: Exclude<ToolcraftRendererStrategy, "none">;
211
- uiSelector?: string;
212
- };
213
-
214
- export type ToolcraftRendererMeasuredAlternativeEvidence = {
215
- alternativeStrategy: "webgl" | "webgpu";
216
- decision: string;
217
- fixture: string;
218
- measuredResult: string;
219
- scenarioId: string;
220
- };
221
-
222
- export type ToolcraftRendererTechnique = {
223
- exportRenderer: ToolcraftExportRenderer;
224
- fidelityRisks: readonly string[];
225
- intentionalRasterizationReason?: string;
226
- layers?: readonly ToolcraftRendererLayer[];
227
- measuredAlternativeEvidence?: readonly ToolcraftRendererMeasuredAlternativeEvidence[];
228
- performanceRisks: readonly string[];
229
- previewExportDifferenceReason?: string;
230
- previewRenderer: ToolcraftPreviewRenderer;
231
- productRepresentation: ToolcraftProductRepresentation;
232
- referenceRendererChangeReason?: string;
233
- rendererStrategy: ToolcraftRendererStrategy;
234
- rendererWorkload: ToolcraftRendererWorkload;
235
- sourceRepresentation: ToolcraftSourceRepresentation;
236
- whyNotAlternativeStrategies: readonly string[];
237
- };
238
-
239
- export type ToolcraftRenderPassKind =
240
- | "decode"
241
- | "preprocess"
242
- | "pixel-transform"
243
- | "vector-build"
244
- | "text-layout"
245
- | "rasterize"
246
- | "composite"
247
- | "handles"
248
- | "export";
249
-
250
- export type ToolcraftRenderPassRunLocation =
251
- | "main"
252
- | "worker"
253
- | "gpu"
254
- | "worker-or-gpu"
255
- | "export-only";
256
-
257
- export type ToolcraftRenderPassOutput =
258
- | "source"
259
- | "intermediate"
260
- | "preview"
261
- | "overlay"
262
- | "export";
263
-
264
- export type ToolcraftRenderPassQuality = "preview" | "full" | "retina" | "export";
265
-
266
- export type ToolcraftRenderPass = {
267
- cacheKey?: readonly string[];
268
- id: string;
269
- inputs: readonly string[];
270
- invalidatedBy: readonly string[];
271
- kind: ToolcraftRenderPassKind;
272
- output: ToolcraftRenderPassOutput;
273
- quality: ToolcraftRenderPassQuality;
274
- runsOn: ToolcraftRenderPassRunLocation;
275
- };
276
-
277
- export type ToolcraftPipelineInteraction =
278
- | "animation-frame"
279
- | "control-change"
280
- | "control-drag"
281
- | "media-import"
282
- | "mask-drag"
283
- | "viewport-drag"
284
- | "viewport-zoom"
285
- | "timeline-playback"
286
- | "timeline-scrub"
287
- | "export";
288
-
289
- export type ToolcraftInteractionInvalidation = {
290
- interaction: ToolcraftPipelineInteraction;
291
- invalidates: readonly string[];
292
- mustNotInvalidate?: readonly string[];
293
- targets: readonly string[];
294
- };
295
-
296
- export type ToolcraftRendererPipeline = {
297
- interactionInvalidation: readonly ToolcraftInteractionInvalidation[];
298
- passes: readonly ToolcraftRenderPass[];
299
- };
300
-
301
- export type ToolcraftPerformanceConfig = {
302
- browserCheckPolicy?: ToolcraftBrowserCheckPolicy;
303
- rendererPipeline?: ToolcraftRendererPipeline;
304
- rendererStrategy: ToolcraftRendererStrategy;
305
- rendererTechnique?: ToolcraftRendererTechnique;
306
- rendererWorkload: ToolcraftRendererWorkload;
307
- scenarios: readonly ToolcraftPerformanceScenario[];
308
- usesCustomRenderer: boolean;
309
- workloadTargets: readonly string[];
310
- };
311
-
312
- export type ToolcraftPerformanceSensitiveControl = {
313
- control: ToolcraftControlSchema;
314
- controlId: string;
315
- target: string;
316
- };
317
-
318
- export type ToolcraftUnclassifiedPerformanceControl = {
319
- control: ToolcraftControlSchema;
320
- controlId: string;
321
- target: string;
322
- };
323
-
324
- const maxPerformanceBudgetCaps: Required<ToolcraftPerformanceBudget> = {
325
- maxExportMs: 8000,
326
- maxFrameGapMs: 120,
327
- maxInteractionMs: 2000,
328
- maxLongTaskMs: 250,
329
- maxPreviewMs: 2000,
330
- maxRenderMs: 2000,
331
- };
332
-
333
- export const defaultToolcraftBrowserCheckPolicy: ToolcraftBrowserCheckPolicy = {
334
- fallbackRunner: "playwright",
335
- fallbackWhen: ["agent-browser-unavailable", "ci"],
336
- preferredRunner: "agent-browser",
337
- };
338
-
339
- export function defineToolcraftPerformance(
340
- config: ToolcraftPerformanceConfig,
341
- ): ToolcraftPerformanceConfig & { browserCheckPolicy: ToolcraftBrowserCheckPolicy } {
342
- return {
343
- ...config,
344
- browserCheckPolicy: {
345
- ...defaultToolcraftBrowserCheckPolicy,
346
- ...config.browserCheckPolicy,
347
- fallbackWhen:
348
- config.browserCheckPolicy?.fallbackWhen ??
349
- defaultToolcraftBrowserCheckPolicy.fallbackWhen,
350
- },
351
- };
352
- }
353
-
354
- const workloadControlPattern =
355
- /char\s*size|cell|density|glyph|grid|iteration|matrix|particle|quality|radius|resolution|sample|scale|size/i;
356
-
357
- const heavyTextInputPattern = /code|css|instruction|json|prompt|script|shader|template/i;
358
-
359
- const largeTextStressMinChars = 50_000;
360
- const largeTextStressMinLines = 1_000;
361
- const mediaStressMinArea = 1920 * 1080;
362
- const mediaStressMinLongEdge = 1920;
363
-
364
- const performanceFixtureKinds = new Set<ToolcraftPerformanceFixtureKind>([
365
- "custom",
366
- "high-density",
367
- "large-canvas",
368
- "large-text",
369
- "many-items",
370
- "max-value",
371
- "media",
372
- ]);
373
-
374
- const performanceLoadMetrics = new Set<ToolcraftPerformanceLoadMetric>([
375
- "canvas-area",
376
- "count",
377
- "custom",
378
- "media-area",
379
- "numeric-max",
380
- "numeric-min",
381
- "text-length",
382
- ]);
383
-
384
- const performanceUserFacingRanges = new Set<ToolcraftPerformanceUserFacingRange>([
385
- "experimental-above-smooth",
386
- "fully-guaranteed",
387
- ]);
388
-
389
- const browserCheckPreferredRunners = new Set<ToolcraftBrowserCheckPreferredRunner>([
390
- "agent-browser",
391
- ]);
392
-
393
- const browserCheckFallbackRunners = new Set<ToolcraftBrowserCheckFallbackRunner>([
394
- "playwright",
395
- ]);
396
-
397
- const browserCheckFallbackConditions = new Set<ToolcraftBrowserCheckFallbackCondition>([
398
- "agent-browser-unavailable",
399
- "ci",
400
- ]);
401
-
402
- const loadProfileRequiredFixtureKinds = new Set<ToolcraftPerformanceFixtureKind>([
403
- "high-density",
404
- "large-canvas",
405
- "many-items",
406
- "max-value",
407
- "media",
408
- ]);
409
-
410
- const renderPassKinds = new Set<ToolcraftRenderPassKind>([
411
- "decode",
412
- "preprocess",
413
- "pixel-transform",
414
- "vector-build",
415
- "text-layout",
416
- "rasterize",
417
- "composite",
418
- "handles",
419
- "export",
420
- ]);
421
-
422
- const renderPassRunLocations = new Set<ToolcraftRenderPassRunLocation>([
423
- "main",
424
- "worker",
425
- "gpu",
426
- "worker-or-gpu",
427
- "export-only",
428
- ]);
429
-
430
- const renderPassOutputs = new Set<ToolcraftRenderPassOutput>([
431
- "source",
432
- "intermediate",
433
- "preview",
434
- "overlay",
435
- "export",
436
- ]);
437
-
438
- const renderPassQualities = new Set<ToolcraftRenderPassQuality>([
439
- "preview",
440
- "full",
441
- "retina",
442
- "export",
443
- ]);
444
-
445
- const pipelineInteractions = new Set<ToolcraftPipelineInteraction>([
446
- "animation-frame",
447
- "control-change",
448
- "control-drag",
449
- "media-import",
450
- "mask-drag",
451
- "viewport-drag",
452
- "viewport-zoom",
453
- "timeline-playback",
454
- "timeline-scrub",
455
- "export",
456
- ]);
457
-
458
- const expensiveRenderPassKinds = new Set<ToolcraftRenderPassKind>([
459
- "decode",
460
- "preprocess",
461
- "pixel-transform",
462
- "text-layout",
463
- "rasterize",
464
- ]);
465
-
466
- const cacheRequiredRenderPassKinds = new Set<ToolcraftRenderPassKind>([
467
- ...expensiveRenderPassKinds,
468
- "composite",
469
- ]);
470
-
471
- const highFrequencyViewportInteractions = new Set<ToolcraftPipelineInteraction>([
472
- "animation-frame",
473
- "mask-drag",
474
- "timeline-playback",
475
- "timeline-scrub",
476
- "viewport-drag",
477
- "viewport-zoom",
478
- ]);
479
-
480
- const vaguePipelineReferencePattern =
481
- /^(?:all|all values|everything|props|runtime|settings|state|values)$/i;
482
-
483
- function getControlSemanticText(control: ToolcraftControlSchema): string {
484
- return [
485
- control.target,
486
- typeof control.label === "string" ? control.label : "",
487
- control.unit ?? "",
488
- control.valueLabel ?? "",
489
- control.xLabel ?? "",
490
- control.yLabel ?? "",
491
- ...(control.options ?? []).flatMap((option) => [option.label, option.value]),
492
- ].join(" ");
493
- }
494
-
495
- function isSemanticallyWorkloadControl(control: ToolcraftControlSchema): boolean {
496
- const semanticText = getControlSemanticText(control);
497
-
498
- if (control.type === "code" || control.type === "text") {
499
- return heavyTextInputPattern.test(semanticText);
500
- }
501
-
502
- return workloadControlPattern.test(semanticText);
503
- }
504
-
505
- function isPotentialWorkloadControl(control: ToolcraftControlSchema): boolean {
506
- return control.performanceRole === "workload" || isSemanticallyWorkloadControl(control);
507
- }
508
-
509
- export function collectToolcraftPerformanceSensitiveControls(
510
- schema: ResolvedToolcraftAppSchema,
511
- ): ToolcraftPerformanceSensitiveControl[] {
512
- return (schema.panels.controls?.sections ?? []).flatMap((section) =>
513
- Object.entries(section.controls)
514
- .filter(
515
- ([, control]) =>
516
- !isToolcraftRuntimeOwnedTarget(control.target) && isPotentialWorkloadControl(control),
517
- )
518
- .map(([controlId, control]) => ({
519
- control,
520
- controlId,
521
- target: control.target,
522
- })),
523
- );
524
- }
525
-
526
- function collectToolcraftPerformanceRoleConflicts(
527
- schema: ResolvedToolcraftAppSchema,
528
- ): ToolcraftPerformanceSensitiveControl[] {
529
- return (schema.panels.controls?.sections ?? []).flatMap((section) =>
530
- Object.entries(section.controls)
531
- .filter(
532
- ([, control]) =>
533
- !isToolcraftRuntimeOwnedTarget(control.target) &&
534
- control.performanceRole === "responsiveness" &&
535
- isSemanticallyWorkloadControl(control),
536
- )
537
- .map(([controlId, control]) => ({
538
- control,
539
- controlId,
540
- target: control.target,
541
- })),
542
- );
543
- }
544
-
545
- export function collectToolcraftUnclassifiedPerformanceControls(
546
- schema: ResolvedToolcraftAppSchema,
547
- ): ToolcraftUnclassifiedPerformanceControl[] {
548
- return (schema.panels.controls?.sections ?? []).flatMap((section) =>
549
- Object.entries(section.controls)
550
- .filter(
551
- ([, control]) =>
552
- control.type !== "panelActions" &&
553
- control.type !== "settingsTransfer" &&
554
- !isToolcraftRuntimeOwnedTarget(control.target) &&
555
- control.performanceRole !== "workload" &&
556
- control.performanceRole !== "responsiveness",
557
- )
558
- .map(([controlId, control]) => ({
559
- control,
560
- controlId,
561
- target: control.target,
562
- })),
563
- );
564
- }
565
-
566
- function hasAnyBudget(budget: ToolcraftPerformanceBudget): boolean {
567
- return Object.values(budget).some((value) => typeof value === "number" && value > 0);
568
- }
569
-
570
- function hasMinDefaultMax(values: ToolcraftPerformanceScenario["values"]): boolean {
571
- return values !== undefined && "default" in values && "min" in values && "max" in values;
572
- }
573
-
574
- function getBrowserCheckPolicyErrors(config: ToolcraftPerformanceConfig): string[] {
575
- const errors: string[] = [];
576
- const policy = config.browserCheckPolicy ?? defaultToolcraftBrowserCheckPolicy;
577
- const fallbackWhen = policy.fallbackWhen as unknown;
578
-
579
- if (
580
- !browserCheckPreferredRunners.has(
581
- policy.preferredRunner as ToolcraftBrowserCheckPreferredRunner,
582
- )
583
- ) {
584
- errors.push(
585
- 'browserCheckPolicy.preferredRunner must be "agent-browser" so AI agents use their controlled browser before fallback automation.',
586
- );
587
- }
588
-
589
- if (
590
- !browserCheckFallbackRunners.has(
591
- policy.fallbackRunner as ToolcraftBrowserCheckFallbackRunner,
592
- )
593
- ) {
594
- errors.push(
595
- 'browserCheckPolicy.fallbackRunner must be "playwright" so generated apps keep a portable CI/non-agent fallback.',
596
- );
597
- }
598
-
599
- if (!Array.isArray(fallbackWhen)) {
600
- errors.push(
601
- 'browserCheckPolicy.fallbackWhen must include "agent-browser-unavailable" and "ci".',
602
- );
603
- return errors;
604
- }
605
-
606
- for (const condition of fallbackWhen) {
607
- if (
608
- !browserCheckFallbackConditions.has(
609
- condition as ToolcraftBrowserCheckFallbackCondition,
610
- )
611
- ) {
612
- errors.push(
613
- `browserCheckPolicy.fallbackWhen contains unsupported condition "${String(
614
- condition,
615
- )}".`,
616
- );
617
- }
618
- }
619
-
620
- if (!fallbackWhen.includes("agent-browser-unavailable")) {
621
- errors.push(
622
- 'browserCheckPolicy.fallbackWhen must include "agent-browser-unavailable" so Playwright is only used when no agent browser exists.',
623
- );
624
- }
625
-
626
- if (!fallbackWhen.includes("ci")) {
627
- errors.push(
628
- 'browserCheckPolicy.fallbackWhen must include "ci" so CI/non-agent automation has a portable performance runner.',
629
- );
630
- }
631
-
632
- return errors;
633
- }
634
-
635
- function getSliderDragControlType(
636
- control: ToolcraftControlSchema | undefined,
637
- ): "rangeSlider" | "slider" | null {
638
- if (control?.type === "slider" || control?.type === "rangeSlider") {
639
- return control.type;
640
- }
641
-
642
- return null;
643
- }
644
-
645
- function hasControlDragScenario(scenarios: readonly ToolcraftPerformanceScenario[]): boolean {
646
- return scenarios.some((scenario) => scenario.interaction === "control-drag");
647
- }
648
-
649
- function hasPerformanceFixtureValue(
650
- fixture: ToolcraftPerformanceFixture,
651
- ): fixture is ToolcraftPerformanceFixture & { value: unknown } {
652
- return Object.prototype.hasOwnProperty.call(fixture, "value");
653
- }
654
-
655
- function isPerformanceFixtureObjectValue(value: unknown): value is Record<string, unknown> {
656
- return typeof value === "object" && value !== null && !Array.isArray(value);
657
- }
658
-
659
- function getMediaStressFixtureDimensions(
660
- value: unknown,
661
- ): { height: number; width: number } | null {
662
- if (!isPerformanceFixtureObjectValue(value)) {
663
- return null;
664
- }
665
-
666
- const width = Number(value.width);
667
- const height = Number(value.height);
668
-
669
- if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
670
- return null;
671
- }
672
-
673
- return { height, width };
674
- }
675
-
676
- function getTextLineCount(value: string): number {
677
- if (value.length === 0) {
678
- return 0;
679
- }
680
-
681
- return value.split(/\r\n|\r|\n/).length;
682
- }
683
-
684
- function getPerformanceFixtureLargeTextValueErrors(
685
- scenarioId: string,
686
- valueLabel: string,
687
- value: unknown,
688
- options: {
689
- minChars?: number;
690
- minLines?: number;
691
- } = {},
692
- ): string[] {
693
- const errors: string[] = [];
694
-
695
- if (typeof value !== "string") {
696
- errors.push(`${scenarioId} ${valueLabel} must be a string.`);
697
- return errors;
698
- }
699
-
700
- const minChars = options.minChars ?? largeTextStressMinChars;
701
- const minLines = options.minLines ?? largeTextStressMinLines;
702
-
703
- if (minChars < largeTextStressMinChars) {
704
- errors.push(`${scenarioId} ${valueLabel}.minChars must be >= ${largeTextStressMinChars}.`);
705
- }
706
-
707
- if (minLines < largeTextStressMinLines) {
708
- errors.push(`${scenarioId} ${valueLabel}.minLines must be >= ${largeTextStressMinLines}.`);
709
- }
710
-
711
- if (value.length < minChars) {
712
- errors.push(`${scenarioId} ${valueLabel} must contain at least ${minChars} characters.`);
713
- }
714
-
715
- if (getTextLineCount(value) < minLines) {
716
- errors.push(`${scenarioId} ${valueLabel} must contain at least ${minLines} lines.`);
717
- }
718
-
719
- return errors;
720
- }
721
-
722
- function getLastFixturePathSegment(path: string): string {
723
- return path.split(/[._-]/).filter(Boolean).at(-1) ?? path;
724
- }
725
-
726
- function isCustomLargeTextFixturePath(path: string): boolean {
727
- return /^(?:content|text|prompt|code|script|shader|template|json|css)$/i.test(
728
- getLastFixturePathSegment(path),
729
- );
730
- }
731
-
732
- function isCustomMediaFixturePath(path: string): boolean {
733
- return /(?:^|[._-])(?:media|image|video|sourceMedia|sourceImage)(?:$|[._-])/i.test(path);
734
- }
735
-
736
- function isCustomRenderScaleFixturePath(path: string): boolean {
737
- return /^(?:canvas\.)?renderScale$|(?:^|[._-])resolutionScale$/i.test(path);
738
- }
739
-
740
- function isCustomCountFixturePath(path: string): boolean {
741
- return /(?:^|[._-])(?:count|items|layers|particles|points|rows|columns|density|detail|quality|samples|iterations)(?:$|[._-])/i.test(
742
- path,
743
- );
744
- }
745
-
746
- function customFixtureValueNeedsLoadProfile(value: unknown, pathPrefix = ""): boolean {
747
- if (!isPerformanceFixtureObjectValue(value)) {
748
- return false;
749
- }
750
-
751
- return Object.entries(value).some(([key, itemValue]) => {
752
- const itemPath = pathPrefix ? `${pathPrefix}.${key}` : key;
753
-
754
- if (
755
- isCustomLargeTextFixturePath(itemPath) ||
756
- isCustomMediaFixturePath(itemPath) ||
757
- isCustomRenderScaleFixturePath(itemPath) ||
758
- isCustomCountFixturePath(itemPath)
759
- ) {
760
- return true;
761
- }
762
-
763
- if (typeof itemValue === "number" && Number.isFinite(itemValue)) {
764
- return true;
765
- }
766
-
767
- return customFixtureValueNeedsLoadProfile(itemValue, itemPath);
768
- });
769
- }
770
-
771
- function getSortedJsonValue(value: unknown): unknown {
772
- if (Array.isArray(value)) {
773
- return value.map(getSortedJsonValue);
774
- }
775
-
776
- if (isPerformanceFixtureObjectValue(value)) {
777
- return Object.fromEntries(
778
- Object.entries(value)
779
- .sort(([left], [right]) => left.localeCompare(right))
780
- .map(([key, itemValue]) => [key, getSortedJsonValue(itemValue)]),
781
- );
782
- }
783
-
784
- return value;
785
- }
786
-
787
- function arePerformanceFixtureValuesEqual(left: unknown, right: unknown): boolean {
788
- return JSON.stringify(getSortedJsonValue(left)) === JSON.stringify(getSortedJsonValue(right));
789
- }
790
-
791
- function getNumericFixtureValue(value: unknown): number | null {
792
- return typeof value === "number" && Number.isFinite(value) ? value : null;
793
- }
794
-
795
- function getCountFixtureValue(value: unknown): number | null {
796
- if (typeof value === "number" && Number.isFinite(value)) {
797
- return value;
798
- }
799
-
800
- if (Array.isArray(value)) {
801
- return value.length;
802
- }
803
-
804
- if (isPerformanceFixtureObjectValue(value)) {
805
- const count = Number(value.count ?? value.items ?? value.length);
806
-
807
- if (Number.isFinite(count)) {
808
- return count;
809
- }
810
-
811
- if (Array.isArray(value.items)) {
812
- return value.items.length;
813
- }
814
- }
815
-
816
- return null;
817
- }
818
-
819
- function getAreaFixtureDimensions(value: unknown): { height: number; width: number } | null {
820
- return getMediaStressFixtureDimensions(value);
821
- }
822
-
823
- function getAreaFixtureValue(value: unknown): number | null {
824
- const dimensions = getAreaFixtureDimensions(value);
825
-
826
- if (!dimensions) {
827
- return null;
828
- }
829
-
830
- return dimensions.width * dimensions.height;
831
- }
832
-
833
- function formatLoadProfileRatio(value: number): string {
834
- return Number(value.toFixed(4)).toString();
835
- }
836
-
837
- function getLoadProfileActualSmoothRatio(
838
- profile: ToolcraftPerformanceLoadProfile,
839
- ): number | null {
840
- if (profile.metric === "numeric-max") {
841
- const hardLimit = getNumericFixtureValue(profile.hardLimit);
842
- const smoothTarget = getNumericFixtureValue(profile.smoothTarget);
843
-
844
- return hardLimit !== null && smoothTarget !== null && hardLimit > 0
845
- ? smoothTarget / hardLimit
846
- : null;
847
- }
848
-
849
- if (profile.metric === "numeric-min") {
850
- const hardLimit = getNumericFixtureValue(profile.hardLimit);
851
- const smoothTarget = getNumericFixtureValue(profile.smoothTarget);
852
-
853
- return hardLimit !== null && smoothTarget !== null && smoothTarget > 0
854
- ? hardLimit / smoothTarget
855
- : null;
856
- }
857
-
858
- if (profile.metric === "count") {
859
- const hardLimit = getCountFixtureValue(profile.hardLimit);
860
- const smoothTarget = getCountFixtureValue(profile.smoothTarget);
861
-
862
- return hardLimit !== null && smoothTarget !== null && hardLimit > 0
863
- ? smoothTarget / hardLimit
864
- : null;
865
- }
866
-
867
- if (profile.metric === "canvas-area" || profile.metric === "media-area") {
868
- const hardArea = getAreaFixtureValue(profile.hardLimit);
869
- const smoothArea = getAreaFixtureValue(profile.smoothTarget);
870
-
871
- return hardArea !== null && smoothArea !== null && hardArea > 0
872
- ? smoothArea / hardArea
873
- : null;
874
- }
875
-
876
- return null;
877
- }
878
-
879
- function getLoadProfileSmoothTargetRatioError(
880
- label: string,
881
- profile: ToolcraftPerformanceLoadProfile,
882
- ): string | null {
883
- const declaredRatio = profile.smoothTargetRatio;
884
-
885
- if (
886
- typeof declaredRatio !== "number" ||
887
- !Number.isFinite(declaredRatio) ||
888
- declaredRatio <= 0 ||
889
- declaredRatio > 1
890
- ) {
891
- return null;
892
- }
893
-
894
- if (
895
- declaredRatio === 1 &&
896
- !arePerformanceFixtureValuesEqual(profile.smoothTarget, profile.hardLimit)
897
- ) {
898
- return `${label}.smoothTargetRatio 1 requires smoothTarget to equal hardLimit.`;
899
- }
900
-
901
- const actualRatio = getLoadProfileActualSmoothRatio(profile);
902
-
903
- if (actualRatio === null) {
904
- return null;
905
- }
906
-
907
- const tolerance = 0.0001;
908
-
909
- if (declaredRatio === 1) {
910
- return Math.abs(actualRatio - 1) <= tolerance
911
- ? null
912
- : `${label}.smoothTargetRatio 1 must describe the actual smooth target ratio ${formatLoadProfileRatio(actualRatio)}.`;
913
- }
914
-
915
- const previousStepRatio = Math.min(1, declaredRatio + 0.1);
916
-
917
- return actualRatio >= declaredRatio - tolerance &&
918
- actualRatio < previousStepRatio - tolerance
919
- ? null
920
- : `${label}.smoothTargetRatio ${formatLoadProfileRatio(declaredRatio)} must describe the actual smooth target ratio ${formatLoadProfileRatio(actualRatio)}.`;
921
- }
922
-
923
- function fixtureRequiresLoadProfile(fixture: ToolcraftPerformanceFixture): boolean {
924
- return (
925
- loadProfileRequiredFixtureKinds.has(fixture.kind) ||
926
- (fixture.kind === "custom" && customFixtureValueNeedsLoadProfile(fixture.value))
927
- );
928
- }
929
-
930
- function getLoadProfileRatioStepCount(ratio: number): number {
931
- return Math.round((1 - ratio) / 0.1);
932
- }
933
-
934
- function isTenPercentRatioStep(ratio: number): boolean {
935
- return Math.abs(ratio * 10 - Math.round(ratio * 10)) < 0.0001;
936
- }
937
-
938
- function getSchemaNumericHardLimit(
939
- control: ToolcraftControlSchema | undefined,
940
- metric: ToolcraftPerformanceLoadMetric,
941
- ): number | null {
942
- if (!control || (metric !== "numeric-max" && metric !== "numeric-min")) {
943
- return null;
944
- }
945
-
946
- const value = metric === "numeric-max" ? control.max : control.min;
947
-
948
- return typeof value === "number" && Number.isFinite(value) ? value : null;
949
- }
950
-
951
- function getPerformanceLoadProfileErrors(
952
- scenario: ToolcraftPerformanceScenario,
953
- fieldName: "stressFixture" | "workloadFixture",
954
- fixture: ToolcraftPerformanceFixture,
955
- control: ToolcraftControlSchema | undefined,
956
- ): string[] {
957
- const errors: string[] = [];
958
- const profile = fixture.loadProfile;
959
-
960
- if (!profile) {
961
- return fixtureRequiresLoadProfile(fixture)
962
- ? [
963
- `${scenario.id} ${fieldName}.loadProfile must declare hardLimit, smoothTarget, and smoothTargetRatio so performance workload is a measured smooth target rather than a hidden toy fixture.`,
964
- ]
965
- : [];
966
- }
967
-
968
- const label = `${scenario.id} ${fieldName}.loadProfile`;
969
-
970
- if (!profile.target.trim()) {
971
- errors.push(`${label}.target must name the product workload target.`);
972
- }
973
-
974
- if (!performanceLoadMetrics.has(profile.metric)) {
975
- errors.push(`${label}.metric "${profile.metric}" is not supported.`);
976
- }
977
-
978
- if (!performanceUserFacingRanges.has(profile.userFacingRange)) {
979
- errors.push(`${label}.userFacingRange "${profile.userFacingRange}" is not supported.`);
980
- }
981
-
982
- if (
983
- typeof profile.smoothTargetRatio !== "number" ||
984
- !Number.isFinite(profile.smoothTargetRatio) ||
985
- profile.smoothTargetRatio <= 0 ||
986
- profile.smoothTargetRatio > 1
987
- ) {
988
- errors.push(`${label}.smoothTargetRatio must be > 0 and <= 1.`);
989
- } else if (!isTenPercentRatioStep(profile.smoothTargetRatio)) {
990
- errors.push(`${label}.smoothTargetRatio must use 10 percent steps such as 1, 0.9, or 0.8.`);
991
- }
992
-
993
- const smoothTargetRatioError = getLoadProfileSmoothTargetRatioError(label, profile);
994
-
995
- if (smoothTargetRatioError) {
996
- errors.push(smoothTargetRatioError);
997
- }
998
-
999
- if (!hasPerformanceFixtureValue(fixture)) {
1000
- errors.push(`${label}.smoothTarget cannot be checked because ${fieldName}.value is missing.`);
1001
- } else if (!arePerformanceFixtureValuesEqual(profile.smoothTarget, fixture.value)) {
1002
- errors.push(`${label}.smoothTarget must match ${fieldName}.value so browser tests apply the documented smooth workload target.`);
1003
- }
1004
-
1005
- const schemaHardLimit = getSchemaNumericHardLimit(control, profile.metric);
1006
-
1007
- if (
1008
- schemaHardLimit !== null &&
1009
- !arePerformanceFixtureValuesEqual(profile.hardLimit, schemaHardLimit)
1010
- ) {
1011
- errors.push(`${label}.hardLimit must match schema ${profile.metric === "numeric-max" ? "max" : "min"} ${schemaHardLimit}.`);
1012
- }
1013
-
1014
- if (profile.metric === "numeric-max" || profile.metric === "numeric-min") {
1015
- const hardLimit = getNumericFixtureValue(profile.hardLimit);
1016
- const smoothTarget = getNumericFixtureValue(profile.smoothTarget);
1017
-
1018
- if (hardLimit === null || smoothTarget === null) {
1019
- errors.push(`${label}.${profile.metric} hardLimit and smoothTarget must be numeric.`);
1020
- } else if (profile.metric === "numeric-max" && smoothTarget > hardLimit) {
1021
- errors.push(`${label}.smoothTarget must be <= hardLimit for numeric-max workloads.`);
1022
- } else if (profile.metric === "numeric-min" && smoothTarget < hardLimit) {
1023
- errors.push(`${label}.smoothTarget must be >= hardLimit for numeric-min workloads.`);
1024
- }
1025
- }
1026
-
1027
- if (profile.metric === "count") {
1028
- const hardLimit = getCountFixtureValue(profile.hardLimit);
1029
- const smoothTarget = getCountFixtureValue(profile.smoothTarget);
1030
-
1031
- if (hardLimit === null || smoothTarget === null) {
1032
- errors.push(`${label}.count hardLimit and smoothTarget must be numeric counts, arrays, or objects with count/items.`);
1033
- } else if (smoothTarget > hardLimit) {
1034
- errors.push(`${label}.smoothTarget count must be <= hardLimit count.`);
1035
- }
1036
- }
1037
-
1038
- if (profile.metric === "canvas-area" || profile.metric === "media-area") {
1039
- const hardArea = getAreaFixtureValue(profile.hardLimit);
1040
- const smoothArea = getAreaFixtureValue(profile.smoothTarget);
1041
-
1042
- if (hardArea === null || smoothArea === null) {
1043
- errors.push(`${label}.${profile.metric} hardLimit and smoothTarget must include numeric width and height.`);
1044
- } else if (smoothArea > hardArea) {
1045
- errors.push(`${label}.smoothTarget area must be <= hardLimit area.`);
1046
- }
1047
- }
1048
-
1049
- if (fixture.kind === "many-items") {
1050
- const smoothCount = getCountFixtureValue(fixture.value);
1051
-
1052
- if (typeof fixture.minCount !== "number" || fixture.minCount <= 0) {
1053
- errors.push(`${scenario.id} ${fieldName}.minCount must be a positive number for many-items fixtures.`);
1054
- } else if (smoothCount !== null && smoothCount < fixture.minCount) {
1055
- errors.push(`${scenario.id} ${fieldName}.value must include at least ${fixture.minCount} items for many-items fixtures.`);
1056
- }
1057
- }
1058
-
1059
- if (
1060
- typeof profile.smoothTargetRatio === "number" &&
1061
- Number.isFinite(profile.smoothTargetRatio)
1062
- ) {
1063
- if (profile.smoothTargetRatio === 1) {
1064
- if (profile.userFacingRange !== "fully-guaranteed") {
1065
- errors.push(`${label}.userFacingRange must be "fully-guaranteed" when smoothTargetRatio is 1.`);
1066
- }
1067
- } else if (profile.smoothTargetRatio > 0 && profile.smoothTargetRatio < 1) {
1068
- if (profile.degradationStepPercent !== 10) {
1069
- errors.push(`${label}.degradationStepPercent must be 10 when smoothTargetRatio is below 1.`);
1070
- }
1071
-
1072
- if (profile.userFacingRange !== "experimental-above-smooth") {
1073
- errors.push(`${label}.userFacingRange must be "experimental-above-smooth" when smoothTargetRatio is below 1.`);
1074
- }
1075
-
1076
- const expectedEvidenceCount = getLoadProfileRatioStepCount(profile.smoothTargetRatio);
1077
- const evidence = profile.evidence ?? [];
1078
-
1079
- if (evidence.length < expectedEvidenceCount) {
1080
- errors.push(`${label}.evidence must include failed measurements for each 10 percent step above smoothTargetRatio ${profile.smoothTargetRatio}.`);
1081
- }
1082
-
1083
- for (const [index, entry] of evidence.entries()) {
1084
- const evidenceLabel = `${label}.evidence[${index}]`;
1085
-
1086
- if (!entry.scenarioId.trim()) {
1087
- errors.push(`${evidenceLabel}.scenarioId must name the measured scenario.`);
1088
- }
1089
-
1090
- if (entry.result !== "failed") {
1091
- errors.push(`${evidenceLabel}.result must be "failed" for degraded smooth targets.`);
1092
- }
1093
-
1094
- if (!entry.measuredResult.trim()) {
1095
- errors.push(`${evidenceLabel}.measuredResult must include the measured budget failure.`);
1096
- }
1097
-
1098
- if (!entry.optimizationAttempted?.trim()) {
1099
- errors.push(`${evidenceLabel}.optimizationAttempted must describe the optimization attempted before lowering the smooth target.`);
1100
- }
1101
-
1102
- if (!entry.decision.trim()) {
1103
- errors.push(`${evidenceLabel}.decision must explain why this target was rejected or lowered.`);
1104
- }
1105
- }
1106
- }
1107
- }
1108
-
1109
- return errors;
1110
- }
1111
-
1112
- function getCustomPerformanceFixtureSemanticErrors(
1113
- scenarioId: string,
1114
- fieldName: "stressFixture" | "workloadFixture",
1115
- value: Record<string, unknown>,
1116
- pathPrefix = "",
1117
- ): string[] {
1118
- const errors: string[] = [];
1119
-
1120
- for (const [key, itemValue] of Object.entries(value)) {
1121
- const itemPath = pathPrefix ? `${pathPrefix}.${key}` : key;
1122
- const itemLabel = `custom ${fieldName}.${itemPath}`;
1123
-
1124
- if (isCustomLargeTextFixturePath(itemPath)) {
1125
- errors.push(
1126
- ...getPerformanceFixtureLargeTextValueErrors(
1127
- scenarioId,
1128
- itemLabel,
1129
- itemValue,
1130
- ),
1131
- );
1132
- continue;
1133
- }
1134
-
1135
- if (isCustomMediaFixturePath(itemPath)) {
1136
- const dimensions = getMediaStressFixtureDimensions(itemValue);
1137
-
1138
- if (!dimensions) {
1139
- errors.push(
1140
- `${scenarioId} ${itemLabel} must be an object with numeric width and height so browser tests can generate or load realistic media.`,
1141
- );
1142
- } else if (
1143
- Math.max(dimensions.width, dimensions.height) < mediaStressMinLongEdge ||
1144
- dimensions.width * dimensions.height < mediaStressMinArea
1145
- ) {
1146
- errors.push(
1147
- `${scenarioId} ${itemLabel} must be at least 1920x1080-equivalent; received ${dimensions.width}x${dimensions.height}.`,
1148
- );
1149
- }
1150
- continue;
1151
- }
1152
-
1153
- if (isCustomRenderScaleFixturePath(itemPath)) {
1154
- if (
1155
- typeof itemValue !== "number" ||
1156
- !Number.isFinite(itemValue) ||
1157
- itemValue <= 1
1158
- ) {
1159
- errors.push(
1160
- `${scenarioId} ${itemLabel} must be a numeric Resolution scale greater than 1 so browser tests prove high-resolution backing pixels.`,
1161
- );
1162
- }
1163
- continue;
1164
- }
1165
-
1166
- if (isPerformanceFixtureObjectValue(itemValue)) {
1167
- errors.push(
1168
- ...getCustomPerformanceFixtureSemanticErrors(
1169
- scenarioId,
1170
- fieldName,
1171
- itemValue,
1172
- itemPath,
1173
- ),
1174
- );
1175
- }
1176
- }
1177
-
1178
- return errors;
1179
- }
1180
-
1181
- function isLargeTextWorkloadControl(
1182
- control: ToolcraftControlSchema | undefined,
1183
- ): boolean {
1184
- if (
1185
- !control ||
1186
- (control.type !== "code" && control.type !== "text" && control.type !== "textarea")
1187
- ) {
1188
- return false;
1189
- }
1190
-
1191
- return (
1192
- control.type === "code" ||
1193
- control.type === "textarea" ||
1194
- heavyTextInputPattern.test(getControlSemanticText(control))
1195
- );
1196
- }
1197
-
1198
- function getPerformanceFixtureShapeErrors(
1199
- scenario: ToolcraftPerformanceScenario,
1200
- fieldName: "stressFixture" | "workloadFixture",
1201
- fixture: ToolcraftPerformanceFixture,
1202
- control: ToolcraftControlSchema | undefined,
1203
- options: {
1204
- requireLargeText?: boolean;
1205
- requireMedia?: boolean;
1206
- } = {},
1207
- ): string[] {
1208
- const errors: string[] = [];
1209
- const scenarioId = scenario.id;
1210
-
1211
- if (!performanceFixtureKinds.has(fixture.kind)) {
1212
- errors.push(`${scenarioId} ${fieldName}.kind "${fixture.kind}" is not supported.`);
1213
- }
1214
-
1215
- if (options.requireMedia) {
1216
- if (fixture.kind !== "media") {
1217
- errors.push(
1218
- `${scenarioId} media-import scenario must use ${fieldName}.kind "media" with a realistic uploaded source size.`,
1219
- );
1220
- }
1221
- }
1222
-
1223
- if (!fixture.reason.trim()) {
1224
- errors.push(
1225
- `${scenarioId} ${fieldName} must explain why this is the heaviest useful fixture.`,
1226
- );
1227
- }
1228
-
1229
- if (!hasPerformanceFixtureValue(fixture)) {
1230
- errors.push(
1231
- `${scenarioId} ${fieldName} must include value so browser tests can apply the exact heavy state.`,
1232
- );
1233
- } else if (fixture.kind === "custom") {
1234
- if (!isPerformanceFixtureObjectValue(fixture.value)) {
1235
- errors.push(
1236
- `${scenarioId} custom ${fieldName}.value must be an object with one key per heavy state part so browser tests can apply every key.`,
1237
- );
1238
- } else if (Object.keys(fixture.value).length === 0) {
1239
- errors.push(
1240
- `${scenarioId} custom ${fieldName}.value must include at least one heavy state key.`,
1241
- );
1242
- } else {
1243
- errors.push(
1244
- ...getCustomPerformanceFixtureSemanticErrors(
1245
- scenarioId,
1246
- fieldName,
1247
- fixture.value,
1248
- ),
1249
- );
1250
- }
1251
- } else if (fixture.kind === "media") {
1252
- const dimensions = getMediaStressFixtureDimensions(fixture.value);
1253
-
1254
- if (!dimensions) {
1255
- errors.push(
1256
- `${scenarioId} media ${fieldName}.value must be an object with numeric width and height so browser tests can generate or load a realistic source image.`,
1257
- );
1258
- } else if (
1259
- Math.max(dimensions.width, dimensions.height) < mediaStressMinLongEdge ||
1260
- dimensions.width * dimensions.height < mediaStressMinArea
1261
- ) {
1262
- errors.push(
1263
- `${scenarioId} media ${fieldName}.value must be at least 1920x1080-equivalent; received ${dimensions.width}x${dimensions.height}.`,
1264
- );
1265
- }
1266
- }
1267
-
1268
- if (!options.requireLargeText && fixture.kind !== "large-text") {
1269
- errors.push(...getPerformanceLoadProfileErrors(scenario, fieldName, fixture, control));
1270
- return errors;
1271
- }
1272
-
1273
- if (fixture.kind !== "large-text") {
1274
- errors.push(
1275
- `${scenarioId} text workload scenario must use ${fieldName}.kind "large-text" with a real long text value.`,
1276
- );
1277
- return errors;
1278
- }
1279
-
1280
- errors.push(
1281
- ...getPerformanceFixtureLargeTextValueErrors(
1282
- scenarioId,
1283
- `large-text ${fieldName}.value`,
1284
- fixture.value,
1285
- {
1286
- minChars: fixture.minChars,
1287
- minLines: fixture.minLines,
1288
- },
1289
- ),
1290
- );
1291
-
1292
- errors.push(...getPerformanceLoadProfileErrors(scenario, fieldName, fixture, control));
1293
-
1294
- return errors;
1295
- }
1296
-
1297
- function getStressFixtureErrors(
1298
- scenario: ToolcraftPerformanceScenario,
1299
- control: ToolcraftControlSchema | undefined,
1300
- ): string[] {
1301
- const isMediaImport = scenario.interaction === "media-import";
1302
-
1303
- if (!scenario.workload && scenario.stress !== true && !scenario.stressFixture && !isMediaImport) {
1304
- return [];
1305
- }
1306
-
1307
- const fixture = scenario.stressFixture;
1308
-
1309
- if (!fixture) {
1310
- if (isMediaImport) {
1311
- return [
1312
- `${scenario.id} media-import scenario must declare stressFixture.kind "media" with a realistic uploaded source size.`,
1313
- ];
1314
- }
1315
-
1316
- return scenario.workload
1317
- ? [
1318
- `${scenario.id} workload scenario must declare stressFixture with the real heaviest value used by browser performance tests.`,
1319
- ]
1320
- : [
1321
- `${scenario.id} stress scenario must declare stressFixture with the real heaviest state used by browser performance tests.`,
1322
- ];
1323
- }
1324
-
1325
- const errors: string[] = [];
1326
-
1327
- if (isMediaImport && !scenario.workload) {
1328
- errors.push(
1329
- `${scenario.id} media-import scenario must set workload true because decoded source size changes renderer workload.`,
1330
- );
1331
- }
1332
-
1333
- errors.push(
1334
- ...getPerformanceFixtureShapeErrors(scenario, "stressFixture", fixture, control, {
1335
- requireLargeText: isLargeTextWorkloadControl(control),
1336
- requireMedia: isMediaImport,
1337
- }),
1338
- );
1339
-
1340
- return errors;
1341
- }
1342
-
1343
- function stressFixtureAlreadyDefinesIndependentWorkload(
1344
- scenario: ToolcraftPerformanceScenario,
1345
- control: ToolcraftControlSchema | undefined,
1346
- ): boolean {
1347
- return (
1348
- scenario.stressFixture?.kind === "large-text" &&
1349
- isLargeTextWorkloadControl(control)
1350
- );
1351
- }
1352
-
1353
- function isWorkloadBaselineSensitiveScenario(
1354
- config: ToolcraftPerformanceConfig,
1355
- scenario: ToolcraftPerformanceScenario,
1356
- control: ToolcraftControlSchema | undefined,
1357
- ): boolean {
1358
- if (
1359
- !scenario.workload ||
1360
- (scenario.interaction !== "control-change" && scenario.interaction !== "control-drag")
1361
- ) {
1362
- return false;
1363
- }
1364
-
1365
- if (!config.usesCustomRenderer) {
1366
- return false;
1367
- }
1368
-
1369
- if (config.rendererWorkload === "none" || config.rendererWorkload === "simple-composition") {
1370
- return false;
1371
- }
1372
-
1373
- return !stressFixtureAlreadyDefinesIndependentWorkload(scenario, control);
1374
- }
1375
-
1376
- function getWorkloadFixtureErrors(
1377
- config: ToolcraftPerformanceConfig,
1378
- scenario: ToolcraftPerformanceScenario,
1379
- control: ToolcraftControlSchema | undefined,
1380
- ): string[] {
1381
- const requiresBaseline = isWorkloadBaselineSensitiveScenario(config, scenario, control);
1382
- const errors: string[] = [];
1383
-
1384
- if (scenario.workloadFixture && !scenario.stressFixture) {
1385
- errors.push(
1386
- `${scenario.id} workloadFixture must be paired with stressFixture so tests apply a heavy baseline and then the measured scenario value.`,
1387
- );
1388
- }
1389
-
1390
- if (!requiresBaseline && !scenario.workloadFixture) {
1391
- return errors;
1392
- }
1393
-
1394
- const fixture = scenario.workloadFixture;
1395
-
1396
- if (!fixture) {
1397
- errors.push(
1398
- `${scenario.id} workload control scenario must declare workloadFixture for the app's heavy baseline state; stressFixture covers the control value only.`,
1399
- );
1400
- return errors;
1401
- }
1402
-
1403
- errors.push(
1404
- ...getPerformanceFixtureShapeErrors(
1405
- scenario,
1406
- "workloadFixture",
1407
- fixture,
1408
- control,
1409
- ),
1410
- );
1411
-
1412
- return errors;
1413
- }
1414
-
1415
- function hasPositiveBudgetField(
1416
- budget: ToolcraftPerformanceBudget,
1417
- field: keyof ToolcraftPerformanceBudget,
1418
- ): boolean {
1419
- const value = budget[field];
1420
- return typeof value === "number" && value > 0;
1421
- }
1422
-
1423
- function getMissingInteractionBudgetFields(
1424
- scenario: ToolcraftPerformanceScenario,
1425
- ): string[] {
1426
- switch (scenario.interaction) {
1427
- case "animation-frame":
1428
- return hasPositiveBudgetField(scenario.budget, "maxFrameGapMs")
1429
- ? hasPositiveBudgetField(scenario.budget, "maxLongTaskMs")
1430
- ? []
1431
- : ["maxLongTaskMs"]
1432
- : hasPositiveBudgetField(scenario.budget, "maxLongTaskMs")
1433
- ? ["maxFrameGapMs"]
1434
- : ["maxFrameGapMs", "maxLongTaskMs"];
1435
- case "animation-viewport-drag":
1436
- case "mask-drag":
1437
- case "viewport-zoom-stress":
1438
- return (["maxInteractionMs", "maxFrameGapMs", "maxLongTaskMs"] as const).filter(
1439
- (field) => !hasPositiveBudgetField(scenario.budget, field),
1440
- );
1441
- case "viewport-stability":
1442
- return hasPositiveBudgetField(scenario.budget, "maxFrameGapMs")
1443
- ? []
1444
- : ["maxFrameGapMs"];
1445
- case "control-change":
1446
- case "control-drag":
1447
- case "media-import":
1448
- return (["maxInteractionMs", "maxFrameGapMs"] as const).filter(
1449
- (field) => !hasPositiveBudgetField(scenario.budget, field),
1450
- );
1451
- case "export-copy":
1452
- return hasPositiveBudgetField(scenario.budget, "maxExportMs") ? [] : ["maxExportMs"];
1453
- case "preview-render":
1454
- return hasPositiveBudgetField(scenario.budget, "maxPreviewMs") ||
1455
- hasPositiveBudgetField(scenario.budget, "maxRenderMs")
1456
- ? []
1457
- : ["maxPreviewMs or maxRenderMs"];
1458
- case "timeline-playback":
1459
- return (["maxFrameGapMs", "maxLongTaskMs"] as const).filter(
1460
- (field) => !hasPositiveBudgetField(scenario.budget, field),
1461
- );
1462
- case "timeline-scrub":
1463
- return (["maxInteractionMs", "maxFrameGapMs", "maxLongTaskMs"] as const).filter(
1464
- (field) => !hasPositiveBudgetField(scenario.budget, field),
1465
- );
1466
- }
1467
- }
1468
-
1469
- function getBudgetCapErrors(scenario: ToolcraftPerformanceScenario): string[] {
1470
- return Object.entries(scenario.budget).flatMap(([field, value]) => {
1471
- const budgetField = field as keyof ToolcraftPerformanceBudget;
1472
- const cap = maxPerformanceBudgetCaps[budgetField];
1473
-
1474
- if (typeof value !== "number" || value <= cap) {
1475
- return [];
1476
- }
1477
-
1478
- return [`${scenario.id} ${budgetField} budget must be <= ${cap}ms, received ${value}ms.`];
1479
- });
1480
- }
1481
-
1482
- function requiresConcreteUiTarget(interaction: ToolcraftPerformanceInteraction): boolean {
1483
- return (
1484
- interaction === "control-change" ||
1485
- interaction === "control-drag" ||
1486
- interaction === "mask-drag" ||
1487
- interaction === "timeline-playback" ||
1488
- interaction === "timeline-scrub"
1489
- );
1490
- }
1491
-
1492
- function getAllSchemaControls(
1493
- schema: ResolvedToolcraftAppSchema,
1494
- ): ToolcraftControlSchema[] {
1495
- return (schema.panels.controls?.sections ?? []).flatMap((section) =>
1496
- Object.values(section.controls),
1497
- );
1498
- }
1499
-
1500
- function getVisiblePerformanceControlTargets(schema: ResolvedToolcraftAppSchema): string[] {
1501
- return getAllSchemaControls(schema)
1502
- .filter((control) => control.type !== "panelActions")
1503
- .map((control) => control.target);
1504
- }
1505
-
1506
- function getActionValue(
1507
- action: NonNullable<ToolcraftControlSchema["actions"]>[number],
1508
- ): string {
1509
- return typeof action === "string" ? action : action.value;
1510
- }
1511
-
1512
- function hasOutputDeliveryAction(schema: ResolvedToolcraftAppSchema): boolean {
1513
- return getAllSchemaControls(schema).some(
1514
- (control) =>
1515
- control.type === "panelActions" &&
1516
- (control.actions ?? []).some((action) =>
1517
- /copy|download|export/i.test(getActionValue(action)),
1518
- ),
1519
- );
1520
- }
1521
-
1522
- function hasKeyframeTimeline(schema: ResolvedToolcraftAppSchema): boolean {
1523
- return schema.panels.timeline?.enabled === true && schema.panels.timeline.mode === "keyframes";
1524
- }
1525
-
1526
- function hasLayersPanel(schema: ResolvedToolcraftAppSchema): boolean {
1527
- return schema.panels.layers === true;
1528
- }
1529
-
1530
- function hasNonEmptyItems(items: readonly string[]): boolean {
1531
- return items.some((item) => item.trim().length > 0);
1532
- }
1533
-
1534
- const rasterRendererStrategies = new Set<ToolcraftRendererStrategy>([
1535
- "canvas-2d",
1536
- "webgl",
1537
- "webgpu",
1538
- ]);
1539
-
1540
- const semanticForegroundContent = new Set<ToolcraftRendererLayerContent>([
1541
- "geometry",
1542
- "text",
1543
- ]);
1544
-
1545
- const detailHeavyRendererContent = new Set<ToolcraftRendererLayerContent>([
1546
- "bitmap-media",
1547
- "dense-pattern",
1548
- "noise",
1549
- "shader",
1550
- ]);
1551
-
1552
- const vectorLayerRendererStrategies = new Set<ToolcraftRendererStrategy>(["dom", "svg"]);
1553
-
1554
- function getLayerContentFamily(content: ToolcraftRendererLayerContent): string {
1555
- if (content === "geometry" || content === "handles") {
1556
- return "vector";
1557
- }
1558
-
1559
- if (content === "text") {
1560
- return "text";
1561
- }
1562
-
1563
- if (
1564
- content === "bitmap-media" ||
1565
- content === "dense-pattern" ||
1566
- content === "noise" ||
1567
- content === "shader"
1568
- ) {
1569
- return "pixel";
1570
- }
1571
-
1572
- return "composite";
1573
- }
1574
-
1575
- function hasSemanticForegroundContent(layer: ToolcraftRendererLayer): boolean {
1576
- return layer.content.some((content) => semanticForegroundContent.has(content));
1577
- }
1578
-
1579
- function hasDetailHeavyRendererLayer(
1580
- technique: ToolcraftRendererTechnique | undefined,
1581
- ): boolean {
1582
- return (technique?.layers ?? []).some(
1583
- (layer) =>
1584
- layer.primitiveCount === "high" ||
1585
- layer.content.some((content) => detailHeavyRendererContent.has(content)),
1586
- );
1587
- }
1588
-
1589
- function hasHighCountCanvas2DRendererLayer(
1590
- technique: ToolcraftRendererTechnique | undefined,
1591
- ): boolean {
1592
- return (technique?.layers ?? []).some(
1593
- (layer) =>
1594
- layer.renderer === "canvas-2d" &&
1595
- layer.primitiveCount === "high" &&
1596
- (hasSemanticForegroundContent(layer) ||
1597
- layer.content.some((content) => detailHeavyRendererContent.has(content))),
1598
- );
1599
- }
1600
-
1601
- function hasDetailHeavyCanvas2DRendererLayer(
1602
- technique: ToolcraftRendererTechnique | undefined,
1603
- ): boolean {
1604
- return (technique?.layers ?? []).some(
1605
- (layer) =>
1606
- layer.renderer === "canvas-2d" &&
1607
- layer.content.some((content) => detailHeavyRendererContent.has(content)),
1608
- );
1609
- }
1610
-
1611
- function getRendererMeasuredAlternativeEvidenceErrors(
1612
- technique: ToolcraftRendererTechnique,
1613
- ): string[] {
1614
- return (technique.measuredAlternativeEvidence ?? []).flatMap((evidence, index) => {
1615
- const label = `rendererTechnique.measuredAlternativeEvidence[${index}]`;
1616
- const errors: string[] = [];
1617
-
1618
- if (evidence.alternativeStrategy !== "webgl" && evidence.alternativeStrategy !== "webgpu") {
1619
- errors.push(`${label}.alternativeStrategy must be "webgl" or "webgpu".`);
1620
- }
1621
-
1622
- if (!evidence.scenarioId.trim()) {
1623
- errors.push(`${label}.scenarioId must name the measured performance scenario.`);
1624
- }
1625
-
1626
- if (!evidence.fixture.trim()) {
1627
- errors.push(`${label}.fixture must name the exact stress fixture that was measured.`);
1628
- }
1629
-
1630
- if (!evidence.measuredResult.trim()) {
1631
- errors.push(`${label}.measuredResult must summarize measured frame, interaction, render, or long-task results.`);
1632
- }
1633
-
1634
- if (!evidence.decision.trim()) {
1635
- errors.push(`${label}.decision must explain why the selected renderer remains appropriate.`);
1636
- }
1637
-
1638
- return errors;
1639
- });
1640
- }
1641
-
1642
- function hasMeasuredGpuAlternativeEvidence(
1643
- technique: ToolcraftRendererTechnique,
1644
- ): boolean {
1645
- return (technique.measuredAlternativeEvidence ?? []).some(
1646
- (evidence) =>
1647
- (evidence.alternativeStrategy === "webgl" ||
1648
- evidence.alternativeStrategy === "webgpu") &&
1649
- evidence.scenarioId.trim().length > 0 &&
1650
- evidence.fixture.trim().length > 0 &&
1651
- evidence.measuredResult.trim().length > 0 &&
1652
- evidence.decision.trim().length > 0,
1653
- );
1654
- }
1655
-
1656
- function hasStressPreviewOrAnimationScenario(config: ToolcraftPerformanceConfig): boolean {
1657
- return config.scenarios.some(
1658
- (scenario) =>
1659
- scenario.stress === true &&
1660
- (scenario.interaction === "preview-render" ||
1661
- scenario.interaction === "animation-frame"),
1662
- );
1663
- }
1664
-
1665
- function hasLongTaskBudgetScenario(config: ToolcraftPerformanceConfig): boolean {
1666
- return config.scenarios.some((scenario) =>
1667
- hasPositiveBudgetField(scenario.budget, "maxLongTaskMs"),
1668
- );
1669
- }
1670
-
1671
- function hasZoomSensitiveRenderer(config: ToolcraftPerformanceConfig): boolean {
1672
- return (
1673
- config.rendererWorkload === "text-output" ||
1674
- config.rendererWorkload === "vector-output" ||
1675
- config.rendererWorkload === "pixel-output" ||
1676
- hasDetailHeavyRendererLayer(config.rendererTechnique)
1677
- );
1678
- }
1679
-
1680
- function getRendererLayerErrors(technique: ToolcraftRendererTechnique): string[] {
1681
- const errors: string[] = [];
1682
- const layers = technique.layers ?? [];
1683
-
1684
- if (technique.productRepresentation === "mixed" && layers.length === 0) {
1685
- errors.push(
1686
- 'productRepresentation "mixed" requires rendererTechnique.layers so mixed output is machine-checkable.',
1687
- );
1688
- }
1689
-
1690
- if (technique.productRepresentation === "mixed") {
1691
- const contentFamilies = new Set(
1692
- layers.flatMap((layer) => layer.content.map((content) => getLayerContentFamily(content))),
1693
- );
1694
- contentFamilies.delete("composite");
1695
-
1696
- if (contentFamilies.size < 2) {
1697
- errors.push(
1698
- 'productRepresentation "mixed" requires rendererTechnique.layers with at least two different content families.',
1699
- );
1700
- }
1701
- }
1702
-
1703
- for (const layer of layers) {
1704
- if (!layer.id.trim()) {
1705
- errors.push("rendererTechnique layers must have non-empty ids.");
1706
- }
1707
-
1708
- if (!hasNonEmptyItems(layer.content)) {
1709
- errors.push(`rendererTechnique layer "${layer.id}" must list content.`);
1710
- }
1711
-
1712
- if (
1713
- layer.kind === "product-foreground" &&
1714
- hasSemanticForegroundContent(layer) &&
1715
- layer.primitiveCount !== "high" &&
1716
- rasterRendererStrategies.has(layer.renderer) &&
1717
- !layer.intentionalRasterizationReason?.trim()
1718
- ) {
1719
- errors.push(
1720
- `rendererTechnique layer "${layer.id}" uses ${layer.renderer} for low-count semantic geometry/text. Use dom/svg for semantic foreground or provide intentionalRasterizationReason.`,
1721
- );
1722
- }
1723
-
1724
- if (
1725
- (layer.kind === "product-foreground" || layer.kind === "editing-handles") &&
1726
- !layer.uiSelector?.trim()
1727
- ) {
1728
- errors.push(
1729
- `rendererTechnique layer "${layer.id}" is ${layer.kind} and must declare uiSelector so browser tests can verify the visible renderer layer.`,
1730
- );
1731
- }
1732
-
1733
- if (
1734
- layer.kind === "editing-handles" &&
1735
- (!vectorLayerRendererStrategies.has(layer.renderer) || layer.exportMode !== "excluded")
1736
- ) {
1737
- errors.push(
1738
- `rendererTechnique layer "${layer.id}" is editing-handles and must use dom/svg with exportMode "excluded".`,
1739
- );
1740
- }
1741
- }
1742
-
1743
- return errors;
1744
- }
1745
-
1746
- function getRendererTechniqueErrors(config: ToolcraftPerformanceConfig): string[] {
1747
- const errors: string[] = [];
1748
- const technique = config.rendererTechnique;
1749
-
1750
- if (config.usesCustomRenderer && !technique) {
1751
- return [
1752
- "Custom renderers must declare rendererTechnique so renderer choice is machine-checkable.",
1753
- ];
1754
- }
1755
-
1756
- if (!config.usesCustomRenderer && technique) {
1757
- errors.push("Non-custom renderer configs must omit rendererTechnique.");
1758
- }
1759
-
1760
- if (!technique) {
1761
- return errors;
1762
- }
1763
-
1764
- if (technique.rendererWorkload !== config.rendererWorkload) {
1765
- errors.push(
1766
- `rendererTechnique.rendererWorkload "${technique.rendererWorkload}" must match rendererWorkload "${config.rendererWorkload}".`,
1767
- );
1768
- }
1769
-
1770
- if (technique.rendererStrategy !== config.rendererStrategy) {
1771
- errors.push(
1772
- `rendererTechnique.rendererStrategy "${technique.rendererStrategy}" must match rendererStrategy "${config.rendererStrategy}".`,
1773
- );
1774
- }
1775
-
1776
- if (config.usesCustomRenderer && !hasNonEmptyItems(technique.whyNotAlternativeStrategies)) {
1777
- errors.push(
1778
- "Custom renderer technique must explain why alternative renderer strategies were rejected.",
1779
- );
1780
- }
1781
-
1782
- if (config.usesCustomRenderer && !hasNonEmptyItems(technique.fidelityRisks)) {
1783
- errors.push("Custom renderer technique must list fidelity risks.");
1784
- }
1785
-
1786
- if (config.usesCustomRenderer && !hasNonEmptyItems(technique.performanceRisks)) {
1787
- errors.push("Custom renderer technique must list performance risks.");
1788
- }
1789
-
1790
- errors.push(...getRendererMeasuredAlternativeEvidenceErrors(technique));
1791
-
1792
- if (
1793
- technique.productRepresentation === "text" &&
1794
- technique.rendererWorkload !== "text-output" &&
1795
- !technique.intentionalRasterizationReason?.trim()
1796
- ) {
1797
- errors.push(
1798
- 'productRepresentation "text" requires rendererWorkload "text-output" unless intentionalRasterizationReason is provided.',
1799
- );
1800
- }
1801
-
1802
- if (
1803
- technique.productRepresentation === "vector" &&
1804
- technique.rendererWorkload !== "vector-output" &&
1805
- !technique.intentionalRasterizationReason?.trim()
1806
- ) {
1807
- errors.push(
1808
- 'productRepresentation "vector" requires rendererWorkload "vector-output" unless intentionalRasterizationReason is provided.',
1809
- );
1810
- }
1811
-
1812
- if (
1813
- technique.productRepresentation === "pixel" &&
1814
- technique.rendererWorkload !== "pixel-output"
1815
- ) {
1816
- errors.push('productRepresentation "pixel" requires rendererWorkload "pixel-output".');
1817
- }
1818
-
1819
- if (
1820
- technique.previewRenderer !== technique.exportRenderer &&
1821
- technique.exportRenderer !== "none" &&
1822
- !technique.previewExportDifferenceReason?.trim()
1823
- ) {
1824
- errors.push("Different preview/export renderers require previewExportDifferenceReason.");
1825
- }
1826
-
1827
- if (
1828
- technique.sourceRepresentation === "reference-runtime" &&
1829
- technique.previewRenderer !== technique.rendererStrategy &&
1830
- !technique.referenceRendererChangeReason?.trim()
1831
- ) {
1832
- errors.push("Reference runtime renderer changes require referenceRendererChangeReason.");
1833
- }
1834
-
1835
- errors.push(...getRendererLayerErrors(technique));
1836
-
1837
- if (
1838
- config.usesCustomRenderer &&
1839
- technique.rendererStrategy === "canvas-2d" &&
1840
- hasDetailHeavyCanvas2DRendererLayer(technique) &&
1841
- !hasMeasuredGpuAlternativeEvidence(technique)
1842
- ) {
1843
- errors.push(
1844
- "Detail-heavy Canvas 2D renderers must include rendererTechnique.measuredAlternativeEvidence for WebGL/WebGPU stress comparison before keeping the pixel work on CPU.",
1845
- );
1846
- }
1847
-
1848
- return errors;
1849
- }
1850
-
1851
- function getPassById(
1852
- pipeline: ToolcraftRendererPipeline,
1853
- ): Map<string, ToolcraftRenderPass> {
1854
- return new Map(pipeline.passes.map((pass) => [pass.id, pass]));
1855
- }
1856
-
1857
- function hasMainThreadCanvasRasterCompositePreviewPressure(
1858
- config: ToolcraftPerformanceConfig,
1859
- ): boolean {
1860
- if (config.rendererStrategy !== "canvas-2d" || !config.rendererPipeline) {
1861
- return false;
1862
- }
1863
-
1864
- const hasMainThreadRasterPass = config.rendererPipeline.passes.some(
1865
- (pass) =>
1866
- pass.runsOn === "main" &&
1867
- (pass.kind === "rasterize" || pass.kind === "pixel-transform") &&
1868
- (pass.output === "intermediate" || pass.output === "preview") &&
1869
- (pass.quality === "full" || pass.quality === "retina"),
1870
- );
1871
- const hasMainThreadPreviewComposite = config.rendererPipeline.passes.some(
1872
- (pass) =>
1873
- pass.runsOn === "main" &&
1874
- pass.kind === "composite" &&
1875
- pass.output === "preview" &&
1876
- (pass.quality === "full" || pass.quality === "retina"),
1877
- );
1878
-
1879
- return hasMainThreadRasterPass && hasMainThreadPreviewComposite;
1880
- }
1881
-
1882
- function hasPipelineReference(value: string): boolean {
1883
- return value.trim().length > 0 && !vaguePipelineReferencePattern.test(value.trim());
1884
- }
1885
-
1886
- function getPipelineReferenceErrors(
1887
- passId: string,
1888
- field: string,
1889
- references: readonly string[] | undefined,
1890
- ): string[] {
1891
- if (!references || references.length === 0) {
1892
- return [`rendererPipeline pass "${passId}" must list ${field}.`];
1893
- }
1894
-
1895
- return references.flatMap((reference) =>
1896
- hasPipelineReference(reference)
1897
- ? []
1898
- : [
1899
- `rendererPipeline pass "${passId}" ${field} entry "${reference}" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.`,
1900
- ],
1901
- );
1902
- }
1903
-
1904
- function getPipelineInteractionForScenario(
1905
- interaction: ToolcraftPerformanceInteraction,
1906
- ): ToolcraftPipelineInteraction | null {
1907
- switch (interaction) {
1908
- case "animation-frame":
1909
- return "animation-frame";
1910
- case "animation-viewport-drag":
1911
- return "viewport-drag";
1912
- case "control-change":
1913
- return "control-change";
1914
- case "control-drag":
1915
- return "control-drag";
1916
- case "export-copy":
1917
- return "export";
1918
- case "mask-drag":
1919
- return "mask-drag";
1920
- case "media-import":
1921
- return "media-import";
1922
- case "preview-render":
1923
- return null;
1924
- case "timeline-playback":
1925
- return "timeline-playback";
1926
- case "timeline-scrub":
1927
- return "timeline-scrub";
1928
- case "viewport-zoom-stress":
1929
- return "viewport-zoom";
1930
- case "viewport-stability":
1931
- return null;
1932
- }
1933
- }
1934
-
1935
- function getRendererPipelineErrors(
1936
- _schema: ResolvedToolcraftAppSchema,
1937
- config: ToolcraftPerformanceConfig,
1938
- ): string[] {
1939
- const errors: string[] = [];
1940
- const pipeline = config.rendererPipeline;
1941
-
1942
- if (config.usesCustomRenderer && !pipeline) {
1943
- return [
1944
- "Custom renderers must declare rendererPipeline so render passes, cache keys, and invalidation are machine-checkable.",
1945
- ];
1946
- }
1947
-
1948
- if (!config.usesCustomRenderer && pipeline) {
1949
- return ["Non-custom renderer configs must omit rendererPipeline."];
1950
- }
1951
-
1952
- if (!pipeline) {
1953
- return errors;
1954
- }
1955
-
1956
- if (pipeline.passes.length === 0) {
1957
- errors.push("rendererPipeline must declare at least one render pass.");
1958
- }
1959
-
1960
- if (pipeline.interactionInvalidation.length === 0) {
1961
- errors.push(
1962
- "rendererPipeline must declare interactionInvalidation so high-frequency UI work cannot accidentally invalidate expensive passes.",
1963
- );
1964
- }
1965
-
1966
- const passIds = new Set<string>();
1967
-
1968
- for (const pass of pipeline.passes) {
1969
- const passId = pass.id.trim();
1970
-
1971
- if (!passId) {
1972
- errors.push("rendererPipeline passes must have non-empty ids.");
1973
- } else if (passIds.has(passId)) {
1974
- errors.push(`rendererPipeline pass id "${passId}" must be unique.`);
1975
- } else {
1976
- passIds.add(passId);
1977
- }
1978
-
1979
- if (!renderPassKinds.has(pass.kind)) {
1980
- errors.push(`rendererPipeline pass "${pass.id}" kind "${pass.kind}" is not supported.`);
1981
- }
1982
-
1983
- if (!renderPassRunLocations.has(pass.runsOn)) {
1984
- errors.push(
1985
- `rendererPipeline pass "${pass.id}" runsOn "${pass.runsOn}" is not supported.`,
1986
- );
1987
- }
1988
-
1989
- if (!renderPassOutputs.has(pass.output)) {
1990
- errors.push(
1991
- `rendererPipeline pass "${pass.id}" output "${pass.output}" is not supported.`,
1992
- );
1993
- }
1994
-
1995
- if (!renderPassQualities.has(pass.quality)) {
1996
- errors.push(
1997
- `rendererPipeline pass "${pass.id}" quality "${pass.quality}" is not supported.`,
1998
- );
1999
- }
2000
-
2001
- errors.push(...getPipelineReferenceErrors(pass.id, "inputs", pass.inputs));
2002
- errors.push(...getPipelineReferenceErrors(pass.id, "invalidatedBy", pass.invalidatedBy));
2003
-
2004
- if (pass.cacheKey) {
2005
- errors.push(...getPipelineReferenceErrors(pass.id, "cacheKey", pass.cacheKey));
2006
- }
2007
-
2008
- if (
2009
- cacheRequiredRenderPassKinds.has(pass.kind) &&
2010
- (!pass.cacheKey || pass.cacheKey.length === 0)
2011
- ) {
2012
- errors.push(
2013
- `rendererPipeline pass "${pass.id}" is a cache-sensitive ${pass.kind} pass and must declare cacheKey so tests can reject full recomputation on every control change.`,
2014
- );
2015
- }
2016
-
2017
- if (pass.kind === "decode") {
2018
- const hasMediaImportScenario = config.scenarios.some(
2019
- (scenario) => scenario.interaction === "media-import",
2020
- );
2021
-
2022
- if (!hasMediaImportScenario) {
2023
- errors.push(
2024
- `rendererPipeline pass "${pass.id}" decodes media, so performance scenarios must include media-import coverage.`,
2025
- );
2026
- }
2027
- }
2028
- }
2029
-
2030
- const passesById = getPassById(pipeline);
2031
- const invalidationTargets = new Set<string>();
2032
- const pipelineInteractionSet = new Set(
2033
- pipeline.interactionInvalidation.map((invalidation) => invalidation.interaction),
2034
- );
2035
-
2036
- for (const invalidation of pipeline.interactionInvalidation) {
2037
- if (!pipelineInteractions.has(invalidation.interaction)) {
2038
- errors.push(
2039
- `rendererPipeline interaction "${invalidation.interaction}" is not supported.`,
2040
- );
2041
- }
2042
-
2043
- errors.push(
2044
- ...getPipelineReferenceErrors(
2045
- invalidation.interaction,
2046
- "targets",
2047
- invalidation.targets,
2048
- ),
2049
- );
2050
-
2051
- for (const target of invalidation.targets) {
2052
- if (hasPipelineReference(target)) {
2053
- invalidationTargets.add(target);
2054
- }
2055
- }
2056
-
2057
- const mustNotInvalidate = new Set(invalidation.mustNotInvalidate ?? []);
2058
-
2059
- for (const passId of invalidation.invalidates) {
2060
- if (!passId.trim()) {
2061
- errors.push(
2062
- `rendererPipeline ${invalidation.interaction} invalidates contains an empty pass id.`,
2063
- );
2064
- continue;
2065
- }
2066
-
2067
- const pass = passesById.get(passId);
2068
-
2069
- if (!pass) {
2070
- errors.push(
2071
- `rendererPipeline ${invalidation.interaction} invalidates unknown pass "${passId}".`,
2072
- );
2073
- continue;
2074
- }
2075
-
2076
- if (mustNotInvalidate.has(passId)) {
2077
- errors.push(
2078
- `rendererPipeline ${invalidation.interaction} cannot both invalidate and mustNotInvalidate pass "${passId}".`,
2079
- );
2080
- }
2081
-
2082
- if (
2083
- highFrequencyViewportInteractions.has(invalidation.interaction) &&
2084
- expensiveRenderPassKinds.has(pass.kind)
2085
- ) {
2086
- errors.push(
2087
- `rendererPipeline ${invalidation.interaction} must not invalidate expensive pass "${passId}" (${pass.kind}). Move viewport work to transforms/uniforms or explain it through a cheaper pass.`,
2088
- );
2089
- }
2090
- }
2091
-
2092
- for (const passId of invalidation.mustNotInvalidate ?? []) {
2093
- if (!passId.trim()) {
2094
- errors.push(
2095
- `rendererPipeline ${invalidation.interaction} mustNotInvalidate contains an empty pass id.`,
2096
- );
2097
- } else if (!passesById.has(passId)) {
2098
- errors.push(
2099
- `rendererPipeline ${invalidation.interaction} mustNotInvalidate unknown pass "${passId}".`,
2100
- );
2101
- }
2102
- }
2103
- }
2104
-
2105
- for (const scenario of config.scenarios) {
2106
- const pipelineInteraction = getPipelineInteractionForScenario(scenario.interaction);
2107
-
2108
- if (pipelineInteraction && !pipelineInteractionSet.has(pipelineInteraction)) {
2109
- errors.push(
2110
- `Performance scenario ${scenario.id} exercises ${pipelineInteraction}, so rendererPipeline.interactionInvalidation must declare that interaction.`,
2111
- );
2112
- }
2113
- }
2114
-
2115
- for (const target of config.workloadTargets) {
2116
- if (!invalidationTargets.has(target)) {
2117
- errors.push(
2118
- `Performance workload target ${target} must appear in rendererPipeline interactionInvalidation targets.`,
2119
- );
2120
- }
2121
- }
2122
-
2123
- return errors;
2124
- }
2125
-
2126
- export function validateToolcraftPerformanceCoverage(
2127
- schema: ResolvedToolcraftAppSchema,
2128
- config: ToolcraftPerformanceConfig,
2129
- ): string[] {
2130
- const errors: string[] = [];
2131
- const scenariosByTarget = new Map<string, ToolcraftPerformanceScenario[]>();
2132
- const browserTestNamesByScenario = new Map<string, string>();
2133
- const customRendererStrategies = new Set<ToolcraftRendererStrategy>([
2134
- "dom",
2135
- "svg",
2136
- "canvas-2d",
2137
- "webgl",
2138
- "webgpu",
2139
- ]);
2140
- const gpuRendererStrategies = new Set<ToolcraftRendererStrategy>(["webgl", "webgpu"]);
2141
- const controlsByTarget = new Map(
2142
- getAllSchemaControls(schema).map((control) => [control.target, control] as const),
2143
- );
2144
-
2145
- errors.push(...getBrowserCheckPolicyErrors(config));
2146
- errors.push(...getRendererTechniqueErrors(config));
2147
- errors.push(...getRendererPipelineErrors(schema, config));
2148
-
2149
- if (config.usesCustomRenderer && !customRendererStrategies.has(config.rendererStrategy)) {
2150
- errors.push(
2151
- 'Custom renderers must declare rendererStrategy "dom", "svg", "canvas-2d", "webgl", or "webgpu".',
2152
- );
2153
- }
2154
-
2155
- if (!config.usesCustomRenderer && config.rendererStrategy !== "none") {
2156
- errors.push(
2157
- `Non-custom renderer configs must use rendererStrategy "none", received "${config.rendererStrategy}".`,
2158
- );
2159
- }
2160
-
2161
- if (config.usesCustomRenderer && config.rendererWorkload === "none") {
2162
- errors.push(
2163
- 'Custom renderers must declare rendererWorkload "simple-composition", "text-output", "vector-output", or "pixel-output".',
2164
- );
2165
- }
2166
-
2167
- if (!config.usesCustomRenderer && config.rendererWorkload !== "none") {
2168
- errors.push(
2169
- `Non-custom renderer configs must use rendererWorkload "none", received "${config.rendererWorkload}".`,
2170
- );
2171
- }
2172
-
2173
- if (
2174
- hasMainThreadCanvasRasterCompositePreviewPressure(config) &&
2175
- config.rendererWorkload !== "pixel-output"
2176
- ) {
2177
- errors.push(
2178
- `Canvas 2D pipelines with main-thread rasterize/composite preview pressure must use rendererWorkload "pixel-output" or move expensive passes off the main thread; received "${config.rendererWorkload}".`,
2179
- );
2180
- }
2181
-
2182
- if (
2183
- config.rendererWorkload === "pixel-output" &&
2184
- !gpuRendererStrategies.has(config.rendererStrategy) &&
2185
- (!config.rendererTechnique || !hasMeasuredGpuAlternativeEvidence(config.rendererTechnique))
2186
- ) {
2187
- errors.push(
2188
- `rendererWorkload "pixel-output" should use rendererStrategy "webgl" or "webgpu", received "${config.rendererStrategy}". Keeping a CPU renderer requires rendererTechnique.measuredAlternativeEvidence for WebGL/WebGPU stress comparison.`,
2189
- );
2190
- }
2191
-
2192
- if (config.rendererWorkload === "pixel-output") {
2193
- if (!hasStressPreviewOrAnimationScenario(config)) {
2194
- errors.push(
2195
- 'rendererWorkload "pixel-output" must include a stress preview-render or animation-frame scenario with stress: true for the largest product canvas and heaviest workload values.',
2196
- );
2197
- }
2198
-
2199
- if (!hasLongTaskBudgetScenario(config)) {
2200
- errors.push(
2201
- 'rendererWorkload "pixel-output" must include at least one maxLongTaskMs budget so GPU-backed previews cannot pass while freezing the main thread.',
2202
- );
2203
- }
2204
- }
2205
-
2206
- if (config.usesCustomRenderer && hasDetailHeavyRendererLayer(config.rendererTechnique)) {
2207
- if (!hasStressPreviewOrAnimationScenario(config)) {
2208
- errors.push(
2209
- "Detail-heavy custom renderers must include a stress preview-render or animation-frame scenario for the largest product canvas and heaviest workload values.",
2210
- );
2211
- }
2212
-
2213
- if (!hasLongTaskBudgetScenario(config)) {
2214
- errors.push(
2215
- "Detail-heavy custom renderers must include at least one maxLongTaskMs budget so renderer technology can be revised when main-thread work stalls.",
2216
- );
2217
- }
2218
- }
2219
-
2220
- if (
2221
- config.usesCustomRenderer &&
2222
- hasHighCountCanvas2DRendererLayer(config.rendererTechnique) &&
2223
- !hasStressPreviewOrAnimationScenario(config)
2224
- ) {
2225
- errors.push(
2226
- "High-count Canvas 2D renderer layers must include stress preview-render or animation-frame evidence before delivery. If that stress evidence fails, revise renderer strategy instead of only lowering product workload.",
2227
- );
2228
- }
2229
-
2230
- for (const scenario of config.scenarios) {
2231
- if (!scenario.id.trim()) {
2232
- errors.push("Performance scenario is missing an id.");
2233
- }
2234
-
2235
- if (!scenario.fixture.trim()) {
2236
- errors.push(`${scenario.id} must name a representative fixture.`);
2237
- }
2238
-
2239
- if (!scenario.expectedObservable.trim()) {
2240
- errors.push(`${scenario.id} must describe a product-level performance observable.`);
2241
- }
2242
-
2243
- if (!hasAnyBudget(scenario.budget)) {
2244
- errors.push(`${scenario.id} must declare at least one numeric performance budget.`);
2245
- }
2246
-
2247
- const missingBudgetFields = getMissingInteractionBudgetFields(scenario);
2248
- if (missingBudgetFields.length > 0) {
2249
- errors.push(
2250
- `${scenario.id} ${scenario.interaction} scenario must declare ${missingBudgetFields.join(
2251
- " and ",
2252
- )}.`,
2253
- );
2254
- }
2255
-
2256
- errors.push(...getBudgetCapErrors(scenario));
2257
-
2258
- if (!scenario.automated || !scenario.automatedTestName.trim()) {
2259
- errors.push(`${scenario.id} must point to an automated performance test.`);
2260
- }
2261
-
2262
- if (!scenario.browser || !scenario.browserTestName.trim()) {
2263
- errors.push(`${scenario.id} must point to a browser performance test.`);
2264
- }
2265
-
2266
- if (scenario.browser && scenario.browserTestName.trim()) {
2267
- const previousScenarioId = browserTestNamesByScenario.get(scenario.browserTestName);
2268
-
2269
- if (previousScenarioId) {
2270
- errors.push(
2271
- `${scenario.id} browserTestName "${scenario.browserTestName}" is already used by ${previousScenarioId}. Give each performance scenario its own browser test so every control is actually exercised.`,
2272
- );
2273
- } else {
2274
- browserTestNamesByScenario.set(scenario.browserTestName, scenario.id);
2275
- }
2276
- }
2277
-
2278
- if (
2279
- scenario.workload &&
2280
- scenario.interaction !== "media-import" &&
2281
- !hasMinDefaultMax(scenario.values)
2282
- ) {
2283
- errors.push(`${scenario.id} workload scenario must include min/default/max values.`);
2284
- }
2285
-
2286
- const scenarioControl = controlsByTarget.get(scenario.target ?? "");
2287
- errors.push(...getStressFixtureErrors(scenario, scenarioControl));
2288
- errors.push(...getWorkloadFixtureErrors(config, scenario, scenarioControl));
2289
-
2290
- if (
2291
- requiresConcreteUiTarget(scenario.interaction) &&
2292
- !scenario.controlLabel?.trim() &&
2293
- !scenario.uiSelector?.trim()
2294
- ) {
2295
- errors.push(
2296
- `${scenario.id} ${scenario.interaction} scenario must declare controlLabel or uiSelector for its real browser interaction.`,
2297
- );
2298
- }
2299
-
2300
- if (scenario.target) {
2301
- const scenarios = scenariosByTarget.get(scenario.target) ?? [];
2302
- scenarios.push(scenario);
2303
- scenariosByTarget.set(scenario.target, scenarios);
2304
- }
2305
- }
2306
-
2307
- const performanceTargets = new Set(config.workloadTargets);
2308
- const schemaTargets = new Set(
2309
- (schema.panels.controls?.sections ?? []).flatMap((section) =>
2310
- Object.values(section.controls).map((control) => control.target),
2311
- ),
2312
- );
2313
- const sensitiveTargets = new Set(
2314
- collectToolcraftPerformanceSensitiveControls(schema)
2315
- .map((entry) => entry.target)
2316
- .filter((target) => !isToolcraftRuntimeOwnedTarget(target)),
2317
- );
2318
- const performanceRoleConflicts = collectToolcraftPerformanceRoleConflicts(schema);
2319
-
2320
- for (const { controlId, target } of performanceRoleConflicts) {
2321
- errors.push(
2322
- `${controlId} (${target}) looks performance-sensitive but declares performanceRole "responsiveness". Use performanceRole "workload" with workloadTargets and min/default/max coverage, or rename/restructure the control if it is truly lightweight.`,
2323
- );
2324
- }
2325
-
2326
- for (const target of sensitiveTargets) {
2327
- if (!performanceTargets.has(target)) {
2328
- errors.push(
2329
- `${target} is performance-sensitive and must be listed in workloadTargets with min/default/max workload coverage.`,
2330
- );
2331
- }
2332
- }
2333
-
2334
- for (const target of performanceTargets) {
2335
- if (!schemaTargets.has(target)) {
2336
- errors.push(`Performance workload target ${target} does not exist in schema controls.`);
2337
- }
2338
-
2339
- const targetScenarios = scenariosByTarget.get(target) ?? [];
2340
- const targetControl = controlsByTarget.get(target);
2341
- const targetRequiresDrag = getSliderDragControlType(targetControl) !== null;
2342
- const hasWorkloadCoverage = targetScenarios.some(
2343
- (scenario) =>
2344
- scenario.workload &&
2345
- (targetRequiresDrag
2346
- ? scenario.interaction === "control-drag"
2347
- : scenario.interaction === "control-drag" ||
2348
- scenario.interaction === "control-change") &&
2349
- hasMinDefaultMax(scenario.values),
2350
- );
2351
-
2352
- if (!hasWorkloadCoverage) {
2353
- errors.push(
2354
- targetRequiresDrag
2355
- ? `${target} must have min/default/max workload performance coverage through a real control-drag scenario.`
2356
- : `${target} must have min/default/max workload performance coverage.`,
2357
- );
2358
- }
2359
- }
2360
-
2361
- for (const target of getVisiblePerformanceControlTargets(schema)) {
2362
- if (isToolcraftRuntimeOwnedTarget(target)) {
2363
- continue;
2364
- }
2365
-
2366
- const targetScenarios = scenariosByTarget.get(target) ?? [];
2367
- const targetControl = controlsByTarget.get(target);
2368
- const sliderControlType = getSliderDragControlType(targetControl);
2369
-
2370
- if (targetScenarios.length === 0) {
2371
- errors.push(
2372
- `${target} must have a performance scenario because every visible control can affect app responsiveness.`,
2373
- );
2374
- } else if (sliderControlType && !hasControlDragScenario(targetScenarios)) {
2375
- errors.push(
2376
- `${target} is a ${sliderControlType} and must have a control-drag performance scenario proving live canvas/product feedback while dragging.`,
2377
- );
2378
- }
2379
- }
2380
-
2381
- if (hasOutputDeliveryAction(schema)) {
2382
- const interactions = new Set(config.scenarios.map((scenario) => scenario.interaction));
2383
-
2384
- if (!interactions.has("export-copy")) {
2385
- errors.push("Output actions must include an export-copy performance scenario.");
2386
- }
2387
- }
2388
-
2389
- if (config.usesCustomRenderer) {
2390
- const interactions = new Set(config.scenarios.map((scenario) => scenario.interaction));
2391
- const hasAnimatedRendererScenario = interactions.has("animation-frame");
2392
- const needsViewportZoomStress =
2393
- hasAnimatedRendererScenario || hasZoomSensitiveRenderer(config);
2394
-
2395
- for (const requiredInteraction of [
2396
- "preview-render",
2397
- "control-drag",
2398
- "viewport-stability",
2399
- ] as const) {
2400
- if (!interactions.has(requiredInteraction)) {
2401
- errors.push(
2402
- `Custom renderers must include a ${requiredInteraction} performance scenario.`,
2403
- );
2404
- }
2405
- }
2406
-
2407
- if (hasAnimatedRendererScenario) {
2408
- const hasAnimatedViewportDrag = config.scenarios.some(
2409
- (scenario) =>
2410
- scenario.interaction === "animation-viewport-drag" &&
2411
- scenario.stress === true,
2412
- );
2413
-
2414
- if (!hasAnimatedViewportDrag) {
2415
- errors.push(
2416
- "Animated custom renderers must include an animation-viewport-drag performance scenario that samples frames while physically moving the canvas viewport.",
2417
- );
2418
- }
2419
- }
2420
-
2421
- if (needsViewportZoomStress) {
2422
- const hasViewportZoomStress = config.scenarios.some(
2423
- (scenario) =>
2424
- scenario.interaction === "viewport-zoom-stress" &&
2425
- scenario.stress === true,
2426
- );
2427
-
2428
- if (!hasViewportZoomStress) {
2429
- errors.push(
2430
- "Detail-heavy or animated custom renderers must include a viewport-zoom-stress performance scenario that uses real zoom controls while sampling frame gaps and long tasks.",
2431
- );
2432
- }
2433
- }
2434
-
2435
- if (schema.canvas.upload) {
2436
- const mediaImportScenarios = config.scenarios.filter(
2437
- (scenario) => scenario.interaction === "media-import",
2438
- );
2439
-
2440
- if (mediaImportScenarios.length === 0) {
2441
- errors.push(
2442
- "Custom renderers with canvas upload must include a media-import performance scenario.",
2443
- );
2444
- } else if (
2445
- !mediaImportScenarios.some(
2446
- (scenario) => scenario.workload && scenario.stressFixture?.kind === "media",
2447
- )
2448
- ) {
2449
- errors.push(
2450
- 'Custom renderers with canvas upload must include a workload media-import performance scenario with stressFixture.kind "media".',
2451
- );
2452
- }
2453
- }
2454
-
2455
- if (hasKeyframeTimeline(schema)) {
2456
- const hasKeyframeViewportStability = config.scenarios.some(
2457
- (scenario) =>
2458
- scenario.interaction === "viewport-stability" &&
2459
- scenario.target === "timeline.keyframes",
2460
- );
2461
-
2462
- if (!hasKeyframeViewportStability) {
2463
- errors.push(
2464
- 'Keyframe custom renderers must include a viewport-stability performance scenario with target "timeline.keyframes" that exercises zoom/radar, expanded keyframes, keyframe creation, and playback or scrubbing.',
2465
- );
2466
- }
2467
- }
2468
-
2469
- if (hasLayersPanel(schema)) {
2470
- const hasLayerViewportStability = config.scenarios.some(
2471
- (scenario) =>
2472
- scenario.interaction === "viewport-stability" &&
2473
- scenario.target === "layers.interactions",
2474
- );
2475
-
2476
- if (!hasLayerViewportStability) {
2477
- errors.push(
2478
- 'Layer-enabled custom renderers must include a viewport-stability performance scenario with target "layers.interactions" that exercises zoom/radar, layer selection, visibility, reorder or grouping, and selected-layer output stability.',
2479
- );
2480
- }
2481
- }
2482
- }
2483
-
2484
- return errors;
2485
- }
1
+ export * from "./performance-types";
2
+ export {
3
+ defaultToolcraftBrowserCheckPolicy,
4
+ defineToolcraftPerformance,
5
+ } from "./performance-browser-policy";
6
+ export {
7
+ collectToolcraftPerformanceSensitiveControls,
8
+ collectToolcraftUnclassifiedPerformanceControls,
9
+ } from "./performance-control-classification";
10
+ export {
11
+ getToolcraftControlPerformanceValues,
12
+ getToolcraftSchemaPerformanceValues,
13
+ requireToolcraftSchemaPerformanceValues,
14
+ } from "./performance-schema-queries";
15
+ export { validateToolcraftPerformanceCoverage } from "./performance-coverage-validator";