@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,3020 +1,121 @@
1
- import { getToolcraftControlKeyframeCapability } from "@repo/toolcraft-runtime";
2
- import type {
3
- ToolcraftActionSchema,
4
- ToolcraftControlConditionSchema,
5
- ToolcraftControlOrderRole,
6
- ToolcraftControlSchema,
7
- ResolvedToolcraftAppSchema,
8
- } from "@repo/toolcraft-runtime";
9
-
10
- import { starterSchema } from "./starter-schema";
11
-
12
- export type ToolcraftAcceptanceEvidence =
13
- | "command-side-effect"
14
- | "exported-bytes"
15
- | "media-lifecycle"
16
- | "persistence-state"
17
- | "product-output"
18
- | "rendered-pixels"
19
- | "timeline-output"
20
- | "viewport-side-effect";
21
-
22
- export type ToolcraftReferenceCoverage =
23
- | "canvas-sizing"
24
- | "control-mapping"
25
- | "export-at-time"
26
- | "export-copy"
27
- | "media-lifecycle"
28
- | "pause-resume"
29
- | "renderer-loop"
30
- | "renderer-state"
31
- | "restart"
32
- | "spawn-update-cadence"
33
- | "time-progress";
34
-
35
- export type ToolcraftReferenceTimelineCoverage =
36
- | "all-range"
37
- | "duration"
38
- | "export-at-time"
39
- | "export-range"
40
- | "jump-to-trim-start"
41
- | "keyframes"
42
- | "loop"
43
- | "playback"
44
- | "range-playback"
45
- | "restart"
46
- | "scrub"
47
- | "state-jump"
48
- | "time-progress"
49
- | "trim-range";
50
-
51
- export type ToolcraftTimelinePlaybackCoverage =
52
- | "duration"
53
- | "loop"
54
- | "pause-resume"
55
- | "rendered-frame"
56
- | "scrub";
57
-
58
- export type ToolcraftCanvasSizingCoverage = "fixed-output-size" | "intrinsic-media-size";
59
-
60
- export type ToolcraftPersistenceCoverage = "reload";
61
-
62
- export type ToolcraftSettingsTransferCoverage = "opt-out";
63
-
64
- export type ToolcraftAutonomousAnimationCoverage =
65
- | "no-duration-control"
66
- | "no-export-at-time"
67
- | "no-loop-control"
68
- | "no-play-pause"
69
- | "no-scrub"
70
- | "no-user-facing-transport";
71
-
72
- export type ToolcraftTimelineLoopDurationSource =
73
- | "product-derived"
74
- | "reference"
75
- | "user-request";
76
-
77
- export type ToolcraftTimelineLoopDurationIntent = {
78
- evidence: string;
79
- seconds: number;
80
- source: ToolcraftTimelineLoopDurationSource;
81
- };
82
-
83
- export type ToolcraftAnimationIntent =
84
- | {
85
- mode: "none";
86
- }
87
- | {
88
- behaviorCoverage: readonly ToolcraftAutonomousAnimationCoverage[];
89
- mode: "autonomous";
90
- reason: string;
91
- }
92
- | {
93
- loopDuration: ToolcraftTimelineLoopDurationIntent;
94
- mode: "timeline-keyframes";
95
- }
96
- | {
97
- loopDuration: ToolcraftTimelineLoopDurationIntent;
98
- mode: "timeline-playback";
99
- };
100
-
101
- export type ToolcraftReferenceTimelineMode =
102
- | "custom-reference-timeline"
103
- | "none"
104
- | "toolcraft-keyframes"
105
- | "toolcraft-playback";
106
-
107
- export type ToolcraftReferenceTimelineContract = {
108
- behaviorCoverage: readonly ToolcraftReferenceTimelineCoverage[];
109
- loopDuration?: ToolcraftTimelineLoopDurationIntent;
110
- mode: ToolcraftReferenceTimelineMode;
111
- };
112
-
113
- export type ToolcraftReferenceStudyStatus =
114
- | "ran-original"
115
- | "restored-local"
116
- | "source-inspection-only";
117
-
118
- export type ToolcraftReferenceStudyEvidence = {
119
- behaviorEvidence: string;
120
- referenceLocation: string;
121
- reproductionSteps: string;
122
- sourceEvidence: string;
123
- sourceOnlyReason?: string;
124
- status: ToolcraftReferenceStudyStatus;
125
- };
126
-
127
- export type ToolcraftVideoReferenceStoryboardFrame = {
128
- behaviorObservation: string;
129
- frameId: string;
130
- frameSource: string;
131
- timeSeconds: number;
132
- visualObservation: string;
133
- };
134
-
135
- export type ToolcraftVideoReferenceTransition = {
136
- behaviorDelta: string;
137
- fromFrameId: string;
138
- id: string;
139
- toFrameId: string;
140
- };
141
-
142
- export type ToolcraftVideoReferenceAcceptanceMapping = {
143
- acceptanceId: string;
144
- behavior: string;
145
- frameIds: readonly string[];
146
- };
147
-
148
- export type ToolcraftVideoReferenceStudyEvidence = {
149
- acceptanceMapping: readonly ToolcraftVideoReferenceAcceptanceMapping[];
150
- behaviorDecomposition: string;
151
- extractionEvidence: string;
152
- referenceLocation: string;
153
- storyboard: readonly ToolcraftVideoReferenceStoryboardFrame[];
154
- transitionAnalysis: readonly ToolcraftVideoReferenceTransition[];
155
- };
156
-
157
- export type ToolcraftReferenceFeatureStatus =
158
- | "intentionally-changed"
159
- | "ported"
160
- | "toolcraft-native";
161
-
162
- export type ToolcraftReferenceFeatureInventoryItem = {
163
- acceptanceId: string;
164
- behaviorEvidence: string;
165
- featureName: string;
166
- id: string;
167
- referenceBehavior: string;
168
- sourceEvidence: string;
169
- status: ToolcraftReferenceFeatureStatus;
170
- toolcraftMapping: string;
171
- userApprovedChangeReason?: string;
172
- };
173
-
174
- export type ToolcraftLayerCoverage =
175
- | "grouping"
176
- | "media-lifecycle"
177
- | "reorder"
178
- | "selected-layer-controls"
179
- | "selection"
180
- | "visibility";
181
-
182
- export type ToolcraftControlPartCoverage =
183
- | "anchorGrid.position"
184
- | "channelMixer.activeChannel"
185
- | "channelMixer.values"
186
- | "curves.activeChannel"
187
- | "curves.points"
188
- | "colorOpacity.hex"
189
- | "colorOpacity.opacity"
190
- | "fontPicker.color"
191
- | "fontPicker.fontId"
192
- | "fontPicker.fontSize"
193
- | "fontPicker.fontWeight"
194
- | "fontPicker.letterSpacing"
195
- | "fontPicker.lineHeight"
196
- | "fontPicker.opacity"
197
- | "fontPicker.textCase"
198
- | "gradient.angle"
199
- | "gradient.gradientType"
200
- | "gradient.stops.color"
201
- | "gradient.stops.opacity"
202
- | "gradient.stops.position"
203
- | "palette.family"
204
- | "palette.shade"
205
- | "rangeInput.end"
206
- | "rangeInput.start"
207
- | "rangeSlider.lower"
208
- | "rangeSlider.upper"
209
- | "vector.x"
210
- | "vector.y";
211
-
212
- export type ToolcraftCustomControlCoverage =
213
- | "built-in-gap"
214
- | "kit-primitives"
215
- | "minimal-ui"
216
- | "product-output"
217
- | "runtime-state";
218
-
219
- const builtInToolcraftControlTypeValues = [
220
- "actions",
221
- "anchorGrid",
222
- "aspectRatio",
223
- "channelMixer",
224
- "checkbox",
225
- "code",
226
- "collectionActions",
227
- "color",
228
- "colorOpacity",
229
- "curves",
230
- "fileDrop",
231
- "fontPicker",
232
- "gradient",
233
- "imagePicker",
234
- "palette",
235
- "panelActions",
236
- "rangeInput",
237
- "rangeSlider",
238
- "segmented",
239
- "select",
240
- "settingsTransfer",
241
- "slider",
242
- "switch",
243
- "text",
244
- "vector",
245
- ] as const;
246
- const insufficientFixedCanvasSizingReasonPattern =
247
- /\b(no|without|missing|lacks?|does\s+not\s+(?:have|expose)|did\s+not\s+(?:have|expose)|has\s+no|had\s+no)\b[^.]{0,80}\b(?:size|dimension|canvas|output)\b[^.]{0,80}\b(?:editor|controls?|settings?|picker|input|ui)\b|\breference\s+app\b[^.]{0,120}\b(?:no|without|missing|lacks?|does\s+not\s+(?:have|expose)|did\s+not\s+(?:have|expose)|has\s+no|had\s+no)\b[^.]{0,80}\b(?:size|dimension|canvas|output)\b/i;
248
-
249
- export type ToolcraftBuiltInControlType =
250
- (typeof builtInToolcraftControlTypeValues)[number];
251
-
252
- export type ToolcraftBuiltInFitCheck = {
253
- checkedBuiltIns: readonly ToolcraftBuiltInControlType[];
254
- closestBuiltIn: ToolcraftBuiltInControlType | "none";
255
- productObservable: string;
256
- whyInsufficient: string;
257
- };
258
-
259
- export type ToolcraftTransferMode =
260
- | {
261
- animationIntent?: ToolcraftAnimationIntent;
262
- mode: "new-toolcraft-app";
263
- videoReferenceStudy?: ToolcraftVideoReferenceStudyEvidence;
264
- }
265
- | {
266
- animationIntent?: ToolcraftAnimationIntent;
267
- behaviorCoverage: readonly ToolcraftReferenceCoverage[];
268
- mode: "reference-runtime-clone";
269
- referenceFeatureInventory?: readonly ToolcraftReferenceFeatureInventoryItem[];
270
- referenceName: string;
271
- referenceStudy?: ToolcraftReferenceStudyEvidence;
272
- referenceTimeline: ToolcraftReferenceTimelineContract;
273
- sourceOfTruth: "reference-runtime";
274
- videoReferenceStudy?: ToolcraftVideoReferenceStudyEvidence;
275
- };
276
-
277
- export type ToolcraftProductReadiness =
278
- | {
279
- mode: "starter";
280
- reason: string;
281
- }
282
- | {
283
- mode: "product";
284
- productName: string;
285
- productSummary: string;
286
- requestedBehavior: string;
287
- };
288
-
289
- export type ToolcraftComponentAcceptance = {
290
- actionCoverage?: readonly string[];
291
- automated: boolean;
292
- automatedTestName: string;
293
- browser: boolean;
294
- browserTestName: string;
295
- componentType: string;
296
- evidence: ToolcraftAcceptanceEvidence;
297
- expectedObservable: string;
298
- fixture: string;
299
- id: string;
300
- canvasHandle?: {
301
- exportCleanTestName: string;
302
- outputObservable: string;
303
- testId: string;
304
- writesTarget: string;
305
- };
306
- kind: "canvas-handle" | "control" | "runtime";
307
- canvasSizingCoverage?: ToolcraftCanvasSizingCoverage;
308
- layerCoverage?: ToolcraftLayerCoverage;
309
- optionCoverage?: "each-visible-item" | readonly string[];
310
- persistenceCoverage?: ToolcraftPersistenceCoverage;
311
- referenceCoverage?: ToolcraftReferenceCoverage;
312
- referenceTimelineCoverage?: ToolcraftReferenceTimelineCoverage;
313
- settingsTransferCoverage?: ToolcraftSettingsTransferCoverage;
314
- target?: string;
315
- timelineCoverage?: "keyframes" | "playback";
316
- timelinePlaybackCoverage?:
317
- | "all-playback-behavior"
318
- | readonly ToolcraftTimelinePlaybackCoverage[];
319
- controlPartCoverage?:
320
- | "all-visible-parts"
321
- | readonly ToolcraftControlPartCoverage[];
322
- customControlCoverage?:
323
- | "all-custom-control-behavior"
324
- | readonly ToolcraftCustomControlCoverage[];
325
- builtInFitCheck?: ToolcraftBuiltInFitCheck;
326
- userAction: string;
327
- };
328
-
329
- export type ToolcraftVisibleControl = {
330
- control: ToolcraftControlSchema;
331
- controlId: string;
332
- sectionTitle?: string;
333
- };
334
-
335
- export type ToolcraftControlOrderItem = {
336
- controlId: string;
337
- rank: number;
338
- role: ToolcraftControlOrderRole;
339
- sectionTitle?: string;
340
- target: string;
341
- type: string;
342
- };
343
-
344
- export type ToolcraftControlSectionInventoryEntry = {
345
- entity?: string;
346
- groupingReason: string;
347
- splitReason?: string;
348
- targets: readonly string[];
349
- title: string;
350
- workflowStage?: string;
351
- };
352
-
353
- export const starterTransferMode: ToolcraftTransferMode = {
354
- animationIntent: { mode: "none" },
355
- mode: "new-toolcraft-app",
356
- };
357
-
358
- export const starterProductReadiness: ToolcraftProductReadiness = {
359
- mode: "starter",
360
- reason:
361
- "Neutral Toolcraft template before a product schema, renderer, and acceptance matrix are authored.",
362
- };
363
-
364
- export const starterAcceptance: readonly ToolcraftComponentAcceptance[] = [];
365
-
366
- export const starterControlSectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = [];
367
-
368
- function getActionValue(action: ToolcraftActionSchema | string): string {
369
- return typeof action === "string" ? action : action.value;
370
- }
371
-
372
- function getActionLabelText(action: ToolcraftActionSchema | string): string {
373
- return typeof action === "string" ? action : (action.label ?? action.value);
374
- }
375
-
376
- function getActionSearchText(action: ToolcraftActionSchema | string): string {
377
- return typeof action === "string" ? action : `${action.label} ${action.value} ${action.command ?? ""}`;
378
- }
379
-
380
- function isCanvasSizeTarget(target: string): boolean {
381
- return target === "canvas.size.width" || target === "canvas.size.height";
382
- }
383
-
384
- const runtimeSetupControlTargets = new Set([
385
- "runtime.settingsTransfer",
386
- "canvas.aspectRatio",
387
- "canvas.renderScale",
388
- "canvas.size.width",
389
- "canvas.size.height",
390
- "panels.timeline.extended",
391
- ]);
392
-
393
- function isRuntimeSetupControlTarget(target: string): boolean {
394
- return runtimeSetupControlTargets.has(target);
395
- }
396
-
397
- function isResetPanelAction(action: ToolcraftActionSchema | string): boolean {
398
- return /\breset\b/i.test(getActionSearchText(action));
399
- }
400
-
401
- function getControlOptionValues(control: ToolcraftControlSchema): readonly string[] {
402
- if (control.type === "imagePicker") {
403
- return control.items?.map((item) => item.value) ?? [];
404
- }
405
-
406
- return control.options?.map((option) => option.value) ?? [];
407
- }
408
-
409
- function hasCoverageForValues(
410
- coverage: ToolcraftComponentAcceptance["actionCoverage"] | ToolcraftComponentAcceptance["optionCoverage"],
411
- values: readonly string[],
412
- ): boolean {
413
- if (values.length === 0) {
414
- return true;
415
- }
416
-
417
- if (coverage === "each-visible-item") {
418
- return true;
419
- }
420
-
421
- if (!Array.isArray(coverage)) {
422
- return false;
423
- }
424
-
425
- return values.every((value) => coverage.includes(value));
426
- }
427
-
428
- function hasControlPartCoverage(
429
- coverage: ToolcraftComponentAcceptance["controlPartCoverage"],
430
- requiredParts: readonly ToolcraftControlPartCoverage[],
431
- ): boolean {
432
- if (requiredParts.length === 0) {
433
- return true;
434
- }
435
-
436
- if (coverage === "all-visible-parts") {
437
- return true;
438
- }
439
-
440
- if (!Array.isArray(coverage)) {
441
- return false;
442
- }
443
-
444
- return requiredParts.every((part) => coverage.includes(part));
445
- }
446
-
447
- function hasCustomControlCoverage(
448
- coverage: ToolcraftComponentAcceptance["customControlCoverage"],
449
- requiredParts: readonly ToolcraftCustomControlCoverage[],
450
- ): boolean {
451
- if (coverage === "all-custom-control-behavior") {
452
- return true;
453
- }
454
-
455
- if (!Array.isArray(coverage)) {
456
- return false;
457
- }
458
-
459
- return requiredParts.every((part) => coverage.includes(part));
460
- }
461
-
462
- function hasTimelinePlaybackCoverage(
463
- coverage: ToolcraftComponentAcceptance["timelinePlaybackCoverage"],
464
- requiredParts: readonly ToolcraftTimelinePlaybackCoverage[],
465
- ): boolean {
466
- if (coverage === "all-playback-behavior") {
467
- return true;
468
- }
469
-
470
- if (!Array.isArray(coverage)) {
471
- return false;
472
- }
473
-
474
- return requiredParts.every((part) => coverage.includes(part));
475
- }
476
-
477
- function hasTimelinePlaybackCoveragePart(
478
- coverage: ToolcraftComponentAcceptance["timelinePlaybackCoverage"],
479
- part: ToolcraftTimelinePlaybackCoverage,
480
- ): boolean {
481
- return coverage === "all-playback-behavior" || (Array.isArray(coverage) && coverage.includes(part));
482
- }
483
-
484
- function getAcceptanceEvidenceText(entry: ToolcraftComponentAcceptance): string {
485
- return [
486
- entry.automatedTestName,
487
- entry.browserTestName,
488
- entry.expectedObservable,
489
- entry.fixture,
490
- entry.userAction,
491
- ].join(" ");
492
- }
493
-
494
- function getTimelineLoopDurationIntent(
495
- animationIntent: ToolcraftAnimationIntent | undefined,
496
- ): ToolcraftTimelineLoopDurationIntent | undefined {
497
- if (
498
- animationIntent?.mode !== "timeline-playback" &&
499
- animationIntent?.mode !== "timeline-keyframes"
500
- ) {
501
- return undefined;
502
- }
503
-
504
- return animationIntent.loopDuration;
505
- }
506
-
507
- function isValidTimelineLoopDurationSource(source: string): boolean {
508
- return source === "reference" || source === "user-request" || source === "product-derived";
509
- }
510
-
511
- const runtimeDefaultLoopDurationEvidencePattern =
512
- /\b(?:runtime|template|generic|fallback|default)\b.{0,40}\b(?:8\s*s|8\s*sec(?:ond)?s?|eight\s*sec(?:ond)?s?)\b|\b(?:8\s*s|8\s*sec(?:ond)?s?|eight\s*sec(?:ond)?s?)\b.{0,40}\b(?:runtime|template|generic|fallback|default)\b/i;
513
-
514
- function hasSeamlessForwardLoopEvidence(text: string): boolean {
515
- const hasDurationChange =
516
- /\b(duration|range|state\.timeline\.durationSeconds|timeline)\b/i.test(text) &&
517
- /\b(edit|change|changed|changing|commit|set|after)\w*\b/i.test(text);
518
- const hasFirstLastFrameEvidence =
519
- /\b(first[-\s/]*(?:and\s+)?last|last[-\s/]*(?:and\s+)?first|wrapped\s+first\s+frame)\b/i.test(
520
- text,
521
- ) ||
522
- (/\bfirst\s+frame\b/i.test(text) && /\blast\s+frame\b/i.test(text)) ||
523
- (/\bstart\s+frame\b/i.test(text) && /\bend\s+frame\b/i.test(text));
524
- const hasNoVisibleJumpEvidence =
525
- /\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\bvisible\s+jump\b/i.test(
526
- text,
527
- );
528
- const hasSeamEvidence =
529
- hasFirstLastFrameEvidence &&
530
- (/\b(stitch(?:es|ed|ing)?|seam(?:less)?)\b/i.test(text) || hasNoVisibleJumpEvidence);
531
- const hasForwardDirectionEvidence =
532
- /\b(forward[-\s]*only|one\s+direction|same\s+direction|advances?\s+in\s+one\s+direction|motion\s+advances?\s+forward|direction\s+does\s+not\s+reverse)\b/i.test(text);
533
- const hasNoMirrorEvidence =
534
- /\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\bmirror(?:ed|ing)?\b/i.test(text);
535
- const hasNoYoyoEvidence =
536
- /\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\byo-?yo\b/i.test(text);
537
- const hasNoPingPongEvidence =
538
- /\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\bping[-\s]*pong\b/i.test(text);
539
- const hasNoReverseEvidence =
540
- /\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\breverse\b/i.test(text);
541
-
542
- return (
543
- hasDurationChange &&
544
- hasSeamEvidence &&
545
- hasForwardDirectionEvidence &&
546
- hasNoMirrorEvidence &&
547
- hasNoYoyoEvidence &&
548
- hasNoPingPongEvidence &&
549
- hasNoReverseEvidence
550
- );
551
- }
552
-
553
- function getFileDropLifecycleCoverageErrors(
554
- label: string,
555
- control: ToolcraftControlSchema,
556
- entry: ToolcraftComponentAcceptance,
557
- hasDefaultMediaAssets: boolean,
558
- ): string[] {
559
- const errors: string[] = [];
560
- const evidenceText = getAcceptanceEvidenceText(entry);
561
-
562
- if (entry.evidence !== "media-lifecycle") {
563
- errors.push(
564
- `${label} fileDrop acceptance evidence must be "media-lifecycle" so upload, clear, and reset behavior cannot be replaced by generic product-output coverage.`,
565
- );
566
- }
567
-
568
- if (
569
- !/\b(upload|import|drop|drag|browse|choose|select file|source image)\b/i.test(
570
- evidenceText,
571
- ) ||
572
- !/\b(clear|remove|delete|trash)\b/i.test(evidenceText) ||
573
- !/\b(reset|reset controls|section reset|global reset)\b/i.test(evidenceText)
574
- ) {
575
- errors.push(
576
- `${label} fileDrop acceptance must prove upload/import, clear/remove, and section or global reset restore default source media or remove uploaded source media when no default exists.`,
577
- );
578
- }
579
-
580
- if (hasDefaultMediaAssets) {
581
- const provesDefaultRemoval =
582
- /\b(default|predefined|preset|attached)\b.{0,60}\b(clear|remove|delete|trash)\b/i.test(
583
- evidenceText,
584
- ) ||
585
- /\b(clear|remove|delete|trash)\b.{0,60}\b(default|predefined|preset|attached)\b/i.test(
586
- evidenceText,
587
- );
588
- const provesDefaultReset =
589
- /\b(reset|section reset|global reset)\b.{0,80}\b(restore|restores|return|returns|recreate|recreates)\b.{0,80}\b(default|predefined|preset|attached)\b/i.test(
590
- evidenceText,
591
- );
592
-
593
- if (!provesDefaultRemoval || !provesDefaultReset) {
594
- errors.push(
595
- `${label} fileDrop acceptance must prove predefined media.defaultAssets render as attached files, can be removed to an empty source/canvas state, and are restored by section or global Reset.`,
596
- );
597
- }
598
- }
599
-
600
- if (control.assetKind !== "file") {
601
- const provesRotate = /\b(rotate|rotation|90°|90\s*degrees?)\b/i.test(evidenceText);
602
- const provesFlip = /\b(flip|flipped)\b/i.test(evidenceText);
603
- const provesTransformConsumption =
604
- /\b(mediaAssets\[\]\.transform|runtime media transform|transform metadata|preview|renderer|rendered output|export)\b/i.test(
605
- evidenceText,
606
- );
607
-
608
- if (!provesRotate || !provesFlip || !provesTransformConsumption) {
609
- errors.push(
610
- `${label} image fileDrop acceptance must prove rotate and flip actions update runtime media transform metadata and that preview, renderer, or export consumes the transform.`,
611
- );
612
- }
613
- }
614
-
615
- if (control.multiple === true) {
616
- const provesReorder = /\b(reorder|sort|sorting|drag|order)\b/i.test(evidenceText);
617
- const provesOrderConsumption =
618
- /\b(runtime media order|mediaAssets order|ordered media|preview|renderer|rendered output|export)\b/i.test(
619
- evidenceText,
620
- );
621
-
622
- if (!provesReorder || !provesOrderConsumption) {
623
- errors.push(
624
- `${label} multiple fileDrop acceptance must prove thumbnail/file reorder updates runtime media order and that preview, renderer, or export consumes that order.`,
625
- );
626
- }
627
- }
628
-
629
- return errors;
630
- }
631
-
632
- const conditionOperatorLabels = [
633
- "equals",
634
- "notEquals",
635
- "oneOf",
636
- "notOneOf",
637
- "greaterThan",
638
- "greaterThanOrEqual",
639
- "lessThan",
640
- "lessThanOrEqual",
641
- ] as const satisfies readonly (keyof ToolcraftControlConditionSchema)[];
642
-
643
- function hasConditionOperator(condition: ToolcraftControlConditionSchema): boolean {
644
- return conditionOperatorLabels.some((operator) => operator in condition);
645
- }
646
-
647
- function getConditionValidationErrors({
648
- condition,
649
- conditionName,
650
- controlTargets,
651
- label,
652
- }: {
653
- condition: ToolcraftControlConditionSchema;
654
- conditionName: "disabledWhen" | "visibleWhen";
655
- controlTargets: ReadonlySet<string>;
656
- label: string;
657
- }): string[] {
658
- const errors: string[] = [];
659
-
660
- if (!hasConditionOperator(condition)) {
661
- errors.push(
662
- `${label} ${conditionName} must declare one of equals, notEquals, oneOf, notOneOf, greaterThan, greaterThanOrEqual, lessThan, or lessThanOrEqual so the dependent state is deterministic.`,
663
- );
664
- }
665
-
666
- for (const arrayOperator of ["oneOf", "notOneOf"] as const) {
667
- if (
668
- arrayOperator in condition &&
669
- (!Array.isArray(condition[arrayOperator]) ||
670
- condition[arrayOperator]?.length === 0)
671
- ) {
672
- errors.push(
673
- `${label} ${conditionName}.${arrayOperator} must be a non-empty array.`,
674
- );
675
- }
676
- }
677
-
678
- for (const numericOperator of [
679
- "greaterThan",
680
- "greaterThanOrEqual",
681
- "lessThan",
682
- "lessThanOrEqual",
683
- ] as const) {
684
- if (
685
- numericOperator in condition &&
686
- (typeof condition[numericOperator] !== "number" ||
687
- !Number.isFinite(condition[numericOperator]))
688
- ) {
689
- errors.push(
690
- `${label} ${conditionName}.${numericOperator} must be a finite number.`,
691
- );
692
- }
693
- }
694
-
695
- if (
696
- !controlTargets.has(condition.target) &&
697
- !isCanvasSizeTarget(condition.target)
698
- ) {
699
- errors.push(
700
- `${label} ${conditionName} target ${condition.target} does not match another schema control target or canvas size target.`,
701
- );
702
- }
703
-
704
- return errors;
705
- }
706
-
707
- export function getRequiredToolcraftControlPartCoverage(
708
- control: ToolcraftControlSchema,
709
- ): readonly ToolcraftControlPartCoverage[] {
710
- switch (control.type) {
711
- case "anchorGrid":
712
- return ["anchorGrid.position"];
713
- case "channelMixer":
714
- return ["channelMixer.activeChannel", "channelMixer.values"];
715
- case "curves":
716
- return control.variant === "single"
717
- ? ["curves.points"]
718
- : ["curves.activeChannel", "curves.points"];
719
- case "fontPicker":
720
- return [
721
- "fontPicker.fontId",
722
- "fontPicker.fontWeight",
723
- "fontPicker.fontSize",
724
- "fontPicker.letterSpacing",
725
- "fontPicker.lineHeight",
726
- "fontPicker.textCase",
727
- "fontPicker.color",
728
- "fontPicker.opacity",
729
- ];
730
- case "gradient":
731
- return [
732
- "gradient.gradientType",
733
- "gradient.angle",
734
- "gradient.stops.position",
735
- "gradient.stops.color",
736
- "gradient.stops.opacity",
737
- ];
738
- case "palette":
739
- return ["palette.family", "palette.shade"];
740
- case "rangeInput":
741
- return ["rangeInput.start", "rangeInput.end"];
742
- case "rangeSlider":
743
- return ["rangeSlider.lower", "rangeSlider.upper"];
744
- case "vector":
745
- return ["vector.x", "vector.y"];
746
- default:
747
- return [];
748
- }
749
- }
750
-
751
- const builtInToolcraftControlTypes = new Set<string>(
752
- builtInToolcraftControlTypeValues,
753
- );
754
-
755
- const requiredCustomControlCoverage: readonly ToolcraftCustomControlCoverage[] = [
756
- "built-in-gap",
757
- "kit-primitives",
758
- "minimal-ui",
759
- "product-output",
760
- "runtime-state",
761
- ];
762
-
763
- function isCustomToolcraftControl(control: ToolcraftControlSchema): boolean {
764
- return !builtInToolcraftControlTypes.has(control.type);
765
- }
766
-
767
- const collectionEntityCustomControlRe =
768
- /\b(collection|repeatable|list|lists|item|items|entry|entries|row|rows|asset|assets|object|objects|color|colors|swatch|swatches|glyph|glyphs|symbol|symbols|point|points|stop|stops|variant|variants|rule|rules|mask|masks|shape|shapes|layer|layers|media|image|images|file|files)\b/i;
769
- const collectionOperationCustomControlRe =
770
- /\b(add|adding|delete|deleting|remove|removing|reorder|reordering|order|ordering|sort|sorting|select|selecting|selected|selection|duplicate|duplicating|upload|import|clear|clearing)\b/i;
771
-
772
- const actionLikeCustomControlRe =
773
- /\b(add|adding|delete|deleting|remove|removing|duplicate|duplicating|sort|sorting|normalize|normalizing|clear|clearing|reset|shuffle|randomize|randomizing)\b/i;
774
-
775
- const chromeOnlyCustomControlReasonRe =
776
- /\b(icon|icons|visual|style|styling|layout|spacing|chrome|button|buttons|compact|custom look|custom ui)\b/i;
777
- const productInteractionCustomControlReasonRe =
778
- /\b(runtime|state|canvas|output|export|upload|import|preview|reorder|ordering|sort|drag|resize|handle|threshold|density|mapping|geometry|nested|multi|multiple|per-item|metadata|hit target|validation|selection)\b/i;
779
- const collectionValueKeyRe =
780
- /^(items?|entries|rows|assets|objects|colors?|glyphs?|symbols?|points?|stops?|variants?|rules?|masks?|shapes?|layers?|media|images?|files?)$/i;
781
-
782
- function isRecord(value: unknown): value is Record<string, unknown> {
783
- return typeof value === "object" && value !== null && !Array.isArray(value);
784
- }
785
-
786
- function stringifyUnknownForFitCheck(value: unknown): string {
787
- if (value == null) {
788
- return "";
789
- }
790
-
791
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
792
- return String(value);
793
- }
794
-
795
- if (Array.isArray(value)) {
796
- return value.map(stringifyUnknownForFitCheck).join(" ");
797
- }
798
-
799
- if (isRecord(value)) {
800
- return Object.entries(value)
801
- .flatMap(([key, entryValue]) => [key, stringifyUnknownForFitCheck(entryValue)])
802
- .join(" ");
803
- }
804
-
805
- return "";
806
- }
807
-
808
- function arrayLooksLikeCollectionValue(value: readonly unknown[]): boolean {
809
- return (
810
- value.length === 0 ||
811
- value.some((item) => isRecord(item) || Array.isArray(item) || typeof item === "string")
812
- );
813
- }
814
-
815
- function hasCollectionValueShape(value: unknown): boolean {
816
- if (!isRecord(value)) {
817
- return Array.isArray(value) && arrayLooksLikeCollectionValue(value);
818
- }
819
-
820
- return Object.entries(value).some(([key, entryValue]) => {
821
- if (Array.isArray(entryValue)) {
822
- return collectionValueKeyRe.test(key) || arrayLooksLikeCollectionValue(entryValue);
823
- }
824
-
825
- return collectionValueKeyRe.test(key) && isRecord(entryValue);
826
- });
827
- }
828
-
829
- function getCustomFitCheckSearchText(
830
- entry: ToolcraftComponentAcceptance,
831
- control?: ToolcraftControlSchema,
832
- ): string {
833
- return [
834
- control?.type,
835
- control?.target,
836
- typeof control?.label === "string" ? control.label : undefined,
837
- stringifyUnknownForFitCheck(control?.defaultValue),
838
- entry.id,
839
- entry.target,
840
- entry.componentType,
841
- entry.expectedObservable,
842
- entry.userAction,
843
- entry.builtInFitCheck?.whyInsufficient,
844
- entry.builtInFitCheck?.productObservable,
845
- ]
846
- .filter(Boolean)
847
- .join(" ");
848
- }
849
-
850
- function isCollectionLikeCustomControl(
851
- entry: ToolcraftComponentAcceptance,
852
- control: ToolcraftControlSchema,
853
- ): boolean {
854
- if (hasCollectionValueShape(control.defaultValue)) {
855
- return true;
856
- }
857
-
858
- const searchText = getCustomFitCheckSearchText(entry, control);
859
-
860
- return (
861
- collectionEntityCustomControlRe.test(searchText) &&
862
- collectionOperationCustomControlRe.test(searchText)
863
- );
864
- }
865
-
866
- function getBuiltInFitCheckErrors(
867
- label: string,
868
- entry: ToolcraftComponentAcceptance,
869
- control: ToolcraftControlSchema,
870
- ): string[] {
871
- const fitCheck = entry.builtInFitCheck;
872
-
873
- if (!fitCheck) {
874
- return [
875
- `${label} is a custom control and must declare builtInFitCheck with checkedBuiltIns, closestBuiltIn, whyInsufficient, and productObservable.`,
876
- ];
877
- }
878
-
879
- const errors: string[] = [];
880
- const checkedBuiltIns = Array.isArray(fitCheck.checkedBuiltIns)
881
- ? fitCheck.checkedBuiltIns
882
- : [];
883
-
884
- if (checkedBuiltIns.length === 0) {
885
- errors.push(
886
- `${label} builtInFitCheck.checkedBuiltIns must name at least one checked built-in control.`,
887
- );
888
- }
889
-
890
- const unknownCheckedBuiltIns = checkedBuiltIns.filter(
891
- (builtIn) => !builtInToolcraftControlTypes.has(builtIn),
892
- );
893
-
894
- if (unknownCheckedBuiltIns.length > 0) {
895
- errors.push(
896
- `${label} builtInFitCheck.checkedBuiltIns contains unknown built-in controls: ${unknownCheckedBuiltIns.join(", ")}.`,
897
- );
898
- }
899
-
900
- if (
901
- fitCheck.closestBuiltIn !== "none" &&
902
- !checkedBuiltIns.includes(fitCheck.closestBuiltIn)
903
- ) {
904
- errors.push(
905
- `${label} builtInFitCheck.closestBuiltIn must be one of the checked built-ins or "none".`,
906
- );
907
- }
908
-
909
- if (fitCheck.whyInsufficient.trim().length < 24) {
910
- errors.push(
911
- `${label} builtInFitCheck.whyInsufficient must explain why the closest built-in cannot express the product interaction.`,
912
- );
913
- }
914
-
915
- if (fitCheck.productObservable.trim().length < 24) {
916
- errors.push(
917
- `${label} builtInFitCheck.productObservable must name the product output or side effect that proves the custom control is necessary.`,
918
- );
919
- }
920
-
921
- const searchText = getCustomFitCheckSearchText(entry, control);
922
-
923
- if (
924
- isCollectionLikeCustomControl(entry, control) &&
925
- !checkedBuiltIns.includes("collectionActions")
926
- ) {
927
- errors.push(
928
- `${label} builtInFitCheck.checkedBuiltIns must include collectionActions when the custom control owns a growable, removable, selectable, or reorderable runtime item set.`,
929
- );
930
- }
931
-
932
- if (actionLikeCustomControlRe.test(searchText) && !checkedBuiltIns.includes("actions")) {
933
- errors.push(
934
- `${label} builtInFitCheck.checkedBuiltIns must include actions when the custom control exposes local command buttons such as add, remove, delete, duplicate, sort, normalize, or clear.`,
935
- );
936
- }
937
-
938
- if (
939
- chromeOnlyCustomControlReasonRe.test(fitCheck.whyInsufficient) &&
940
- !productInteractionCustomControlReasonRe.test(fitCheck.whyInsufficient)
941
- ) {
942
- errors.push(
943
- `${label} builtInFitCheck.whyInsufficient cannot justify a custom control only with icons, layout, styling, or custom buttons; name the product interaction or value model that built-ins cannot express.`,
944
- );
945
- }
946
-
947
- return errors;
948
- }
949
-
950
- function isSliderLikeControl(control: ToolcraftControlSchema): boolean {
951
- return control.type === "slider" || control.type === "rangeSlider";
952
- }
953
-
954
- const SMALL_SEMANTIC_DISCRETE_POSITION_LIMIT = 13;
955
- const MAX_VISUAL_DISCRETE_POSITION_COUNT = 32;
956
- const SEMANTIC_DISCRETE_SLIDER_RE =
957
- /\b(anchor|band|bands|cell|cells|col|cols|column|columns|count|gap|grid|jitter|level|levels|octave|octaves|pass|passes|point|points|row|rows|segment|segments|step|steps|tile|tiles)\b/i;
958
- const FINITE_ANIMATION_STEP_SLIDER_RE =
959
- /\b(char|chars|character|characters|flip|flips|glyph|glyphs|frame|frames|letter|letters)\b/i;
960
- const FINITE_ANIMATION_STEP_VALUE_RE = /\b(count|depth|step|steps)\b/i;
961
- const SEMANTIC_CONTINUOUS_SLIDER_RE =
962
- /\b(duration|fps|frame rate|frames per second|rate|speed|time|seconds?|ms|milliseconds?|hz|cols\/s|ch\/s)\b/i;
963
-
964
- function getStepPositionCount(control: ToolcraftControlSchema): number | undefined {
965
- if (
966
- typeof control.step !== "number" ||
967
- typeof control.min !== "number" ||
968
- typeof control.max !== "number" ||
969
- !Number.isFinite(control.step) ||
970
- !Number.isFinite(control.min) ||
971
- !Number.isFinite(control.max) ||
972
- control.step <= 0 ||
973
- control.max <= control.min
974
- ) {
975
- return undefined;
976
- }
977
-
978
- const rawStepCount = (control.max - control.min) / control.step;
979
- const roundedStepCount = Math.round(rawStepCount);
980
- const intervalCount =
981
- Math.abs(rawStepCount - roundedStepCount) < Number.EPSILON * 100
982
- ? roundedStepCount
983
- : Math.floor(rawStepCount) + 1;
984
-
985
- return Math.max(2, intervalCount + 1);
986
- }
987
-
988
- function getStepMarkerCount(control: ToolcraftControlSchema): number | undefined {
989
- return getStepPositionCount(control);
990
- }
991
-
992
- function isIntegerStepDomain(control: ToolcraftControlSchema): boolean {
993
- return (
994
- typeof control.min === "number" &&
995
- typeof control.max === "number" &&
996
- typeof control.step === "number" &&
997
- Number.isInteger(control.min) &&
998
- Number.isInteger(control.max) &&
999
- Number.isInteger(control.step)
1000
- );
1001
- }
1002
-
1003
- function getSliderSemanticText(
1004
- controlId: string,
1005
- control: ToolcraftControlSchema,
1006
- ): string {
1007
- return [
1008
- controlId,
1009
- control.target,
1010
- getControlLabelText(control),
1011
- typeof control.unit === "string" ? control.unit : "",
1012
- ].join(" ");
1013
- }
1014
-
1015
- function shouldUseVisualDiscreteSlider(
1016
- controlId: string,
1017
- control: ToolcraftControlSchema,
1018
- ): boolean {
1019
- const positionCount = getStepPositionCount(control);
1020
-
1021
- if (!positionCount || !isIntegerStepDomain(control)) {
1022
- return false;
1023
- }
1024
-
1025
- const semanticText = getSliderSemanticText(controlId, control);
1026
-
1027
- if (SEMANTIC_CONTINUOUS_SLIDER_RE.test(semanticText)) {
1028
- return false;
1029
- }
1030
-
1031
- const hasFiniteAnimationStepSemantics =
1032
- FINITE_ANIMATION_STEP_SLIDER_RE.test(semanticText) &&
1033
- FINITE_ANIMATION_STEP_VALUE_RE.test(semanticText);
1034
-
1035
- if (hasFiniteAnimationStepSemantics) {
1036
- return positionCount <= MAX_VISUAL_DISCRETE_POSITION_COUNT;
1037
- }
1038
-
1039
- if (positionCount > SMALL_SEMANTIC_DISCRETE_POSITION_LIMIT) {
1040
- return false;
1041
- }
1042
-
1043
- return SEMANTIC_DISCRETE_SLIDER_RE.test(semanticText);
1044
- }
1045
-
1046
- function getSliderVariantClassificationErrors({
1047
- control,
1048
- controlId,
1049
- label,
1050
- }: {
1051
- control: ToolcraftControlSchema;
1052
- controlId: string;
1053
- label: string;
1054
- }): string[] {
1055
- const errors: string[] = [];
1056
- const positionCount = getStepPositionCount(control);
1057
-
1058
- if (!positionCount) {
1059
- return errors;
1060
- }
1061
-
1062
- if (
1063
- shouldUseVisualDiscreteSlider(controlId, control) &&
1064
- control.variant !== "discrete"
1065
- ) {
1066
- errors.push(
1067
- `${label} has ${positionCount} semantic integer positions and must use variant "discrete" so Toolcraft renders tick markers.`,
1068
- );
1069
- }
1070
-
1071
- if (
1072
- control.variant === "discrete" &&
1073
- positionCount > MAX_VISUAL_DISCRETE_POSITION_COUNT
1074
- ) {
1075
- errors.push(
1076
- `${label} declares variant "discrete" with ${positionCount} positions, which would overload tick markers. Keep it stepped continuous or use a different control.`,
1077
- );
1078
- }
1079
-
1080
- return errors;
1081
- }
1082
-
1083
- function getControlLabelText(control: ToolcraftControlSchema): string {
1084
- return typeof control.label === "string" ? control.label : "";
1085
- }
1086
-
1087
- function hasVisibleControlLabel(control: ToolcraftControlSchema): boolean {
1088
- return typeof control.label === "string" && control.label.trim().length > 0;
1089
- }
1090
-
1091
- const shortSingleLineTextMaxLength = 80;
1092
- const longTextControlEvidencePattern =
1093
- /\b(multiline|multi-line|multi\s+line|long\s+(?:text|content|prompt|instructions?)|paragraph|body\s+(?:copy|text)|rich\s+text|textarea|instructions?|script|code|json|css|shader|template|markdown|csv|xml|yaml|html|svg|list|lines|dataset|structured|source\s+text)\b/i;
1094
- const shortSingleLineTextIntentPattern =
1095
- /\b(button|label|title|name|caption|headline|cta|badge|token|word|phrase|single-line|one-line|short\s+text)\b/i;
1096
-
1097
- function getControlDefaultString(control: ToolcraftControlSchema): string | undefined {
1098
- return typeof control.defaultValue === "string" ? control.defaultValue : undefined;
1099
- }
1100
-
1101
- function getTextValueControlIntentText(
1102
- controlId: string,
1103
- control: ToolcraftControlSchema,
1104
- ): string {
1105
- return [
1106
- controlId,
1107
- control.target,
1108
- getControlLabelText(control),
1109
- control.description ?? "",
1110
- ]
1111
- .join(" ")
1112
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2");
1113
- }
1114
-
1115
- function getCodeTextareaControlError({
1116
- control,
1117
- controlId,
1118
- label,
1119
- }: {
1120
- control: ToolcraftControlSchema;
1121
- controlId: string;
1122
- label: string;
1123
- }): string | undefined {
1124
- if (control.type !== "code") {
1125
- return undefined;
1126
- }
1127
-
1128
- const defaultValue = getControlDefaultString(control);
1129
- const intentText = getTextValueControlIntentText(controlId, control);
1130
- const hasLongTextEvidence =
1131
- longTextControlEvidencePattern.test(intentText) ||
1132
- (defaultValue !== undefined &&
1133
- (defaultValue.length > shortSingleLineTextMaxLength ||
1134
- /[\r\n]/.test(defaultValue)));
1135
- const looksLikeShortSingleLineText =
1136
- shortSingleLineTextIntentPattern.test(intentText) ||
1137
- (defaultValue !== undefined &&
1138
- defaultValue.trim().length > 0 &&
1139
- defaultValue.trim().length <= shortSingleLineTextMaxLength &&
1140
- !/[\r\n]/.test(defaultValue));
1141
-
1142
- if (!looksLikeShortSingleLineText || hasLongTextEvidence) {
1143
- return undefined;
1144
- }
1145
-
1146
- return `${label} uses CodeTextarea for short single-line text. Use type "text" for button labels, canvas labels, titles, captions, names, badges, tokens, and other short one-line content. Reserve type "code" for long, multiline, or structured values, and document that reason in description when the default is short.`;
1147
- }
1148
-
1149
- const singleCurveSemanticPattern =
1150
- /\b(acceleration|accel|bend|easing|ease|response|depth|mask|opacity|alpha|motion|velocity|threshold|falloff|remap|remapping)\b|speed\s+profile|mapping\s+curve|curve\s+mapping/i;
1151
- const rgbCurveSemanticPattern =
1152
- /\b(rgb|rgba|channel|channels|red|green|blue|color\s*correction|colour\s*correction|color\s*grading|colour\s*grading|color\s*grade|colour\s*grade|color\s*curve|colour\s*curve|tone\s*mapping|hue|saturation|chroma)\b/i;
1153
-
1154
- function getCurveSemanticText(
1155
- controlId: string,
1156
- control: ToolcraftControlSchema,
1157
- ): string {
1158
- return [
1159
- controlId,
1160
- control.target,
1161
- getControlLabelText(control),
1162
- control.description ?? "",
1163
- ]
1164
- .join(" ")
1165
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2");
1166
- }
1167
-
1168
- function shouldUseSingleCurveVariant(
1169
- controlId: string,
1170
- control: ToolcraftControlSchema,
1171
- ): boolean {
1172
- if (control.type !== "curves" || control.variant === "single") {
1173
- return false;
1174
- }
1175
-
1176
- const text = getCurveSemanticText(controlId, control);
1177
-
1178
- return singleCurveSemanticPattern.test(text) && !rgbCurveSemanticPattern.test(text);
1179
- }
1180
-
1181
- function getToggleControlLabelError(
1182
- control: ToolcraftControlSchema,
1183
- sectionTitle?: string,
1184
- ): string | undefined {
1185
- if (control.type !== "switch" && control.type !== "checkbox") {
1186
- return undefined;
1187
- }
1188
-
1189
- const label = getControlLabelText(control).trim();
1190
-
1191
- if (/^(enable|disable)\b/i.test(label)) {
1192
- return `toggle labels must name the setting context only; use "CRT", "Background", "Glow", or "Loop" instead of "${label}".`;
1193
- }
1194
-
1195
- if (
1196
- label &&
1197
- sectionTitle &&
1198
- normalizeToolcraftSemanticText(label) ===
1199
- normalizeToolcraftSemanticText(sectionTitle)
1200
- ) {
1201
- return `toggle label "${label}" duplicates section title "${sectionTitle}". Use a shorter contextual label such as "Include" or rename the toggle to a more specific setting.`;
1202
- }
1203
-
1204
- return undefined;
1205
- }
1206
-
1207
- function getSingleActionsControlLabelError(
1208
- control: ToolcraftControlSchema,
1209
- ): string | undefined {
1210
- if (control.type !== "actions") {
1211
- return undefined;
1212
- }
1213
-
1214
- const actions = getControlActions(control);
1215
-
1216
- if (actions.length !== 1) {
1217
- return undefined;
1218
- }
1219
-
1220
- const label = getControlLabelText(control).trim();
1221
- const actionLabel = getActionLabelText(actions[0] as ToolcraftActionSchema | string).trim();
1222
-
1223
- if (
1224
- label &&
1225
- actionLabel &&
1226
- normalizeToolcraftSemanticText(label) === normalizeToolcraftSemanticText(actionLabel)
1227
- ) {
1228
- return `single Actions control label "${label}" duplicates its only button label "${actionLabel}". Keep the button as the command and use a short context label such as "Ink wash", "Palette action", or "Current layer".`;
1229
- }
1230
-
1231
- return undefined;
1232
- }
1233
-
1234
- const maxInlineSwitchLabelLength = 16;
1235
- const maxInlineSwitchLabelWordCount = 2;
1236
-
1237
- function getInlineSwitchLabelText(
1238
- controlId: string,
1239
- control: ToolcraftControlSchema,
1240
- ): string {
1241
- if (control.label === false) {
1242
- return "";
1243
- }
1244
-
1245
- const label = getControlLabelText(control).trim();
1246
-
1247
- return label || controlId;
1248
- }
1249
-
1250
- function isInlineSwitchLabelSafe(
1251
- controlId: string,
1252
- control: ToolcraftControlSchema,
1253
- ): boolean {
1254
- const label = getInlineSwitchLabelText(controlId, control);
1255
-
1256
- if (!label) {
1257
- return true;
1258
- }
1259
-
1260
- const wordCount = label.split(/\s+/u).filter(Boolean).length;
1261
-
1262
- return label.length <= maxInlineSwitchLabelLength && wordCount <= maxInlineSwitchLabelWordCount;
1263
- }
1264
-
1265
- function isBooleanControl(control: ToolcraftControlSchema | undefined): boolean {
1266
- return control?.type === "checkbox" || control?.type === "switch";
1267
- }
1268
-
1269
- function controlsShareToolcraftTargetEntity(
1270
- firstControl: ToolcraftControlSchema,
1271
- secondControl: ToolcraftControlSchema,
1272
- ): boolean {
1273
- const firstPrefix = getToolcraftLooseTargetPrefix(firstControl.target);
1274
- const secondPrefix = getToolcraftLooseTargetPrefix(secondControl.target);
1275
-
1276
- return Boolean(firstPrefix && firstPrefix === secondPrefix);
1277
- }
1278
-
1279
- function sectionHasInlineLayoutGroupForPair(
1280
- section: NonNullable<ResolvedToolcraftAppSchema["panels"]["controls"]>["sections"][number],
1281
- firstControlId: string,
1282
- secondControlId: string,
1283
- ): boolean {
1284
- return (section.layoutGroups ?? []).some(
1285
- (layoutGroup) =>
1286
- layoutGroup.layout === "inline" &&
1287
- layoutGroup.columns === 2 &&
1288
- layoutGroup.controls.length === 2 &&
1289
- layoutGroup.controls.includes(firstControlId) &&
1290
- layoutGroup.controls.includes(secondControlId),
1291
- );
1292
- }
1293
-
1294
- function getControlActions(
1295
- control: ToolcraftControlSchema,
1296
- ): readonly (ToolcraftActionSchema | string)[] {
1297
- const maybeControlWithActions = control as {
1298
- actions?: readonly (ToolcraftActionSchema | string)[];
1299
- };
1300
-
1301
- return Array.isArray(maybeControlWithActions.actions)
1302
- ? maybeControlWithActions.actions
1303
- : [];
1304
- }
1305
-
1306
- function getTimelineTransportControlText(
1307
- controlId: string,
1308
- control: ToolcraftControlSchema,
1309
- ): string {
1310
- return [
1311
- controlId,
1312
- control.target,
1313
- getControlLabelText(control),
1314
- ...getControlActions(control).map(getActionSearchText),
1315
- ].join(" ");
1316
- }
1317
-
1318
- function getAnimationIntentControlText({
1319
- control,
1320
- controlId,
1321
- sectionTitle,
1322
- }: ToolcraftVisibleControl): string {
1323
- return [
1324
- sectionTitle ?? "",
1325
- controlId,
1326
- control.target,
1327
- getControlLabelText(control),
1328
- ].join(" ");
1329
- }
1330
-
1331
- function getSearchableControlText({
1332
- control,
1333
- controlId,
1334
- sectionTitle,
1335
- }: ToolcraftVisibleControl): string {
1336
- return [
1337
- sectionTitle ?? "",
1338
- controlId,
1339
- control.target,
1340
- getControlLabelText(control),
1341
- ]
1342
- .join(" ")
1343
- .replace(/([a-z])([A-Z])/g, "$1 $2");
1344
- }
1345
-
1346
- function actionLooksLikePngExport(action: ToolcraftActionSchema | string): boolean {
1347
- const text = getActionSearchText(action).replace(/([a-z])([A-Z])/g, "$1 $2");
1348
-
1349
- return (
1350
- (/\b(export|download)\b/i.test(text) && /\b(png|image)\b/i.test(text)) ||
1351
- /\bexport\.png\b/i.test(text)
1352
- );
1353
- }
1354
-
1355
- function actionLooksLikeVideoExport(action: ToolcraftActionSchema | string): boolean {
1356
- const text = getActionSearchText(action).replace(/([a-z])([A-Z])/g, "$1 $2");
1357
-
1358
- return (
1359
- (/\b(export|download)\b/i.test(text) && /\b(video|mp4|webm|mov)\b/i.test(text)) ||
1360
- /\bexport\.video\b/i.test(text)
1361
- );
1362
- }
1363
-
1364
- function schemaHasPngExportPanelAction(schema: ResolvedToolcraftAppSchema): boolean {
1365
- return (schema.panels.controls?.sections ?? []).some((section) =>
1366
- Object.values(section.controls).some(
1367
- (control) =>
1368
- control.type === "panelActions" &&
1369
- getControlActions(control).some(actionLooksLikePngExport),
1370
- ),
1371
- );
1372
- }
1373
-
1374
- function schemaHasVideoExportPanelAction(schema: ResolvedToolcraftAppSchema): boolean {
1375
- return (schema.panels.controls?.sections ?? []).some((section) =>
1376
- Object.values(section.controls).some(
1377
- (control) =>
1378
- control.type === "panelActions" &&
1379
- getControlActions(control).some(actionLooksLikeVideoExport),
1380
- ),
1381
- );
1382
- }
1383
-
1384
- function getFirstPanelActionsSectionIndex(schema: ResolvedToolcraftAppSchema): number {
1385
- return (schema.panels.controls?.sections ?? []).findIndex((section) =>
1386
- Object.values(section.controls).some((control) => control.type === "panelActions"),
1387
- );
1388
- }
1389
-
1390
- function getSchemaControlsSectionByTitle(
1391
- schema: ResolvedToolcraftAppSchema,
1392
- title: string,
1393
- ): NonNullable<ResolvedToolcraftAppSchema["panels"]["controls"]>["sections"][number] | undefined {
1394
- const normalizedTitle = normalizeToolcraftSemanticText(title);
1395
-
1396
- return (schema.panels.controls?.sections ?? []).find(
1397
- (section) => normalizeToolcraftSemanticText(section.title) === normalizedTitle,
1398
- );
1399
- }
1400
-
1401
- function getSchemaControlsSectionIndexByTitle(
1402
- schema: ResolvedToolcraftAppSchema,
1403
- title: string,
1404
- ): number {
1405
- const normalizedTitle = normalizeToolcraftSemanticText(title);
1406
-
1407
- return (schema.panels.controls?.sections ?? []).findIndex(
1408
- (section) => normalizeToolcraftSemanticText(section.title) === normalizedTitle,
1409
- );
1410
- }
1411
-
1412
- function getSectionControlEntryByTarget(
1413
- section:
1414
- | NonNullable<ResolvedToolcraftAppSchema["panels"]["controls"]>["sections"][number]
1415
- | undefined,
1416
- target: string,
1417
- ): readonly [string, ToolcraftControlSchema] | undefined {
1418
- if (!section) {
1419
- return undefined;
1420
- }
1421
-
1422
- return Object.entries(section.controls).find(([, control]) => control.target === target);
1423
- }
1424
-
1425
- function schemaHasOutputBackgroundColorControl(
1426
- controls: readonly ToolcraftVisibleControl[],
1427
- ): boolean {
1428
- return controls.some((visibleControl) => {
1429
- const { control } = visibleControl;
1430
-
1431
- if (control.type !== "color") {
1432
- return false;
1433
- }
1434
-
1435
- return /\b(background|backdrop|scene|canvas)\b/i.test(
1436
- getSearchableControlText(visibleControl),
1437
- );
1438
- });
1439
- }
1440
-
1441
- function schemaHasOutputBackgroundToggleControl(
1442
- controls: readonly ToolcraftVisibleControl[],
1443
- ): boolean {
1444
- return controls.some(isOutputBackgroundToggleControl);
1445
- }
1446
-
1447
- function isOutputBackgroundToggleControl(visibleControl: ToolcraftVisibleControl): boolean {
1448
- const { control } = visibleControl;
1449
-
1450
- if (
1451
- control.type !== "switch" &&
1452
- control.type !== "checkbox" &&
1453
- control.type !== "select" &&
1454
- control.type !== "segmented"
1455
- ) {
1456
- return false;
1457
- }
1458
-
1459
- return /\b(background|backdrop|transparent|transparency|alpha)\b/i.test(
1460
- getSearchableControlText(visibleControl),
1461
- );
1462
- }
1463
-
1464
- function getOutputBackgroundColorEntry(
1465
- section:
1466
- | NonNullable<ResolvedToolcraftAppSchema["panels"]["controls"]>["sections"][number]
1467
- | undefined,
1468
- ): readonly [string, ToolcraftControlSchema] | undefined {
1469
- if (!section) {
1470
- return undefined;
1471
- }
1472
-
1473
- return Object.entries(section.controls).find(([controlId, control]) => {
1474
- if (control.type !== "color") {
1475
- return false;
1476
- }
1477
-
1478
- return /\b(background|backdrop|scene|canvas)\b/i.test(
1479
- [section.title, controlId, control.target, getControlLabelText(control)]
1480
- .join(" ")
1481
- .replace(/([a-z])([A-Z])/g, "$1 $2"),
1482
- );
1483
- });
1484
- }
1485
-
1486
- function sectionHasEqualWidthOutputBackgroundRow(
1487
- section:
1488
- | NonNullable<ResolvedToolcraftAppSchema["panels"]["controls"]>["sections"][number]
1489
- | undefined,
1490
- toggleControlId: string | undefined,
1491
- colorControlId: string | undefined,
1492
- ): boolean {
1493
- if (!section || !toggleControlId || !colorControlId) {
1494
- return false;
1495
- }
1496
-
1497
- return (section.layoutGroups ?? []).some(
1498
- (layoutGroup) =>
1499
- layoutGroup.layout === "inline" &&
1500
- layoutGroup.columns === 2 &&
1501
- layoutGroup.controls.length === 2 &&
1502
- layoutGroup.controls[0] === toggleControlId &&
1503
- layoutGroup.controls[1] === colorControlId,
1504
- );
1505
- }
1506
-
1507
- const SEGMENTED_CONTROL_MAX_OPTIONS = 4;
1508
- const SEGMENTED_CONTROL_MAX_OPTION_LABEL_LENGTH = 9;
1509
- const SEGMENTED_CONTROL_MAX_TOTAL_LABEL_LENGTH = 24;
1510
-
1511
- function getSegmentedControlLayoutError(
1512
- control: ToolcraftControlSchema,
1513
- ): string | null {
1514
- if (control.type !== "segmented") {
1515
- return null;
1516
- }
1517
-
1518
- const labels = control.options?.map((option) => option.label.trim()) ?? [];
1519
- const totalLabelLength = labels.reduce((total, label) => total + label.length, 0);
1520
- const longLabels = labels.filter(
1521
- (label) => label.length > SEGMENTED_CONTROL_MAX_OPTION_LABEL_LENGTH,
1522
- );
1523
-
1524
- if (
1525
- labels.length > SEGMENTED_CONTROL_MAX_OPTIONS ||
1526
- longLabels.length > 0 ||
1527
- totalLabelLength > SEGMENTED_CONTROL_MAX_TOTAL_LABEL_LENGTH
1528
- ) {
1529
- return [
1530
- `segmented controls must preserve cell padding: use at most ${SEGMENTED_CONTROL_MAX_OPTIONS} short options`,
1531
- `(max ${SEGMENTED_CONTROL_MAX_OPTION_LABEL_LENGTH} characters per label and ${SEGMENTED_CONTROL_MAX_TOTAL_LABEL_LENGTH} total)`,
1532
- "or shorten labels first; if the compact names still exceed the budget, use a select dropdown instead.",
1533
- ].join(" ");
1534
- }
1535
-
1536
- return null;
1537
- }
1538
-
1539
- const controlOrderRoleRanks = {
1540
- input: 0,
1541
- mode: 1,
1542
- primary: 2,
1543
- spatial: 2,
1544
- color: 2,
1545
- strength: 3,
1546
- detail: 4,
1547
- advanced: 5,
1548
- action: 6,
1549
- } satisfies Record<ToolcraftControlOrderRole, number>;
1550
-
1551
- const requiredReferenceCloneCoverage = [
1552
- "canvas-sizing",
1553
- "control-mapping",
1554
- "renderer-state",
1555
- ] satisfies readonly ToolcraftReferenceCoverage[];
1556
-
1557
- const referenceTransportCoverage = new Set<ToolcraftReferenceCoverage>([
1558
- "export-at-time",
1559
- "pause-resume",
1560
- "restart",
1561
- "time-progress",
1562
- ]);
1563
-
1564
- const toolcraftReferenceTimelineCoverage = new Set<ToolcraftReferenceTimelineCoverage>([
1565
- "duration",
1566
- "export-at-time",
1567
- "keyframes",
1568
- "loop",
1569
- "playback",
1570
- "restart",
1571
- "scrub",
1572
- "time-progress",
1573
- ]);
1574
-
1575
- const customReferenceTimelineCoverage = new Set<ToolcraftReferenceTimelineCoverage>([
1576
- "all-range",
1577
- "export-range",
1578
- "jump-to-trim-start",
1579
- "range-playback",
1580
- "state-jump",
1581
- "trim-range",
1582
- ]);
1583
-
1584
- const referenceFeatureStatusValues = new Set<ToolcraftReferenceFeatureStatus>([
1585
- "intentionally-changed",
1586
- "ported",
1587
- "toolcraft-native",
1588
- ]);
1589
-
1590
- const referenceStudyStatusValues = new Set<ToolcraftReferenceStudyStatus>([
1591
- "ran-original",
1592
- "restored-local",
1593
- "source-inspection-only",
1594
- ]);
1595
-
1596
- const explicitReferenceChangeReasonPattern =
1597
- /\b(user|requested|explicit|approved|redesign|change request)\b/i;
1598
-
1599
- const referenceStudySourceOnlyReasonPattern =
1600
- /\b(cannot|can't|unable|unavailable|missing|broken|fails?|blocked|no\s+(?:server|deps?|dependency|access)|not\s+(?:runnable|available))\b/i;
1601
-
1602
- const videoReferenceEvidencePattern =
1603
- /\b(?:reference\s+(?:video|gif)|screen\s*recording|contact[-\s]*sheet|storyboard|frame[-\s]*by[-\s]*frame|extracted[-\s]*frames?|ffprobe)\b|(?:^|[\\/"'\s])[^\\/"'\s]+\.(?:mp4|mov|webm|gif)\b/i;
1604
-
1605
- const timelineTransportControlPattern =
1606
- /\b(play|pause|paused|resume|animate|restart)\b/i;
1607
-
1608
- const animationIntentControlPattern =
1609
- /\b(animation|animate|motion|playback)\b/i;
1610
-
1611
- const requiredAutonomousAnimationCoverage = [
1612
- "no-user-facing-transport",
1613
- "no-play-pause",
1614
- "no-scrub",
1615
- "no-duration-control",
1616
- "no-loop-control",
1617
- "no-export-at-time",
1618
- ] satisfies readonly ToolcraftAutonomousAnimationCoverage[];
1619
-
1620
- const requiredLayerCoverage = [
1621
- "selection",
1622
- "visibility",
1623
- "reorder",
1624
- "grouping",
1625
- ] satisfies readonly ToolcraftLayerCoverage[];
1626
-
1627
- const requiredTimelinePlaybackCoverage = [
1628
- "pause-resume",
1629
- "scrub",
1630
- "duration",
1631
- "loop",
1632
- "rendered-frame",
1633
- ] satisfies readonly ToolcraftTimelinePlaybackCoverage[];
1634
-
1635
- function getToolcraftTransferModeEvidenceText(
1636
- transferMode: ToolcraftTransferMode,
1637
- ): string {
1638
- return JSON.stringify(transferMode);
1639
- }
1640
-
1641
- function validateToolcraftVideoReferenceStudy(
1642
- study: ToolcraftVideoReferenceStudyEvidence,
1643
- acceptance: readonly ToolcraftComponentAcceptance[],
1644
- ): string[] {
1645
- const errors: string[] = [];
1646
- const acceptanceById = new Map(acceptance.map((entry) => [entry.id, entry]));
1647
- const frameIds = new Set<string>();
1648
-
1649
- if (!study.referenceLocation.trim()) {
1650
- errors.push(
1651
- "videoReferenceStudy.referenceLocation must name the inspected video, GIF, screen recording, contact sheet, or extracted-frame folder.",
1652
- );
1653
- }
1654
-
1655
- if (!study.extractionEvidence.trim()) {
1656
- errors.push(
1657
- "videoReferenceStudy.extractionEvidence must explain how frames were inspected or extracted before implementation.",
1658
- );
1659
- }
1660
-
1661
- if (!study.behaviorDecomposition.trim()) {
1662
- errors.push(
1663
- "videoReferenceStudy.behaviorDecomposition must decompose the observed frame-to-frame changes into product behavior to preserve.",
1664
- );
1665
- }
1666
-
1667
- if (study.storyboard.length < 4) {
1668
- errors.push(
1669
- "videoReferenceStudy.storyboard must include at least four timecoded frames so the reference is studied as motion, not a single screenshot.",
1670
- );
1671
- }
1672
-
1673
- for (const [index, frame] of study.storyboard.entries()) {
1674
- const frameLabel = frame.frameId.trim() || `#${index + 1}`;
1675
-
1676
- if (!frame.frameId.trim()) {
1677
- errors.push(`videoReferenceStudy.storyboard frame ${index + 1} must include a stable frameId.`);
1678
- } else if (frameIds.has(frame.frameId)) {
1679
- errors.push(
1680
- `videoReferenceStudy.storyboard frameId "${frame.frameId}" is duplicated; each sampled frame needs a stable id.`,
1681
- );
1682
- } else {
1683
- frameIds.add(frame.frameId);
1684
- }
1685
-
1686
- if (!Number.isFinite(frame.timeSeconds) || frame.timeSeconds < 0) {
1687
- errors.push(
1688
- `videoReferenceStudy.storyboard "${frameLabel}" must include a non-negative finite timeSeconds value.`,
1689
- );
1690
- }
1691
-
1692
- if (!frame.frameSource.trim()) {
1693
- errors.push(
1694
- `videoReferenceStudy.storyboard "${frameLabel}" must cite the frame image, contact sheet cell, or source timecode inspected.`,
1695
- );
1696
- }
1697
-
1698
- if (!frame.visualObservation.trim()) {
1699
- errors.push(
1700
- `videoReferenceStudy.storyboard "${frameLabel}" must describe the visible frame state.`,
1701
- );
1702
- }
1703
-
1704
- if (!frame.behaviorObservation.trim()) {
1705
- errors.push(
1706
- `videoReferenceStudy.storyboard "${frameLabel}" must describe the behavior inferred from that frame.`,
1707
- );
1708
- }
1709
- }
1710
-
1711
- if (study.transitionAnalysis.length < 3) {
1712
- errors.push(
1713
- "videoReferenceStudy.transitionAnalysis must include at least three frame-to-frame deltas proving how behavior changes between sampled frames.",
1714
- );
1715
- }
1716
-
1717
- for (const [index, transition] of study.transitionAnalysis.entries()) {
1718
- const transitionLabel = transition.id.trim() || `#${index + 1}`;
1719
-
1720
- if (!transition.id.trim()) {
1721
- errors.push(
1722
- `videoReferenceStudy.transitionAnalysis item ${index + 1} must include a stable id.`,
1723
- );
1724
- }
1725
-
1726
- if (!frameIds.has(transition.fromFrameId)) {
1727
- errors.push(
1728
- `videoReferenceStudy.transitionAnalysis "${transitionLabel}" fromFrameId "${transition.fromFrameId}" must point to a storyboard frameId.`,
1729
- );
1730
- }
1731
-
1732
- if (!frameIds.has(transition.toFrameId)) {
1733
- errors.push(
1734
- `videoReferenceStudy.transitionAnalysis "${transitionLabel}" toFrameId "${transition.toFrameId}" must point to a storyboard frameId.`,
1735
- );
1736
- }
1737
-
1738
- if (!transition.behaviorDelta.trim()) {
1739
- errors.push(
1740
- `videoReferenceStudy.transitionAnalysis "${transitionLabel}" must describe the behavior delta between frames.`,
1741
- );
1742
- }
1743
- }
1744
-
1745
- if (study.acceptanceMapping.length === 0) {
1746
- errors.push(
1747
- "videoReferenceStudy.acceptanceMapping must map observed video behaviors to acceptance rows.",
1748
- );
1749
- }
1750
-
1751
- for (const [index, mapping] of study.acceptanceMapping.entries()) {
1752
- const mappingLabel = mapping.behavior.trim() || `#${index + 1}`;
1753
-
1754
- if (!mapping.behavior.trim()) {
1755
- errors.push(
1756
- `videoReferenceStudy.acceptanceMapping item ${index + 1} must name the observed behavior.`,
1757
- );
1758
- }
1759
-
1760
- if (mapping.frameIds.length === 0) {
1761
- errors.push(
1762
- `videoReferenceStudy.acceptanceMapping "${mappingLabel}" must cite storyboard frameIds that prove the behavior.`,
1763
- );
1764
- }
1765
-
1766
- for (const frameId of mapping.frameIds) {
1767
- if (!frameIds.has(frameId)) {
1768
- errors.push(
1769
- `videoReferenceStudy.acceptanceMapping "${mappingLabel}" frameId "${frameId}" must point to a storyboard frameId.`,
1770
- );
1771
- }
1772
- }
1773
-
1774
- const acceptanceId = mapping.acceptanceId.trim();
1775
- const entry = acceptanceById.get(acceptanceId);
1776
-
1777
- if (!acceptanceId) {
1778
- errors.push(
1779
- `videoReferenceStudy.acceptanceMapping "${mappingLabel}" must include acceptanceId for the test proving the copied behavior.`,
1780
- );
1781
- continue;
1782
- }
1783
-
1784
- if (!entry) {
1785
- errors.push(
1786
- `videoReferenceStudy.acceptanceMapping "${mappingLabel}" points to missing acceptanceId "${acceptanceId}".`,
1787
- );
1788
- continue;
1789
- }
1790
-
1791
- if (!entry.automated || !entry.automatedTestName.trim()) {
1792
- errors.push(
1793
- `${acceptanceId} must have automated coverage proving video reference behavior "${mappingLabel}".`,
1794
- );
1795
- }
1796
-
1797
- if (!entry.browser || !entry.browserTestName.trim()) {
1798
- errors.push(
1799
- `${acceptanceId} must have browser coverage proving video reference behavior "${mappingLabel}".`,
1800
- );
1801
- }
1802
-
1803
- if (!entry.expectedObservable.trim()) {
1804
- errors.push(
1805
- `${acceptanceId} must describe the observable result for video reference behavior "${mappingLabel}".`,
1806
- );
1807
- }
1808
- }
1809
-
1810
- return errors;
1811
- }
1812
-
1813
- function isModeSelectorControl(
1814
- controlId: string,
1815
- control: ToolcraftControlSchema,
1816
- ): boolean {
1817
- if (control.type !== "select" && control.type !== "segmented") {
1818
- return false;
1819
- }
1820
-
1821
- return /mode|type|filter|style|preset|variant/i.test(
1822
- `${controlId} ${control.target} ${getControlLabelText(control)}`,
1823
- );
1824
- }
1825
-
1826
- function matchesControlMeaning(
1827
- controlId: string,
1828
- control: ToolcraftControlSchema,
1829
- pattern: RegExp,
1830
- ): boolean {
1831
- return pattern.test(`${controlId} ${control.target} ${getControlLabelText(control)}`);
1832
- }
1833
-
1834
- export function inferToolcraftControlOrderRole(
1835
- controlId: string,
1836
- control: ToolcraftControlSchema,
1837
- ): ToolcraftControlOrderRole {
1838
- if (control.orderRole) {
1839
- return control.orderRole;
1840
- }
1841
-
1842
- if (control.type === "panelActions") {
1843
- return "action";
1844
- }
1845
-
1846
- if (
1847
- control.type === "fileDrop" ||
1848
- control.target.startsWith("media.") ||
1849
- control.target === "canvas.size.width" ||
1850
- control.target === "canvas.size.height"
1851
- ) {
1852
- return "input";
1853
- }
1854
-
1855
- if (isModeSelectorControl(controlId, control)) {
1856
- return "mode";
1857
- }
1858
-
1859
- if (control.type === "vector") {
1860
- return "spatial";
1861
- }
1862
-
1863
- if (control.type === "color" || control.type === "gradient") {
1864
- return "color";
1865
- }
1866
-
1867
- if (
1868
- matchesControlMeaning(
1869
- controlId,
1870
- control,
1871
- /grain|noise|texture|detail|blur|threshold|sample|quality|density|iteration|radius/i,
1872
- )
1873
- ) {
1874
- return "detail";
1875
- }
1876
-
1877
- if (
1878
- isSliderLikeControl(control) ||
1879
- matchesControlMeaning(
1880
- controlId,
1881
- control,
1882
- /amount|brightness|contrast|depth|highlight|intensity|mix|opacity|saturation|scale|spread|strength/i,
1883
- )
1884
- ) {
1885
- return "strength";
1886
- }
1887
-
1888
- return "primary";
1889
- }
1890
-
1891
- function getToolcraftControlOrderErrors(schema: ResolvedToolcraftAppSchema): string[] {
1892
- const errors: string[] = [];
1893
-
1894
- for (const section of schema.panels.controls?.sections ?? []) {
1895
- let previousItem: ToolcraftControlOrderItem | undefined;
1896
-
1897
- for (const [controlId, control] of Object.entries(section.controls)) {
1898
- if (control.type === "panelActions" || isRuntimeSetupControlTarget(control.target)) {
1899
- continue;
1900
- }
1901
-
1902
- const role = inferToolcraftControlOrderRole(controlId, control);
1903
- const item: ToolcraftControlOrderItem = {
1904
- controlId,
1905
- rank: controlOrderRoleRanks[role],
1906
- role,
1907
- sectionTitle: section.title,
1908
- target: control.target,
1909
- type: control.type,
1910
- };
1911
-
1912
- if (previousItem && item.rank < previousItem.rank) {
1913
- const sectionLabel = section.title ? `${section.title} / ` : "";
1914
-
1915
- errors.push(
1916
- `${sectionLabel}${controlId} (${control.target}) has orderRole "${role}" after ${previousItem.controlId} (${previousItem.target}) with orderRole "${previousItem.role}". Move mode/input/primary controls before dependent strength/detail/advanced controls or split them into an earlier section.`,
1917
- );
1918
- }
1919
-
1920
- previousItem = item;
1921
- }
1922
- }
1923
-
1924
- return errors;
1925
- }
1926
-
1927
- function getToolcraftRuntimeSetupSectionErrors(
1928
- schema: ResolvedToolcraftAppSchema,
1929
- ): string[] {
1930
- const errors: string[] = [];
1931
- const controlsPanel = schema.panels.controls;
1932
-
1933
- if (!controlsPanel) {
1934
- return [
1935
- "Generated Toolcraft apps must define a controls panel so the mandatory runtime Setup section is visible.",
1936
- ];
1937
- }
1938
-
1939
- const sections = schema.panels.controls?.sections ?? [];
1940
-
1941
- if (sections.length === 0) {
1942
- return [
1943
- 'Runtime Setup must be the first visible controls-panel section titled "Setup". Do not ship an empty controls panel.',
1944
- ];
1945
- }
1946
-
1947
- const setupSection = sections[0];
1948
- const setupTitle = setupSection?.title?.trim();
1949
- const setupControls = Object.values(setupSection?.controls ?? {});
1950
- const setupTargets = new Set(setupControls.map((control) => control.target));
1951
- const hasSetupTarget = (target: string) => setupTargets.has(target);
1952
-
1953
- if (setupTitle !== "Setup") {
1954
- errors.push(
1955
- 'Runtime Setup must be the first visible controls-panel section titled "Setup". Do not move Export Settings, Import Settings, canvas sizing, Resolution scale, or Timeline into app-authored sections.',
1956
- );
1957
- }
1958
-
1959
- if (
1960
- !setupControls.some(
1961
- (control) =>
1962
- control.type === "settingsTransfer" &&
1963
- control.target === "runtime.settingsTransfer",
1964
- )
1965
- ) {
1966
- errors.push(
1967
- 'Runtime Setup must include settingsTransfer at target "runtime.settingsTransfer" so Export Settings and Import Settings are always visible.',
1968
- );
1969
- }
1970
-
1971
- if (schema.canvas.enabled && schema.canvas.sizing.mode === "editable-output") {
1972
- const missingCanvasTargets = [
1973
- "canvas.aspectRatio",
1974
- "canvas.size.width",
1975
- "canvas.size.height",
1976
- ].filter((target) => !hasSetupTarget(target));
1977
-
1978
- if (missingCanvasTargets.length > 0) {
1979
- errors.push(
1980
- `Runtime Setup for editable-output canvas must include Aspect ratio, Canvas width, and Canvas height. Missing targets: ${missingCanvasTargets.join(", ")}.`,
1981
- );
1982
- }
1983
- }
1984
-
1985
- if (schema.canvas.renderScale.enabled && !hasSetupTarget("canvas.renderScale")) {
1986
- errors.push(
1987
- 'Runtime Setup must include Resolution scale at target "canvas.renderScale" whenever canvas.renderScale is enabled.',
1988
- );
1989
- }
1990
-
1991
- if (schema.panels.timeline?.enabled) {
1992
- if (!hasSetupTarget("panels.timeline.extended")) {
1993
- errors.push(
1994
- 'Runtime Setup must include the Timeline switch at target "panels.timeline.extended" whenever panels.timeline is enabled.',
1995
- );
1996
- }
1997
- } else if (
1998
- sections.some((section) =>
1999
- Object.values(section.controls).some(
2000
- (control) => control.target === "panels.timeline.extended",
2001
- ),
2002
- )
2003
- ) {
2004
- errors.push(
2005
- 'Runtime Setup must not include the Timeline switch unless panels.timeline is enabled.',
2006
- );
2007
- }
2008
-
2009
- for (const [sectionIndex, section] of sections.entries()) {
2010
- const isRuntimeSetupSection = sectionIndex === 0 && section.title?.trim() === "Setup";
2011
-
2012
- for (const [controlId, control] of Object.entries(section.controls)) {
2013
- if (!isRuntimeSetupControlTarget(control.target) || isRuntimeSetupSection) {
2014
- continue;
2015
- }
2016
-
2017
- const sectionLabel = getToolcraftSectionLabel(section.title, sectionIndex);
2018
-
2019
- errors.push(
2020
- `${sectionLabel} / ${controlId} uses runtime Setup target "${control.target}". Runtime Setup owns Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, Resolution scale, and Timeline; do not declare these controls in app-authored sections.`,
2021
- );
2022
- }
2023
- }
2024
-
2025
- return errors;
2026
- }
2027
-
2028
- const genericControlSectionTitlePattern =
2029
- /^(controls?|settings?|parameters?|options?|configuration|config|adjustments?)$/i;
2030
-
2031
- const controlTypeSectionTitlePattern =
2032
- /^(sliders?|colors?|colours?|inputs?|selects?|switches?|checkboxes?|toggles?|buttons?|actions?)$/i;
2033
-
2034
- const weakControlLabelContextSectionTitlePattern =
2035
- /^(appearance|look|looks|properties?|style|styles|values?|visuals?)$/i;
2036
-
2037
- const broadControlSectionTitlePattern =
2038
- /^(animation|export|flow|icon|logo|motion|output|scene|shape|shapes|text|typography|visual|visuals)$/i;
2039
-
2040
- const genericControlLabelPattern =
2041
- /^(angle|amount|blur|brightness|color|contrast|count|density|depth|frequency|height|hue|intensity|offset|opacity|phase|position|quality|radius|rotation|saturation|scale|size|spacing|speed|strength|threshold|tint|width)$/i;
2042
-
2043
- const maxPreferredControlsPerSection = 7;
2044
- const maxHardControlsPerSection = 10;
2045
-
2046
- const controlSemanticClusterPatterns: ReadonlyArray<readonly [string, RegExp]> = [
2047
- ["input", /\b(upload|source|prompt|content|text|phrase|copy|message|file|media|image)\b/i],
2048
- ["mode", /\b(mode|type|preset|style|variant|filter|layout|format|quality)\b/i],
2049
- ["motion", /\b(animation|speed|velocity|accel|acceleration|correlation|duration|timing|loop|phase|fps|rate)\b/i],
2050
- ["geometry", /\b(width|height|size|scale|position|offset|anchor|origin|target|radius|distance|spread|bend|curve|curves|path|shape|grid|gap)\b/i],
2051
- ["density", /\b(fill|density|amount|count|ratio|word|words|letter|letters|particle|particles|layer|layers|island|islands)\b/i],
2052
- ["color", /\b(color|colour|gradient|shade|tint|background|halo|glow|opacity|alpha|stroke|fillColor|fillOpacity)\b/i],
2053
- ["typography", /\b(font|weight|case|leading|tracking|lineHeight|letterSpacing|typeface)\b/i],
2054
- ["export", /\b(export|copy|download|video|png|webm|mp4|mov|bitrate|resolution)\b/i],
2055
- ];
2056
-
2057
- const fontPickerOwnedTypographyPartLabels = new Map<string, string>([
2058
- ["case", "case"],
2059
- ["color", "color"],
2060
- ["colour", "color"],
2061
- ["family", "font family"],
2062
- ["fill", "color"],
2063
- ["fillcolor", "color"],
2064
- ["fillopacity", "opacity"],
2065
- ["font", "font family"],
2066
- ["fontcolor", "color"],
2067
- ["fontfamily", "font family"],
2068
- ["fontid", "font family"],
2069
- ["fontsize", "font size"],
2070
- ["fontweight", "font weight"],
2071
- ["foreground", "color"],
2072
- ["foregroundcolor", "color"],
2073
- ["leading", "line height"],
2074
- ["letterspacing", "letter spacing"],
2075
- ["lineheight", "line height"],
2076
- ["opacity", "opacity"],
2077
- ["size", "font size"],
2078
- ["textcase", "case"],
2079
- ["textcolor", "color"],
2080
- ["textfill", "color"],
2081
- ["textopacity", "opacity"],
2082
- ["tracking", "letter spacing"],
2083
- ["typeface", "font family"],
2084
- ["weight", "font weight"],
2085
- ]);
2086
-
2087
- const fontPickerDescriptionOwnedPartPatterns: ReadonlyArray<readonly [string, RegExp]> = [
2088
- ["font family", /\b(?:font\s+family|family|typeface)\b/i],
2089
- ["font weight", /\b(?:font\s+weight|weight)\b/i],
2090
- ["font size", /\b(?:font\s+size|size)\b/i],
2091
- ["case", /\b(?:text\s+case|case|uppercase|lowercase|capitalize|title\s+case)\b/i],
2092
- ["color", /\b(?:text\s+color|font\s+color|color|colour|fill)\b/i],
2093
- ["opacity", /\b(?:text\s+opacity|font\s+opacity|opacity|alpha)\b/i],
2094
- ["letter spacing", /\b(?:letter\s+spacing|tracking)\b/i],
2095
- ["line height", /\b(?:line\s+height|leading)\b/i],
2096
- ];
2097
-
2098
- function getToolcraftSectionLabel(sectionTitle: string | undefined, sectionIndex: number): string {
2099
- return sectionTitle?.trim() || `untitled section ${sectionIndex + 1}`;
2100
- }
2101
-
2102
- function humanizeToolcraftLabelPart(value: string): string {
2103
- const text = value
2104
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
2105
- .replace(/[_-]+/g, " ")
2106
- .replace(/\s+/g, " ")
2107
- .trim();
2108
-
2109
- if (!text) {
2110
- return "";
2111
- }
2112
-
2113
- return text.replace(/\b([a-z])/g, (match) => match.toUpperCase());
2114
- }
2115
-
2116
- function lowerCaseToolcraftLabelStart(value: string): string {
2117
- return value ? `${value.charAt(0).toLowerCase()}${value.slice(1)}` : value;
2118
- }
2119
-
2120
- function normalizeToolcraftSemanticText(value: string | undefined): string {
2121
- return humanizeToolcraftLabelPart(value ?? "")
2122
- .toLowerCase()
2123
- .replace(/[^a-z0-9]+/g, "");
2124
- }
2125
-
2126
- function getToolcraftTargetParts(target: string): string[] {
2127
- return target.split(".").filter(Boolean);
2128
- }
2129
-
2130
- function getToolcraftTargetProperty(target: string): string {
2131
- return getToolcraftTargetParts(target).at(-1) ?? "";
2132
- }
2133
-
2134
- function getToolcraftStrictTargetPrefix(target: string): string | null {
2135
- const parts = getToolcraftTargetParts(target);
2136
-
2137
- if (parts.length < 3) {
2138
- return null;
2139
- }
2140
-
2141
- const prefix = parts.slice(0, -1).join(".");
2142
-
2143
- if (prefix === "canvas.size") {
2144
- return null;
2145
- }
2146
-
2147
- return prefix;
2148
- }
2149
-
2150
- function getToolcraftLooseTargetPrefix(target: string): string | null {
2151
- const parts = getToolcraftTargetParts(target);
2152
-
2153
- if (parts.length < 2) {
2154
- return null;
2155
- }
2156
-
2157
- const prefix = parts.slice(0, -1).join(".");
2158
-
2159
- if (prefix === "canvas.size") {
2160
- return null;
2161
- }
2162
-
2163
- return prefix;
2164
- }
2165
-
2166
- function isToolcraftWeakSectionContext(sectionTitle: string | undefined): boolean {
2167
- if (!sectionTitle) {
2168
- return true;
2169
- }
2170
-
2171
- return (
2172
- genericControlSectionTitlePattern.test(sectionTitle) ||
2173
- controlTypeSectionTitlePattern.test(sectionTitle) ||
2174
- weakControlLabelContextSectionTitlePattern.test(sectionTitle)
2175
- );
2176
- }
2177
-
2178
- function doesToolcraftSectionMatchTarget(
2179
- sectionTitle: string | undefined,
2180
- target: string,
2181
- ): boolean {
2182
- const sectionText = normalizeToolcraftSemanticText(sectionTitle);
2183
-
2184
- if (!sectionText) {
2185
- return false;
2186
- }
2187
-
2188
- return getToolcraftTargetParts(target).some((part) => {
2189
- const targetText = normalizeToolcraftSemanticText(part);
2190
- return (
2191
- targetText.length > 0 &&
2192
- (targetText === sectionText ||
2193
- targetText.includes(sectionText) ||
2194
- sectionText.includes(targetText))
2195
- );
2196
- });
2197
- }
2198
-
2199
- function getToolcraftSuggestedControlLabel(
2200
- control: ToolcraftControlSchema,
2201
- sectionTitle: string | undefined,
2202
- ): string {
2203
- const label = getControlLabelText(control).trim();
2204
- const targetProperty = humanizeToolcraftLabelPart(control.target.split(".").at(-1) ?? "");
2205
- const normalizedLabel = normalizeToolcraftSemanticText(label);
2206
- const normalizedTargetProperty = normalizeToolcraftSemanticText(targetProperty);
2207
-
2208
- if (
2209
- label &&
2210
- normalizedTargetProperty &&
2211
- normalizedTargetProperty !== normalizedLabel &&
2212
- normalizedTargetProperty.endsWith(normalizedLabel)
2213
- ) {
2214
- return targetProperty;
2215
- }
2216
-
2217
- const property = label || targetProperty;
2218
- const loosePrefix = getToolcraftLooseTargetPrefix(control.target);
2219
- const prefixParts = loosePrefix ? getToolcraftTargetParts(loosePrefix) : [];
2220
- const prefixEntity = humanizeToolcraftLabelPart(prefixParts.at(-1) ?? "");
2221
- const sectionEntity =
2222
- sectionTitle && !isToolcraftWeakSectionContext(sectionTitle)
2223
- ? humanizeToolcraftLabelPart(sectionTitle)
2224
- : "";
2225
- const entity = prefixEntity || sectionEntity;
2226
-
2227
- if (!entity) {
2228
- return property;
2229
- }
2230
-
2231
- const normalizedEntity = normalizeToolcraftSemanticText(entity);
2232
- const normalizedProperty = normalizeToolcraftSemanticText(property);
2233
-
2234
- if (normalizedEntity && normalizedProperty.includes(normalizedEntity)) {
2235
- return property;
2236
- }
2237
-
2238
- return `${entity} ${lowerCaseToolcraftLabelStart(property)}`;
2239
- }
2240
-
2241
- function getToolcraftFontPickerOwnedTypographyPart(
2242
- control: ToolcraftControlSchema,
2243
- ): string | undefined {
2244
- if (control.type === "fontPicker") {
2245
- return undefined;
2246
- }
2247
-
2248
- const normalizedCandidates = [
2249
- getToolcraftTargetProperty(control.target),
2250
- getControlLabelText(control),
2251
- ].map(normalizeToolcraftSemanticText);
2252
-
2253
- for (const candidate of normalizedCandidates) {
2254
- const ownedPart = fontPickerOwnedTypographyPartLabels.get(candidate);
2255
-
2256
- if (ownedPart) {
2257
- return ownedPart;
2258
- }
2259
- }
2260
-
2261
- return undefined;
2262
- }
2263
-
2264
- function getToolcraftControlSemanticCluster(
2265
- controlId: string,
2266
- control: ToolcraftControlSchema,
2267
- ): string {
2268
- const text = [
2269
- controlId,
2270
- getToolcraftTargetProperty(control.target),
2271
- getControlLabelText(control),
2272
- control.description ?? "",
2273
- ]
2274
- .filter(Boolean)
2275
- .join(" ");
2276
-
2277
- for (const [cluster, pattern] of controlSemanticClusterPatterns) {
2278
- if (pattern.test(text)) {
2279
- return cluster;
2280
- }
2281
- }
2282
-
2283
- return inferToolcraftControlOrderRole(controlId, control);
2284
- }
2285
-
2286
- function normalizeToolcraftConditionValue(value: unknown): string | null {
2287
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2288
- const normalized = normalizeToolcraftSemanticText(String(value));
2289
- return normalized || null;
2290
- }
2291
-
2292
- return null;
2293
- }
2294
-
2295
- function getToolcraftConditionBranchTexts(
2296
- condition: ToolcraftControlConditionSchema,
2297
- gateControl: ToolcraftControlSchema,
2298
- ): string[] {
2299
- const rawValues =
2300
- "equals" in condition
2301
- ? [condition.equals]
2302
- : Array.isArray(condition.oneOf)
2303
- ? [...condition.oneOf]
2304
- : [];
2305
- const normalizedValues = rawValues
2306
- .map(normalizeToolcraftConditionValue)
2307
- .filter((value): value is string => Boolean(value));
2308
- const optionLabels = (gateControl.options ?? [])
2309
- .filter((option) =>
2310
- normalizedValues.includes(normalizeToolcraftSemanticText(option.value)),
2311
- )
2312
- .map((option) => normalizeToolcraftSemanticText(option.label))
2313
- .filter(Boolean);
2314
-
2315
- return [...new Set([...normalizedValues, ...optionLabels])];
2316
- }
2317
-
2318
- function sectionTitleLooksLikeDependencyBranch({
2319
- condition,
2320
- gateControl,
2321
- sectionTitle,
2322
- }: {
2323
- condition: ToolcraftControlConditionSchema;
2324
- gateControl: ToolcraftControlSchema;
2325
- sectionTitle: string | undefined;
2326
- }): boolean {
2327
- const sectionText = normalizeToolcraftSemanticText(sectionTitle);
2328
-
2329
- if (!sectionText) {
2330
- return false;
2331
- }
2332
-
2333
- return getToolcraftConditionBranchTexts(condition, gateControl).some(
2334
- (branchText) =>
2335
- branchText.length > 2 &&
2336
- (sectionText === branchText ||
2337
- sectionText.includes(branchText) ||
2338
- branchText.includes(sectionText)),
2339
- );
2340
- }
2341
-
2342
- function isToolcraftProductSectionControl(control: ToolcraftControlSchema): boolean {
2343
- return (
2344
- control.type !== "panelActions" &&
2345
- control.type !== "settingsTransfer" &&
2346
- !isRuntimeSetupControlTarget(control.target)
2347
- );
2348
- }
2349
-
2350
- function isToolcraftVisibleAcceptanceControl(control: ToolcraftControlSchema): boolean {
2351
- return control.type === "panelActions" || isToolcraftProductSectionControl(control);
2352
- }
2353
-
2354
- function getToolcraftGenericControlLabelError({
2355
- control,
2356
- controlId,
2357
- sectionLabel,
2358
- sectionLoosePrefixCount,
2359
- sectionTitle,
2360
- }: {
2361
- control: ToolcraftControlSchema;
2362
- controlId: string;
2363
- sectionLabel: string;
2364
- sectionLoosePrefixCount: number;
2365
- sectionTitle: string | undefined;
2366
- }): string | undefined {
2367
- const label = getControlLabelText(control).trim();
2368
-
2369
- if (!genericControlLabelPattern.test(label)) {
2370
- return undefined;
2371
- }
2372
-
2373
- const hasWeakContext =
2374
- isToolcraftWeakSectionContext(sectionTitle) ||
2375
- (sectionLoosePrefixCount > 1 &&
2376
- !doesToolcraftSectionMatchTarget(sectionTitle, control.target));
2377
-
2378
- if (!hasWeakContext) {
2379
- return undefined;
2380
- }
2381
-
2382
- const suggestedLabel = getToolcraftSuggestedControlLabel(control, sectionTitle);
2383
-
2384
- return `${sectionLabel} / ${controlId} label "${label}" is too generic in this context. Short labels are allowed when the nearest visible section or group clearly names the affected product entity. Rename it to "${suggestedLabel}".`;
2385
- }
2386
-
2387
- function getToolcraftControlDescriptionError({
2388
- control,
2389
- controlId,
2390
- sectionLabel,
2391
- sectionTitle,
2392
- }: {
2393
- control: ToolcraftControlSchema;
2394
- controlId: string;
2395
- sectionLabel: string;
2396
- sectionTitle: string | undefined;
2397
- }): string | undefined {
2398
- const description = control.description?.trim();
2399
-
2400
- if (!description) {
2401
- return undefined;
2402
- }
2403
-
2404
- const label = getControlLabelText(control).trim();
2405
-
2406
- if (
2407
- isToolcraftObviousColorSectionControlDescription({
2408
- control,
2409
- description,
2410
- label,
2411
- sectionTitle,
2412
- })
2413
- ) {
2414
- return `${sectionLabel} / ${controlId} description adds a help icon to an obvious color-section control. Omit control.description when the section title and visible label already explain the setting.`;
2415
- }
2416
-
2417
- if (control.type !== "fontPicker") {
2418
- return undefined;
2419
- }
2420
-
2421
- const repeatedParts = fontPickerDescriptionOwnedPartPatterns
2422
- .filter(([, pattern]) => pattern.test(description))
2423
- .map(([part]) => part);
2424
-
2425
- if (repeatedParts.length < 2) {
2426
- return undefined;
2427
- }
2428
-
2429
- return `${sectionLabel} / ${controlId} description repeats FontPicker-owned fields (${repeatedParts.join(", ")}). FontPicker help must explain only non-obvious product behavior; use section titles and visible field labels for font family, weight, size, case, color, opacity, letter spacing, and line height, or omit description.`;
2430
- }
2431
-
2432
- function isToolcraftColorSectionTitle(sectionTitle: string | undefined): boolean {
2433
- return /\b(colou?rs?|palette|palettes|shades?|accents?)\b/i.test(
2434
- sectionTitle ?? "",
2435
- );
2436
- }
2437
-
2438
- function isToolcraftSequentialColorLabel(label: string): boolean {
2439
- return /^colou?r\s+\d+$/i.test(label.trim());
2440
- }
2441
-
2442
- function isToolcraftPaletteVariationTarget(target: string): boolean {
2443
- return (
2444
- /(?:^|\.)(?:palette|palettes|colou?rs?|shades?|accents?)\b/i.test(
2445
- target,
2446
- ) || /(?:^|\.)(?:accent|shade|colou?r)\d+\b/i.test(target)
2447
- );
2448
- }
2449
-
2450
- function isToolcraftSimplePaletteDistributionLabel(label: string): boolean {
2451
- return /^(spread|mix|distribution)$/i.test(label.trim());
2452
- }
2453
-
2454
- function isToolcraftGenericControlHelpDescription(description: string): boolean {
2455
- return /^(adjusts?|changes?|chooses?|controls?|defines?|selects?|sets?|updates?)\b/i.test(
2456
- description.trim(),
2457
- );
2458
- }
2459
-
2460
- function isToolcraftObviousColorSectionControlDescription({
2461
- control,
2462
- description,
2463
- label,
2464
- sectionTitle,
2465
- }: {
2466
- control: ToolcraftControlSchema;
2467
- description: string;
2468
- label: string;
2469
- sectionTitle: string | undefined;
2470
- }): boolean {
2471
- if (!isToolcraftColorSectionTitle(sectionTitle)) {
2472
- return false;
2473
- }
2474
-
2475
- if (
2476
- (control.type === "color" || control.type === "colorOpacity") &&
2477
- isToolcraftSequentialColorLabel(label)
2478
- ) {
2479
- return true;
2480
- }
2481
-
2482
- return (
2483
- isToolcraftSimplePaletteDistributionLabel(label) &&
2484
- isToolcraftGenericControlHelpDescription(description)
2485
- );
2486
- }
2487
-
2488
- function getToolcraftColorBankLabelErrors({
2489
- controls,
2490
- sectionLabel,
2491
- sectionTitle,
2492
- }: {
2493
- controls: readonly [string, ToolcraftControlSchema][];
2494
- sectionLabel: string;
2495
- sectionTitle: string | undefined;
2496
- }): string[] {
2497
- const colorControls = controls.filter(([, control]) => {
2498
- if (control.type !== "color" && control.type !== "colorOpacity") {
2499
- return false;
2500
- }
2501
-
2502
- return true;
2503
- });
2504
-
2505
- if (colorControls.length < 2) {
2506
- return [];
2507
- }
2508
-
2509
- const loosePrefixes = new Set(
2510
- colorControls
2511
- .map(([, control]) => getToolcraftLooseTargetPrefix(control.target))
2512
- .filter((prefix): prefix is string => Boolean(prefix)),
2513
- );
2514
-
2515
- if (loosePrefixes.size !== 1) {
2516
- return [];
2517
- }
2518
-
2519
- const sequentialColorControls = colorControls.filter(([, control]) =>
2520
- isToolcraftSequentialColorLabel(getControlLabelText(control)),
2521
- );
2522
- const isPaletteVariationBank =
2523
- colorControls.every(([, control]) =>
2524
- isToolcraftPaletteVariationTarget(control.target),
2525
- ) ||
2526
- (isToolcraftColorSectionTitle(sectionTitle) &&
2527
- sequentialColorControls.length > 0);
2528
-
2529
- if (!isPaletteVariationBank) {
2530
- return [];
2531
- }
2532
-
2533
- const visibleColorControls = colorControls.filter(([, control]) =>
2534
- hasVisibleControlLabel(control),
2535
- );
2536
- const errors: string[] = [];
2537
-
2538
- if (
2539
- visibleColorControls.length > 0 &&
2540
- visibleColorControls.length < colorControls.length
2541
- ) {
2542
- errors.push(
2543
- `${sectionLabel} mixes labeled and unlabeled color items in one palette variation group. Decide label visibility for the whole group: omit all per-item labels when colors only add variety, or label every item only when each color has a distinct user-facing role.`,
2544
- );
2545
- }
2546
-
2547
- for (const [controlId, control] of sequentialColorControls) {
2548
- const label = getControlLabelText(control).trim();
2549
-
2550
- errors.push(
2551
- `${sectionLabel} / ${controlId} uses visible label "${label}" for a palette variation color. When colors only add variety to one shared palette, set label: false or use collectionActions with unlabeled items. Keep visible labels only when each color edits a distinct user-facing entity such as Fill, Stroke, Background, Connector, or Object color.`,
2552
- );
2553
- }
2554
-
2555
- return errors;
2556
- }
2557
-
2558
- function getToolcraftSectionInventoryByTitle(
2559
- sectionInventory: readonly ToolcraftControlSectionInventoryEntry[],
2560
- ): Map<string, ToolcraftControlSectionInventoryEntry> {
2561
- return new Map(
2562
- sectionInventory.map((entry) => [entry.title.trim(), entry]),
2563
- );
2564
- }
2565
-
2566
- function hasToolcraftInventorySplitEvidence({
2567
- sectionInventoryByTitle,
2568
- sections,
2569
- }: {
2570
- sectionInventoryByTitle: ReadonlyMap<string, ToolcraftControlSectionInventoryEntry>;
2571
- sections: ReadonlySet<string>;
2572
- }): boolean {
2573
- const entries = [...sections].map((sectionLabel) =>
2574
- sectionInventoryByTitle.get(sectionLabel),
2575
- );
2576
-
2577
- return (
2578
- entries.length === sections.size &&
2579
- entries.every(
2580
- (entry) =>
2581
- entry &&
2582
- (entry.workflowStage?.trim().length ?? 0) > 0 &&
2583
- (entry.splitReason?.trim().length ?? 0) >= 12,
2584
- )
2585
- );
2586
- }
2587
-
2588
- function getToolcraftControlSectionInventoryErrors(
2589
- schema: ResolvedToolcraftAppSchema,
2590
- sectionInventory: readonly ToolcraftControlSectionInventoryEntry[],
2591
- ): string[] {
2592
- const errors: string[] = [];
2593
-
2594
- if (sectionInventory.length === 0) {
2595
- return errors;
2596
- }
2597
-
2598
- const schemaSections = (schema.panels.controls?.sections ?? []).flatMap(
2599
- (section, sectionIndex) => {
2600
- const targets = Object.values(section.controls)
2601
- .filter(isToolcraftProductSectionControl)
2602
- .map((control) => control.target);
2603
-
2604
- if (targets.length === 0) {
2605
- return [];
2606
- }
2607
-
2608
- return [
2609
- {
2610
- label: getToolcraftSectionLabel(section.title, sectionIndex),
2611
- targets,
2612
- },
2613
- ];
2614
- },
2615
- );
2616
- const schemaSectionTitles = new Set(schemaSections.map((section) => section.label));
2617
- const schemaTargetToSection = new Map<string, string>();
2618
-
2619
- for (const section of schemaSections) {
2620
- for (const target of section.targets) {
2621
- schemaTargetToSection.set(target, section.label);
2622
- }
2623
- }
2624
-
2625
- const inventoryTitleCounts = new Map<string, number>();
2626
- const inventoryTargetToSection = new Map<string, string>();
2627
-
2628
- for (const entry of sectionInventory) {
2629
- const title = entry.title.trim();
2630
- const entity = entry.entity?.trim() ?? "";
2631
- const workflowStage = entry.workflowStage?.trim() ?? "";
2632
- const groupingReason = entry.groupingReason.trim();
2633
- const splitReason = entry.splitReason?.trim() ?? "";
2634
-
2635
- if (!title) {
2636
- errors.push(
2637
- "Control Section Inventory contains an entry without a section title.",
2638
- );
2639
- continue;
2640
- }
2641
-
2642
- inventoryTitleCounts.set(title, (inventoryTitleCounts.get(title) ?? 0) + 1);
2643
-
2644
- if (!schemaSectionTitles.has(title)) {
2645
- errors.push(
2646
- `Control Section Inventory references "${title}", but no rendered product controls section uses that title.`,
2647
- );
2648
- }
2649
-
2650
- if (!entity && !workflowStage) {
2651
- errors.push(
2652
- `Control Section Inventory entry "${title}" must declare entity or workflowStage so grouping is based on product meaning, not UI layout.`,
2653
- );
2654
- }
2655
-
2656
- if (groupingReason.length < 12) {
2657
- errors.push(
2658
- `Control Section Inventory entry "${title}" must include a concrete groupingReason explaining why these controls belong together.`,
2659
- );
2660
- }
2661
-
2662
- if (splitReason && splitReason.length < 12) {
2663
- errors.push(
2664
- `Control Section Inventory entry "${title}" splitReason is too vague. Explain the product workflow split or omit splitReason.`,
2665
- );
2666
- }
2667
-
2668
- if (entry.targets.length === 0) {
2669
- errors.push(
2670
- `Control Section Inventory entry "${title}" must list the product control targets rendered in that section.`,
2671
- );
2672
- }
2673
-
2674
- for (const target of entry.targets) {
2675
- const schemaSection = schemaTargetToSection.get(target);
2676
-
2677
- if (!schemaSection) {
2678
- errors.push(
2679
- `Control Section Inventory entry "${title}" lists target "${target}", but that target is not rendered by any product controls section.`,
2680
- );
2681
- continue;
2682
- }
2683
-
2684
- if (schemaSection !== title) {
2685
- errors.push(
2686
- `Control Section Inventory entry "${title}" lists target "${target}", but the schema renders it in "${schemaSection}". Update the schema grouping or the inventory.`,
2687
- );
2688
- }
2689
-
2690
- const existingSection = inventoryTargetToSection.get(target);
2691
-
2692
- if (existingSection && existingSection !== title) {
2693
- errors.push(
2694
- `Control Section Inventory lists target "${target}" in both "${existingSection}" and "${title}". Each product control target belongs to exactly one section inventory entry.`,
2695
- );
2696
- }
2697
-
2698
- inventoryTargetToSection.set(target, title);
2699
- }
2700
- }
2701
-
2702
- for (const [title, count] of inventoryTitleCounts) {
2703
- if (count > 1) {
2704
- errors.push(
2705
- `Control Section Inventory repeats section "${title}" ${count} times. Section inventory entries must be unique.`,
2706
- );
2707
- }
2708
- }
2709
-
2710
- for (const section of schemaSections) {
2711
- const entry = sectionInventory.find(
2712
- (inventoryEntry) => inventoryEntry.title.trim() === section.label,
2713
- );
2714
-
2715
- if (!entry) {
2716
- errors.push(
2717
- `Control Section Inventory is missing product section "${section.label}". Add its entity/workflow stage, targets, and groupingReason.`,
2718
- );
2719
- continue;
2720
- }
2721
-
2722
- const inventoryTargets = new Set(entry.targets);
2723
-
2724
- for (const target of section.targets) {
2725
- if (!inventoryTargets.has(target)) {
2726
- errors.push(
2727
- `Control Section Inventory entry "${section.label}" is missing rendered target "${target}". The inventory must cover every product control in the section.`,
2728
- );
2729
- }
2730
- }
2731
- }
2732
-
2733
- return errors;
2734
- }
2735
-
2736
- function getToolcraftControlSectionGroupingErrors(
2737
- schema: ResolvedToolcraftAppSchema,
2738
- sectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = [],
2739
- ): string[] {
2740
- const errors: string[] = [];
2741
- const visibleControls: Array<{
2742
- control: ToolcraftControlSchema;
2743
- controlId: string;
2744
- loosePrefix: string | null;
2745
- sectionLabel: string;
2746
- sectionTitle: string | undefined;
2747
- }> = [];
2748
- const strictPrefixSections = new Map<string, Set<string>>();
2749
- const loosePrefixSections = new Map<string, Set<string>>();
2750
- const colorSectionLoosePrefixes = new Map<string, string>();
2751
- const sectionInventoryByTitle = getToolcraftSectionInventoryByTitle(sectionInventory);
2752
- const sectionTitleCounts = new Map<string, { count: number; label: string }>();
2753
- const controlsByTarget = new Map<
2754
- string,
2755
- {
2756
- control: ToolcraftControlSchema;
2757
- controlId: string;
2758
- loosePrefix: string | null;
2759
- sectionLabel: string;
2760
- sectionTitle: string | undefined;
2761
- }
2762
- >();
2763
-
2764
- for (const [sectionIndex, section] of (schema.panels.controls?.sections ?? []).entries()) {
2765
- const sectionTitle = section.title?.trim();
2766
- const sectionLabel = getToolcraftSectionLabel(sectionTitle, sectionIndex);
2767
- const controls = Object.entries(section.controls).filter(([, control]) =>
2768
- isToolcraftProductSectionControl(control),
2769
- );
2770
-
2771
- if (controls.length === 0) {
2772
- continue;
2773
- }
2774
-
2775
- if (!sectionTitle) {
2776
- errors.push(
2777
- `${sectionLabel} is missing a controls section title. Every visible controls-panel section must name the product entity, workflow stage, or behavior it edits.`,
2778
- );
2779
- }
2780
-
2781
- if (sectionTitle) {
2782
- const normalizedSectionTitle = normalizeToolcraftSemanticText(sectionTitle);
2783
- const titleCount = sectionTitleCounts.get(normalizedSectionTitle);
2784
- sectionTitleCounts.set(normalizedSectionTitle, {
2785
- count: (titleCount?.count ?? 0) + 1,
2786
- label: titleCount?.label ?? sectionTitle,
2787
- });
2788
- }
2789
-
2790
- if (sectionTitle && genericControlSectionTitlePattern.test(sectionTitle)) {
2791
- errors.push(
2792
- `${sectionLabel} is too generic for a controls section. Name the product entity, workflow stage, or behavior it edits instead of using a bucket title.`,
2793
- );
2794
- }
2795
-
2796
- if (sectionTitle && controlTypeSectionTitlePattern.test(sectionTitle)) {
2797
- errors.push(
2798
- `${sectionLabel} names a UI control type instead of the product entity. Group controls by product meaning, not by Slider, Color, Input, Button, or similar component type.`,
2799
- );
2800
- }
2801
-
2802
- const sectionLoosePrefixes = new Set(
2803
- controls
2804
- .map(([, control]) => getToolcraftLooseTargetPrefix(control.target))
2805
- .filter((prefix): prefix is string => Boolean(prefix)),
2806
- );
2807
- const productControls = controls.filter(([, control]) =>
2808
- isToolcraftProductSectionControl(control),
2809
- );
2810
- const semanticClusters = new Set(
2811
- productControls.map(([controlId, control]) =>
2812
- getToolcraftControlSemanticCluster(controlId, control),
2813
- ),
2814
- );
2815
- const clusterList = [...semanticClusters].join(", ");
2816
- const hasBroadSectionTitle =
2817
- sectionTitle !== undefined && broadControlSectionTitlePattern.test(sectionTitle);
2818
-
2819
- if (
2820
- productControls.length > maxPreferredControlsPerSection &&
2821
- hasBroadSectionTitle &&
2822
- semanticClusters.size >= 3
2823
- ) {
2824
- errors.push(
2825
- `${sectionLabel} has ${productControls.length} controls across multiple semantic clusters (${clusterList}). Broad section titles are only valid for small cohesive groups; split this into discrete sections with specific titles such as motion, geometry, density, color, typography, or export sub-entities.`,
2826
- );
2827
- }
2828
-
2829
- if (productControls.length > maxHardControlsPerSection && semanticClusters.size > 1) {
2830
- errors.push(
2831
- `${sectionLabel} has ${productControls.length} controls across ${semanticClusters.size} semantic clusters (${clusterList}). Controls-panel sections should stay discrete; split sections that grow past ${maxHardControlsPerSection} controls unless every control edits one tightly scoped entity.`,
2832
- );
2833
- }
2834
-
2835
- errors.push(
2836
- ...getToolcraftColorBankLabelErrors({
2837
- controls,
2838
- sectionLabel,
2839
- sectionTitle,
2840
- }),
2841
- );
2842
-
2843
- for (const [controlId, control] of controls) {
2844
- const strictPrefix = getToolcraftStrictTargetPrefix(control.target);
2845
- const loosePrefix = getToolcraftLooseTargetPrefix(control.target);
2846
- const genericLabelError = getToolcraftGenericControlLabelError({
2847
- control,
2848
- controlId,
2849
- sectionLabel,
2850
- sectionLoosePrefixCount: sectionLoosePrefixes.size,
2851
- sectionTitle,
2852
- });
2853
-
2854
- if (genericLabelError) {
2855
- errors.push(genericLabelError);
2856
- }
2857
-
2858
- const descriptionError = getToolcraftControlDescriptionError({
2859
- control,
2860
- controlId,
2861
- sectionLabel,
2862
- sectionTitle,
2863
- });
2864
-
2865
- if (descriptionError) {
2866
- errors.push(descriptionError);
2867
- }
2868
-
2869
- visibleControls.push({
2870
- control,
2871
- controlId,
2872
- loosePrefix,
2873
- sectionLabel,
2874
- sectionTitle,
2875
- });
2876
- controlsByTarget.set(control.target, {
2877
- control,
2878
- controlId,
2879
- loosePrefix,
2880
- sectionLabel,
2881
- sectionTitle,
2882
- });
2883
-
2884
- if (strictPrefix) {
2885
- const sections = strictPrefixSections.get(strictPrefix) ?? new Set<string>();
2886
- sections.add(sectionLabel);
2887
- strictPrefixSections.set(strictPrefix, sections);
2888
- }
2889
-
2890
- if (loosePrefix) {
2891
- const sections = loosePrefixSections.get(loosePrefix) ?? new Set<string>();
2892
- sections.add(sectionLabel);
2893
- loosePrefixSections.set(loosePrefix, sections);
2894
- }
2895
-
2896
- if (
2897
- control.type === "color" &&
2898
- sectionTitle &&
2899
- /^colors?$/i.test(sectionTitle) &&
2900
- loosePrefix
2901
- ) {
2902
- colorSectionLoosePrefixes.set(loosePrefix, `${sectionLabel} / ${controlId}`);
2903
- }
2904
- }
2905
- }
2906
-
2907
- for (const { count, label } of sectionTitleCounts.values()) {
2908
- if (count > 1) {
2909
- errors.push(
2910
- `Controls panel repeats the section title "${label}" ${count} times. Section titles must be unique and describe distinct product entities or workflow stages.`,
2911
- );
2912
- }
2913
- }
2914
-
2915
- for (const item of visibleControls) {
2916
- for (const [conditionName, condition] of [
2917
- ["visibleWhen", item.control.visibleWhen],
2918
- ["disabledWhen", item.control.disabledWhen],
2919
- ] as const) {
2920
- if (!condition || !item.loosePrefix) {
2921
- continue;
2922
- }
2923
-
2924
- const gateControl = controlsByTarget.get(condition.target);
2925
-
2926
- if (!gateControl || gateControl.sectionLabel === item.sectionLabel) {
2927
- continue;
2928
- }
2929
-
2930
- const sharesTargetEntity =
2931
- gateControl.loosePrefix !== null &&
2932
- gateControl.loosePrefix === item.loosePrefix;
2933
- const looksLikeBranchSection =
2934
- conditionName === "visibleWhen" &&
2935
- sectionTitleLooksLikeDependencyBranch({
2936
- condition,
2937
- gateControl: gateControl.control,
2938
- sectionTitle: item.sectionTitle,
2939
- });
2940
-
2941
- if (!sharesTargetEntity && !looksLikeBranchSection) {
2942
- continue;
2943
- }
2944
-
2945
- errors.push(
2946
- `${item.sectionLabel} / ${item.controlId} is gated by ${conditionName} target "${condition.target}" in ${gateControl.sectionLabel}, but it belongs to the same dependency group. Keep selectors and their dependent controls in one semantic section when they describe one product entity or branch; use visibleWhen for branch-specific controls inside that section instead of splitting branch controls into their own section. Do not use disabledWhen for product controls.`,
2947
- );
2948
- }
2949
- }
2950
-
2951
- const fontPickerControls = visibleControls.filter(
2952
- (item) => item.control.type === "fontPicker" && item.loosePrefix,
2953
- );
2954
-
2955
- for (const item of visibleControls) {
2956
- if (!item.loosePrefix || item.control.type === "fontPicker") {
2957
- continue;
2958
- }
2959
-
2960
- const ownedTypographyPart =
2961
- getToolcraftFontPickerOwnedTypographyPart(item.control);
2962
-
2963
- if (!ownedTypographyPart) {
2964
- continue;
2965
- }
2966
-
2967
- const owningFontPicker = fontPickerControls.find(
2968
- (fontPicker) => fontPicker.loosePrefix === item.loosePrefix,
2969
- );
2970
-
2971
- if (!owningFontPicker) {
2972
- continue;
2973
- }
2974
-
2975
- const label = getControlLabelText(item.control).trim() || item.controlId;
2976
-
2977
- errors.push(
2978
- `${item.sectionLabel} / ${item.controlId} splits "${label}" out of the FontPicker-owned typography block for "${item.loosePrefix}". Keep font family, weight, size, case, letter spacing, line height, color, and opacity in the same fontPicker value.`,
2979
- );
2980
- }
2981
-
2982
- for (const [prefix, sections] of strictPrefixSections) {
2983
- if (
2984
- sections.size > 1 &&
2985
- !hasToolcraftInventorySplitEvidence({ sectionInventoryByTitle, sections })
2986
- ) {
2987
- errors.push(
2988
- `Controls for product entity "${prefix}" are split across sections: ${[...sections].join(", ")}. Keep controls for the same product entity in one semantic section unless the Control Section Inventory declares workflowStage and splitReason for every split section.`,
2989
- );
2990
- }
2991
- }
1
+ import {
2
+ getToolcraftControlKeyframeCapability,
3
+ type ResolvedToolcraftAppSchema,
4
+ } from "@repo/toolcraft-runtime";
2992
5
 
2993
- for (const [prefix, colorControlLabel] of colorSectionLoosePrefixes) {
2994
- const sections = loosePrefixSections.get(prefix);
6
+ import { getRequiredToolcraftControlPartCoverage } from "./acceptance/control-parts";
7
+ import {
8
+ getToolcraftControlOrder as getToolcraftControlOrderForSchema,
9
+ getToolcraftControlOrderTargets as getToolcraftControlOrderTargetsForSchema,
10
+ inferToolcraftControlOrderRole,
11
+ } from "./acceptance/control-order";
12
+ import type {
13
+ ToolcraftAcceptanceEvidence,
14
+ ToolcraftAnimationIntent,
15
+ ToolcraftAutonomousAnimationCoverage,
16
+ ToolcraftBuiltInControlType,
17
+ ToolcraftBuiltInFitCheck,
18
+ ToolcraftBackgroundOutputCoverage,
19
+ ToolcraftCanvasSizingCoverage,
20
+ ToolcraftConditionalVisibilityCoverage,
21
+ ToolcraftComponentAcceptance,
22
+ ToolcraftControlOrderItem,
23
+ ToolcraftControlPartCoverage,
24
+ ToolcraftControlSectionInventoryEntry,
25
+ ToolcraftCustomControlCoverage,
26
+ ToolcraftLayerCoverage,
27
+ ToolcraftPersistenceCoverage,
28
+ ToolcraftProductReadiness,
29
+ ToolcraftReferenceCoverage,
30
+ ToolcraftReferenceFeatureInventoryItem,
31
+ ToolcraftReferenceFeatureStatus,
32
+ ToolcraftReferenceStudyEvidence,
33
+ ToolcraftReferenceStudyStatus,
34
+ ToolcraftReferenceTimelineContract,
35
+ ToolcraftReferenceTimelineCoverage,
36
+ ToolcraftReferenceTimelineMode,
37
+ ToolcraftSettingsTransferCoverage,
38
+ ToolcraftTimelineLoopDurationIntent,
39
+ ToolcraftTimelineLoopDurationSource,
40
+ ToolcraftTimelinePlaybackCoverage,
41
+ ToolcraftTransferMode,
42
+ ToolcraftVideoReferenceAcceptanceMapping,
43
+ ToolcraftVideoReferenceStoryboardFrame,
44
+ ToolcraftVideoReferenceStudyEvidence,
45
+ ToolcraftVideoReferenceTransition,
46
+ ToolcraftVisibleControl,
47
+ } from "./acceptance/types";
48
+ import {
49
+ collectToolcraftVisibleAcceptanceControls,
50
+ validateToolcraftAcceptanceDiagnostics as validateToolcraftAcceptanceDiagnosticsInput,
51
+ validateToolcraftAcceptanceCoverage as validateToolcraftAcceptanceInput,
52
+ type ToolcraftAcceptanceValidationInput,
53
+ } from "./acceptance/validate-coverage";
54
+ import type { ToolcraftAcceptanceDiagnostic } from "./acceptance/validation-pipeline";
55
+ import {
56
+ starterAcceptance,
57
+ starterControlSectionInventory,
58
+ starterProductReadiness,
59
+ starterTransferMode,
60
+ } from "./starter-acceptance-data";
61
+ import { starterSchema } from "./starter-schema";
2995
62
 
2996
- if (sections && sections.size > 1) {
2997
- errors.push(
2998
- `${colorControlLabel} is separated from other "${prefix}" controls. A color that configures the same product entity belongs inside that entity section with a concise field label that stays unambiguous in context.`,
2999
- );
3000
- }
3001
- }
63
+ export {
64
+ getRequiredToolcraftControlPartCoverage,
65
+ inferToolcraftControlOrderRole,
66
+ starterAcceptance,
67
+ starterControlSectionInventory,
68
+ starterProductReadiness,
69
+ starterTransferMode,
70
+ };
71
+ export type {
72
+ ToolcraftAcceptanceEvidence,
73
+ ToolcraftAcceptanceDiagnostic,
74
+ ToolcraftAnimationIntent,
75
+ ToolcraftAutonomousAnimationCoverage,
76
+ ToolcraftBuiltInControlType,
77
+ ToolcraftBuiltInFitCheck,
78
+ ToolcraftBackgroundOutputCoverage,
79
+ ToolcraftCanvasSizingCoverage,
80
+ ToolcraftConditionalVisibilityCoverage,
81
+ ToolcraftComponentAcceptance,
82
+ ToolcraftControlOrderItem,
83
+ ToolcraftControlPartCoverage,
84
+ ToolcraftControlSectionInventoryEntry,
85
+ ToolcraftCustomControlCoverage,
86
+ ToolcraftLayerCoverage,
87
+ ToolcraftPersistenceCoverage,
88
+ ToolcraftProductReadiness,
89
+ ToolcraftReferenceCoverage,
90
+ ToolcraftReferenceFeatureInventoryItem,
91
+ ToolcraftReferenceFeatureStatus,
92
+ ToolcraftReferenceStudyEvidence,
93
+ ToolcraftReferenceStudyStatus,
94
+ ToolcraftReferenceTimelineContract,
95
+ ToolcraftReferenceTimelineCoverage,
96
+ ToolcraftReferenceTimelineMode,
97
+ ToolcraftSettingsTransferCoverage,
98
+ ToolcraftTimelineLoopDurationIntent,
99
+ ToolcraftTimelineLoopDurationSource,
100
+ ToolcraftTimelinePlaybackCoverage,
101
+ ToolcraftTransferMode,
102
+ ToolcraftVideoReferenceAcceptanceMapping,
103
+ ToolcraftVideoReferenceStoryboardFrame,
104
+ ToolcraftVideoReferenceStudyEvidence,
105
+ ToolcraftVideoReferenceTransition,
106
+ ToolcraftVisibleControl,
107
+ };
3002
108
 
3003
- return errors;
109
+ export function validateToolcraftAcceptanceDiagnostics(
110
+ input: ToolcraftAcceptanceValidationInput,
111
+ ): ToolcraftAcceptanceDiagnostic[] {
112
+ return validateToolcraftAcceptanceDiagnosticsInput(input);
3004
113
  }
3005
114
 
3006
115
  export function collectToolcraftVisibleControls(
3007
116
  schema: ResolvedToolcraftAppSchema = starterSchema,
3008
117
  ): ToolcraftVisibleControl[] {
3009
- return (schema.panels.controls?.sections ?? []).flatMap((section) =>
3010
- Object.entries(section.controls)
3011
- .filter(([, control]) => isToolcraftVisibleAcceptanceControl(control))
3012
- .map(([controlId, control]) => ({
3013
- control,
3014
- controlId,
3015
- sectionTitle: section.title,
3016
- })),
3017
- );
118
+ return collectToolcraftVisibleAcceptanceControls(schema);
3018
119
  }
3019
120
 
3020
121
  export function collectToolcraftKeyframeableControls(
@@ -3028,1585 +129,59 @@ export function collectToolcraftKeyframeableControls(
3028
129
  export function getToolcraftControlOrder(
3029
130
  schema: ResolvedToolcraftAppSchema = starterSchema,
3030
131
  ): ToolcraftControlOrderItem[] {
3031
- return (schema.panels.controls?.sections ?? []).flatMap((section) =>
3032
- Object.entries(section.controls)
3033
- .filter(([, control]) => isToolcraftProductSectionControl(control))
3034
- .map(([controlId, control]) => {
3035
- const role = inferToolcraftControlOrderRole(controlId, control);
3036
-
3037
- return {
3038
- controlId,
3039
- rank: controlOrderRoleRanks[role],
3040
- role,
3041
- sectionTitle: section.title,
3042
- target: control.target,
3043
- type: control.type,
3044
- };
3045
- }),
3046
- );
132
+ return getToolcraftControlOrderForSchema(schema);
3047
133
  }
3048
134
 
3049
135
  export function getToolcraftControlOrderTargets(
3050
136
  schema: ResolvedToolcraftAppSchema = starterSchema,
3051
137
  ): string[] {
3052
- return getToolcraftControlOrder(schema).map((item) => item.target);
138
+ return getToolcraftControlOrderTargetsForSchema(schema);
139
+ }
140
+
141
+ function isToolcraftAcceptanceValidationInput(
142
+ value: ToolcraftAcceptanceValidationInput | ResolvedToolcraftAppSchema,
143
+ ): value is ToolcraftAcceptanceValidationInput {
144
+ return (
145
+ "acceptance" in value &&
146
+ "schema" in value &&
147
+ "sectionInventory" in value &&
148
+ "transferMode" in value
149
+ );
3053
150
  }
3054
151
 
3055
152
  export function validateToolcraftAcceptanceCoverage(
3056
- schema: ResolvedToolcraftAppSchema = starterSchema,
153
+ input: ToolcraftAcceptanceValidationInput,
154
+ ): string[];
155
+ export function validateToolcraftAcceptanceCoverage(
156
+ schema?: ResolvedToolcraftAppSchema,
157
+ acceptance?: readonly ToolcraftComponentAcceptance[],
158
+ transferMode?: ToolcraftTransferMode,
159
+ sectionInventory?: readonly ToolcraftControlSectionInventoryEntry[],
160
+ ): string[];
161
+ export function validateToolcraftAcceptanceCoverage(
162
+ inputOrSchema: ToolcraftAcceptanceValidationInput | ResolvedToolcraftAppSchema = starterSchema,
3057
163
  acceptance: readonly ToolcraftComponentAcceptance[] = starterAcceptance,
3058
164
  transferMode: ToolcraftTransferMode = starterTransferMode,
3059
- sectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = starterControlSectionInventory,
165
+ sectionInventory: readonly ToolcraftControlSectionInventoryEntry[] =
166
+ starterControlSectionInventory,
3060
167
  ): string[] {
3061
- const errors: string[] = [];
3062
- const controls = collectToolcraftVisibleControls(schema);
3063
- const controlAcceptance = new Map(
3064
- acceptance
3065
- .filter((entry) => entry.kind === "control")
3066
- .map((entry) => [entry.target, entry]),
3067
- );
3068
- const timelineMode = schema.panels.timeline?.enabled ? schema.panels.timeline.mode : null;
3069
- const layersEnabled = Boolean(schema.panels.layers);
3070
- const controlTargets = new Set(controls.map(({ control }) => control.target));
3071
- const animationIntent = transferMode.animationIntent;
3072
- const hasVideoExportAction = schemaHasVideoExportPanelAction(schema);
3073
- const animationControls = controls.filter(
3074
- (visibleControl) =>
3075
- visibleControl.control.type !== "panelActions" &&
3076
- animationIntentControlPattern.test(getAnimationIntentControlText(visibleControl)),
3077
- );
3078
- const commandTargets = new Set([
3079
- "canvas.center",
3080
- "canvas.setOffset",
3081
- "canvas.setSize",
3082
- "canvas.setViewport",
3083
- "canvas.zoomIn",
3084
- "canvas.zoomOut",
3085
- "controls.setValue",
3086
- "history.redo",
3087
- "history.undo",
3088
- ]);
3089
-
3090
- errors.push(...getToolcraftRuntimeSetupSectionErrors(schema));
3091
- errors.push(...getToolcraftControlOrderErrors(schema));
3092
- errors.push(...getToolcraftControlSectionGroupingErrors(schema, sectionInventory));
3093
- errors.push(...getToolcraftControlSectionInventoryErrors(schema, sectionInventory));
3094
-
3095
- const transferModeEvidenceText = getToolcraftTransferModeEvidenceText(transferMode);
3096
-
3097
- if (
3098
- videoReferenceEvidencePattern.test(transferModeEvidenceText) &&
3099
- !transferMode.videoReferenceStudy
3100
- ) {
3101
- errors.push(
3102
- "starterTransferMode cites a video reference, screen recording, GIF, extracted frames, or contact sheet; declare videoReferenceStudy with storyboard frames, frame-to-frame transition analysis, behavior decomposition, and acceptance mapping before implementation.",
3103
- );
3104
- }
3105
-
3106
- if (transferMode.videoReferenceStudy) {
3107
- errors.push(
3108
- ...validateToolcraftVideoReferenceStudy(
3109
- transferMode.videoReferenceStudy,
168
+ const input = isToolcraftAcceptanceValidationInput(inputOrSchema)
169
+ ? inputOrSchema
170
+ : {
3110
171
  acceptance,
3111
- ),
3112
- );
3113
- }
3114
-
3115
- for (const [sectionIndex, section] of (schema.panels.controls?.sections ?? []).entries()) {
3116
- const sectionLabel = getToolcraftSectionLabel(section.title, sectionIndex);
3117
-
3118
- for (const layoutGroup of section.layoutGroups ?? []) {
3119
- if (layoutGroup.layout !== "inline") {
3120
- continue;
3121
- }
3122
-
3123
- const rangeSliderIds = layoutGroup.controls.filter(
3124
- (controlId) => section.controls[controlId]?.type === "rangeSlider",
3125
- );
3126
-
3127
- if (rangeSliderIds.length > 0) {
3128
- errors.push(
3129
- `${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" includes rangeSlider ${rangeSliderIds.join(", ")}. RangeSlider is a full-width two-thumb control and must not share a row with another slider or range slider.`,
3130
- );
3131
- }
3132
-
3133
- const segmentedIds = layoutGroup.controls.filter(
3134
- (controlId) => section.controls[controlId]?.type === "segmented",
3135
- );
3136
-
3137
- if (segmentedIds.length > 0) {
3138
- errors.push(
3139
- `${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" includes segmented control ${segmentedIds.join(", ")}. Segmented is full-width and must not share a two-column or half-width row; use Select when a finite choice must fit beside another control.`,
3140
- );
3141
- }
3142
-
3143
- const switchEntries = layoutGroup.controls
3144
- .map((controlId) => [controlId, section.controls[controlId]] as const)
3145
- .filter(
3146
- (entry): entry is readonly [string, ToolcraftControlSchema] =>
3147
- Boolean(entry[1]) && entry[1].type === "switch",
3148
- );
3149
- const booleanEntries = layoutGroup.controls
3150
- .map((controlId) => [controlId, section.controls[controlId]] as const)
3151
- .filter(
3152
- (entry): entry is readonly [string, ToolcraftControlSchema] =>
3153
- Boolean(entry[1]) && isBooleanControl(entry[1]),
3154
- );
3155
- const parameterEntries = layoutGroup.controls
3156
- .map((controlId) => [controlId, section.controls[controlId]] as const)
3157
- .filter(
3158
- (entry): entry is readonly [string, ToolcraftControlSchema] =>
3159
- Boolean(entry[1]) && !isBooleanControl(entry[1]),
3160
- );
3161
-
3162
- if (switchEntries.length > 1) {
3163
- const unsafeSwitchLabels = switchEntries.filter(
3164
- ([controlId, control]) => !isInlineSwitchLabelSafe(controlId, control),
3165
- );
3166
-
3167
- if (unsafeSwitchLabels.length > 0) {
3168
- errors.push(
3169
- `${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" includes switch labels ${unsafeSwitchLabels.map(([controlId, control]) => `${controlId} "${getInlineSwitchLabelText(controlId, control)}"`).join(", ")} that are too long for a two-column toggle row. Switches share a row only when every visible label fits without truncation; shorten labels or stack them.`,
3170
- );
3171
- }
3172
- }
3173
-
3174
- if (booleanEntries.length === 1 && parameterEntries.length === 1) {
3175
- const unsafeBooleanLabels = booleanEntries.filter(
3176
- ([controlId, control]) => !isInlineSwitchLabelSafe(controlId, control),
3177
- );
3178
-
3179
- if (unsafeBooleanLabels.length > 0) {
3180
- errors.push(
3181
- `${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" includes toggle label ${unsafeBooleanLabels.map(([controlId, control]) => `${controlId} "${getInlineSwitchLabelText(controlId, control)}"`).join(", ")} that is too long for a compact toggle-plus-parameter row. Keep the toggle label short, such as "Include" inside Background, or stack the controls.`,
3182
- );
3183
- }
3184
-
3185
- const visibleParameterLabels = parameterEntries.filter(
3186
- ([, control]) => control.label !== false,
3187
- );
3188
-
3189
- if (visibleParameterLabels.length > 0) {
3190
- errors.push(
3191
- `${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" pairs a toggle with parameter labels ${visibleParameterLabels.map(([controlId, control]) => `${controlId} "${getControlLabelText(control)}"`).join(", ")}. In toggle-plus-parameter rows, the non-toggle parameter must use label false; if that label is needed, stack the controls instead.`,
3192
- );
3193
- }
3194
- }
3195
- }
3196
-
3197
- const sectionControls = Object.entries(section.controls).filter(([, control]) =>
3198
- isToolcraftProductSectionControl(control),
3199
- );
3200
-
3201
- for (let index = 0; index < sectionControls.length - 1; index += 1) {
3202
- const [firstControlId, firstControl] = sectionControls[index] ?? [];
3203
- const [secondControlId, secondControl] = sectionControls[index + 1] ?? [];
3204
-
3205
- if (
3206
- !firstControlId ||
3207
- !secondControlId ||
3208
- !firstControl ||
3209
- !secondControl ||
3210
- firstControl.visibleWhen ||
3211
- secondControl.visibleWhen ||
3212
- !isBooleanControl(firstControl) ||
3213
- !isBooleanControl(secondControl) ||
3214
- !isInlineSwitchLabelSafe(firstControlId, firstControl) ||
3215
- !isInlineSwitchLabelSafe(secondControlId, secondControl) ||
3216
- !controlsShareToolcraftTargetEntity(firstControl, secondControl) ||
3217
- sectionHasInlineLayoutGroupForPair(section, firstControlId, secondControlId)
3218
- ) {
3219
- continue;
3220
- }
3221
-
3222
- errors.push(
3223
- `${sectionLabel} has adjacent short toggle controls "${firstControlId}" and "${secondControlId}" for the same product entity "${getToolcraftLooseTargetPrefix(firstControl.target)}". Put them in a two-column inline layoutGroup so compact paired toggles share one row.`,
3224
- );
3225
- }
3226
- }
3227
-
3228
- if (schemaHasPngExportPanelAction(schema)) {
3229
- const backgroundSection = getSchemaControlsSectionByTitle(schema, "Background");
3230
- const backgroundSectionIndex = getSchemaControlsSectionIndexByTitle(schema, "Background");
3231
- const panelActionsSectionIndex = getFirstPanelActionsSectionIndex(schema);
3232
- const imageExportSectionIndex = getSchemaControlsSectionIndexByTitle(schema, "Image Export");
3233
- const videoExportSectionIndex = getSchemaControlsSectionIndexByTitle(schema, "Video Export");
3234
- const expectedOutputSettingsIndex =
3235
- imageExportSectionIndex >= 0 ? imageExportSectionIndex : videoExportSectionIndex;
3236
- const finalExportSettingsIndex = hasVideoExportAction
3237
- ? videoExportSectionIndex
3238
- : imageExportSectionIndex;
3239
- const includeBackgroundEntry = getSectionControlEntryByTarget(
3240
- backgroundSection,
3241
- "export.includeBackground",
3242
- );
3243
- const backgroundColorEntry = getOutputBackgroundColorEntry(backgroundSection);
3244
- const imageExportSection = getSchemaControlsSectionByTitle(schema, "Image Export");
3245
- const imageFormatEntry = getSectionControlEntryByTarget(
3246
- imageExportSection,
3247
- "export.image.format",
3248
- );
3249
- const imageResolutionEntry = getSectionControlEntryByTarget(
3250
- imageExportSection,
3251
- "export.image.resolution",
3252
- );
3253
- const imageFormatControl = imageFormatEntry?.[1];
3254
- const imageResolutionControl = imageResolutionEntry?.[1];
3255
- const imageFormatOptionValues =
3256
- imageFormatControl?.options?.map((option) => option.value.toLowerCase()) ?? [];
3257
- const imageResolutionOptionValues =
3258
- imageResolutionControl?.options?.map((option) => option.value.toLowerCase()) ?? [];
3259
-
3260
- if (!backgroundSection) {
3261
- errors.push(
3262
- 'Product apps with Export PNG must expose a separate controls section titled "Background" directly before the first export settings section.',
3263
- );
3264
- }
3265
-
3266
- if (
3267
- backgroundSectionIndex >= 0 &&
3268
- expectedOutputSettingsIndex >= 0 &&
3269
- backgroundSectionIndex !== expectedOutputSettingsIndex - 1
3270
- ) {
3271
- errors.push(
3272
- 'The "Background" controls section must sit directly before the first export settings section: Image Export when PNG export exists, otherwise Video Export.',
3273
- );
3274
- }
3275
-
3276
- if (
3277
- finalExportSettingsIndex >= 0 &&
3278
- panelActionsSectionIndex >= 0 &&
3279
- finalExportSettingsIndex !== panelActionsSectionIndex - 1
3280
- ) {
3281
- errors.push(
3282
- 'Export settings must sit directly above sticky footer actions: Image Export for still apps, or Video Export after Image Export for animated apps.',
3283
- );
3284
- }
3285
-
3286
- if (
3287
- hasVideoExportAction &&
3288
- imageExportSectionIndex >= 0 &&
3289
- videoExportSectionIndex >= 0 &&
3290
- imageExportSectionIndex !== videoExportSectionIndex - 1
3291
- ) {
3292
- errors.push(
3293
- 'Animated apps with both Export PNG and Export Video must place Image Export immediately before Video Export.',
3294
- );
3295
- }
3296
-
3297
- if (!schemaHasOutputBackgroundColorControl(controls)) {
3298
- errors.push(
3299
- "Product apps with Export PNG must expose a user-facing background color control such as appearance.background or scene.background. Preview, PNG export, and video export must read that runtime value instead of hardcoding the product background.",
3300
- );
3301
- }
3302
-
3303
- if (!backgroundColorEntry) {
3304
- errors.push(
3305
- 'The "Background" section must contain the renderer-owned background color control, such as appearance.background or scene.background.',
3306
- );
3307
- } else {
3308
- const [, backgroundColorControl] = backgroundColorEntry;
3309
-
3310
- if (backgroundColorControl.label !== false) {
3311
- errors.push(
3312
- 'The background color control inside the required "Background" section must use label false; the section title already supplies the visible context.',
3313
- );
3314
- }
3315
- }
3316
-
3317
- if (!schemaHasOutputBackgroundToggleControl(controls)) {
3318
- errors.push(
3319
- 'Product apps with Export PNG must expose export.includeBackground inside the required "Background" section as a Switch labeled "Include". PNG export must pass that runtime value to createToolcraftPngExportCanvas includeBackground; live preview must use shouldIncludeToolcraftPreviewBackground(state); video export keeps the background.',
3320
- );
3321
- }
3322
-
3323
- if (!includeBackgroundEntry) {
3324
- errors.push(
3325
- 'The "Background" section must contain export.includeBackground as the Include switch.',
3326
- );
3327
- } else {
3328
- const [, includeBackgroundControl] = includeBackgroundEntry;
3329
-
3330
- if (includeBackgroundControl.type !== "switch") {
3331
- errors.push('export.includeBackground must be a Switch control labeled "Include".');
3332
- }
3333
-
3334
- if (getControlLabelText(includeBackgroundControl) !== "Include") {
3335
- errors.push(
3336
- 'export.includeBackground must use the short visible label "Include"; the Background section title already supplies the rest of the context.',
3337
- );
3338
- }
3339
- }
3340
-
3341
- if (
3342
- !sectionHasEqualWidthOutputBackgroundRow(
3343
- backgroundSection,
3344
- includeBackgroundEntry?.[0],
3345
- backgroundColorEntry?.[0],
3346
- )
3347
- ) {
3348
- errors.push(
3349
- 'The "Background" section must render export.includeBackground and the background color in one two-column inline layoutGroup, with Include on the left and the unlabeled background color on the right.',
3350
- );
3351
- }
3352
-
3353
- if (!imageExportSection) {
3354
- errors.push(
3355
- 'Apps with Export PNG must expose image export settings in a separate controls section titled "Image Export" directly above sticky footer export actions or directly before "Video Export" when video export also exists.',
3356
- );
3357
- }
3358
-
3359
- if (!imageFormatControl) {
3360
- errors.push(
3361
- 'The separate "Image Export" section must include a format control with target "export.image.format".',
3362
- );
3363
- } else {
3364
- if (imageFormatControl.type !== "select") {
3365
- errors.push(
3366
- 'Image Export format must be a Select control so it matches the Video Export settings structure.',
3367
- );
3368
- }
3369
-
3370
- if (!imageFormatOptionValues.includes("png") || !imageFormatOptionValues.includes("jpg")) {
3371
- errors.push('Image Export format options must include "png" and "jpg".');
3372
- }
3373
-
3374
- if (imageFormatControl.defaultValue !== "png") {
3375
- errors.push('Image Export format must default to "png".');
3376
- }
3377
- }
3378
-
3379
- if (!imageResolutionControl) {
3380
- errors.push(
3381
- 'The separate "Image Export" section must include a resolution control with target "export.image.resolution".',
3382
- );
3383
- } else {
3384
- if (imageResolutionControl.type !== "select") {
3385
- errors.push(
3386
- 'Image Export resolution must be a Select control so it matches the Video Export settings structure.',
3387
- );
3388
- }
3389
-
3390
- if (
3391
- !imageResolutionOptionValues.includes("2k") ||
3392
- !imageResolutionOptionValues.includes("4k") ||
3393
- !imageResolutionOptionValues.includes("8k")
3394
- ) {
3395
- errors.push(
3396
- 'Image Export resolution options must include "2k", "4k", and "8k".',
3397
- );
3398
- }
3399
-
3400
- if (imageResolutionControl.defaultValue !== "4k") {
3401
- errors.push('Image Export resolution must default to "4k".');
3402
- }
3403
- }
3404
-
3405
- const imageFormatControlId = imageFormatEntry?.[0];
3406
- const imageResolutionControlId = imageResolutionEntry?.[0];
3407
- const imageExportHasInlinePair =
3408
- imageExportSection === undefined ||
3409
- imageFormatControlId === undefined ||
3410
- imageResolutionControlId === undefined
3411
- ? false
3412
- : sectionHasInlineLayoutGroupForPair(
3413
- imageExportSection,
3414
- imageFormatControlId,
3415
- imageResolutionControlId,
3416
- );
3417
-
3418
- if (!imageExportHasInlinePair) {
3419
- errors.push(
3420
- "Image Export format and resolution must render as one compact two-column inline row, matching Video Export settings.",
3421
- );
3422
- }
3423
- }
3424
-
3425
- if (hasVideoExportAction && !timelineMode) {
3426
- errors.push(
3427
- 'Apps with Export Video must enable the top Toolcraft timeline. Use panels.timeline mode "playback" for product animation transport, or mode "keyframes" when exported animation is driven by keyframes; autonomous no-timeline animation is only allowed when there is no video export.',
3428
- );
3429
- }
3430
-
3431
- if (animationControls.length > 0 && !timelineMode && animationIntent?.mode !== "autonomous") {
3432
- errors.push(
3433
- [
3434
- `Animation controls ${animationControls.map(({ control, controlId, sectionTitle }) => `"${sectionTitle ? `${sectionTitle} / ` : ""}${controlId}" (${control.target})`).join(", ")} exist while panels.timeline is omitted.`,
3435
- 'Use panels.timeline mode "playback" for product animation transport, mode "keyframes" for editable keyframes, or declare starterTransferMode.animationIntent mode "autonomous" with coverage proving there is no user-facing transport.',
3436
- ].join(" "),
3437
- );
3438
- }
3439
-
3440
- if (animationIntent?.mode === "autonomous") {
3441
- const declaredAutonomousCoverage = new Set(animationIntent.behaviorCoverage);
3442
- const missingAutonomousCoverage = requiredAutonomousAnimationCoverage.filter(
3443
- (coverage) => !declaredAutonomousCoverage.has(coverage),
3444
- );
3445
-
3446
- if (timelineMode) {
3447
- errors.push(
3448
- `starterTransferMode.animationIntent mode "autonomous" conflicts with panels.timeline mode "${timelineMode}". Use timeline-playback, timeline-keyframes, or remove the timeline.`,
3449
- );
3450
- }
3451
-
3452
- if (hasVideoExportAction) {
3453
- errors.push(
3454
- 'starterTransferMode.animationIntent mode "autonomous" conflicts with Export Video. Video export creates product-time behavior, so the renderer and export must use the top Toolcraft timeline duration, loop, and deterministic timestamps.',
3455
- );
3456
- }
3457
-
3458
- if (!animationIntent.reason.trim()) {
3459
- errors.push(
3460
- 'starterTransferMode.animationIntent mode "autonomous" must include a reason explaining why the animation is decorative/self-running and does not need top timeline transport.',
3461
- );
3462
- }
3463
-
3464
- if (missingAutonomousCoverage.length > 0) {
3465
- errors.push(
3466
- `starterTransferMode.animationIntent mode "autonomous" must include behaviorCoverage ${missingAutonomousCoverage.map((coverage) => `"${coverage}"`).join(", ")}.`,
3467
- );
3468
- }
3469
- }
3470
-
3471
- if (animationIntent?.mode === "timeline-playback" && timelineMode !== "playback") {
3472
- errors.push(
3473
- 'starterTransferMode.animationIntent mode "timeline-playback" requires panels.timeline mode "playback".',
3474
- );
3475
- }
3476
-
3477
- if (animationIntent?.mode === "timeline-keyframes" && timelineMode !== "keyframes") {
3478
- errors.push(
3479
- 'starterTransferMode.animationIntent mode "timeline-keyframes" requires panels.timeline mode "keyframes".',
3480
- );
3481
- }
3482
-
3483
- if (
3484
- transferMode.mode === "new-toolcraft-app" &&
3485
- timelineMode === "playback" &&
3486
- animationIntent?.mode !== "timeline-playback"
3487
- ) {
3488
- errors.push(
3489
- 'panels.timeline mode "playback" requires starterTransferMode.animationIntent mode "timeline-playback" with loopDuration provenance.',
3490
- );
3491
- }
3492
-
3493
- if (
3494
- transferMode.mode === "new-toolcraft-app" &&
3495
- timelineMode === "keyframes" &&
3496
- animationIntent?.mode !== "timeline-keyframes"
3497
- ) {
3498
- errors.push(
3499
- 'panels.timeline mode "keyframes" requires starterTransferMode.animationIntent mode "timeline-keyframes" with loopDuration provenance.',
3500
- );
3501
- }
3502
-
3503
- if (
3504
- timelineMode &&
3505
- (animationIntent?.mode === "timeline-playback" ||
3506
- animationIntent?.mode === "timeline-keyframes")
3507
- ) {
3508
- const timelineDefaultDurationSeconds = schema.panels.timeline?.defaultDurationSeconds;
3509
- const loopDuration = getTimelineLoopDurationIntent(animationIntent);
3510
-
3511
- if (!loopDuration) {
3512
- errors.push(
3513
- `starterTransferMode.animationIntent mode "${animationIntent.mode}" must declare loopDuration with source, seconds, and evidence. Do not let runtime/template fallback duration such as 8s stand in for product loop intent.`,
3514
- );
3515
- } else {
3516
- if (
3517
- !Number.isFinite(loopDuration.seconds) ||
3518
- loopDuration.seconds <= 0
3519
- ) {
3520
- errors.push(
3521
- `starterTransferMode.animationIntent.loopDuration.seconds must be a positive finite duration; received ${String(loopDuration.seconds)}.`,
3522
- );
3523
- }
3524
-
3525
- if (!isValidTimelineLoopDurationSource(loopDuration.source)) {
3526
- errors.push(
3527
- `starterTransferMode.animationIntent.loopDuration.source must be "reference", "user-request", or "product-derived"; received "${String(loopDuration.source)}". Runtime/template fallback is not a valid loop-duration source.`,
3528
- );
3529
- }
3530
-
3531
- if (!loopDuration.evidence.trim()) {
3532
- errors.push(
3533
- "starterTransferMode.animationIntent.loopDuration.evidence must explain where the initial loop duration came from, such as reference timing, an explicit user request, or a product-derived timing rule.",
3534
- );
3535
- }
3536
-
3537
- if (runtimeDefaultLoopDurationEvidencePattern.test(loopDuration.evidence)) {
3538
- errors.push(
3539
- "starterTransferMode.animationIntent.loopDuration.evidence must not cite the runtime/template fallback 8s default as the product loop source. Use reference timing, an explicit user request, or a product-derived timing rule.",
3540
- );
3541
- }
3542
-
3543
- if (typeof timelineDefaultDurationSeconds !== "number") {
3544
- errors.push(
3545
- "Timeline playback/keyframe apps must set panels.timeline.defaultDurationSeconds to the declared loopDuration.seconds so the initial UI duration is not the runtime fallback.",
3546
- );
3547
- } else if (
3548
- Number.isFinite(loopDuration.seconds) &&
3549
- Math.abs(timelineDefaultDurationSeconds - loopDuration.seconds) > 0.001
3550
- ) {
3551
- errors.push(
3552
- `panels.timeline.defaultDurationSeconds (${timelineDefaultDurationSeconds}) must match starterTransferMode.animationIntent.loopDuration.seconds (${loopDuration.seconds}).`,
3553
- );
3554
- }
3555
- }
3556
- }
3557
-
3558
- if (transferMode.mode === "reference-runtime-clone") {
3559
- const declaredReferenceCoverage = new Set(transferMode.behaviorCoverage);
3560
- const referenceTimeline = transferMode.referenceTimeline;
3561
- const referenceStudy = transferMode.referenceStudy;
3562
- const referenceFeatureInventory = transferMode.referenceFeatureInventory ?? [];
3563
- const acceptanceById = new Map(acceptance.map((entry) => [entry.id, entry]));
3564
- const referenceFeatureIds = new Set<string>();
3565
- const referenceFeatureAcceptanceIds = new Set<string>();
3566
- const referenceCoverageFromInventory = new Set<ToolcraftReferenceCoverage>();
3567
- const referenceTimelineCoverageFromInventory =
3568
- new Set<ToolcraftReferenceTimelineCoverage>();
3569
-
3570
- if (!schema.assembly.surfaces.canvas.enabled) {
3571
- errors.push(
3572
- "reference-runtime-clone must keep the Toolcraft canvas shell enabled; preserve the reference renderer inside ToolcraftApp canvasContent instead of replacing the app with the original UI.",
3573
- );
3574
- }
3575
-
3576
- if (!transferMode.referenceName.trim()) {
3577
- errors.push(
3578
- "reference-runtime-clone transferMode must name the reference app or artifact.",
3579
- );
3580
- }
3581
-
3582
- if (transferMode.sourceOfTruth !== "reference-runtime") {
3583
- errors.push(
3584
- 'reference-runtime-clone transferMode must set sourceOfTruth to "reference-runtime".',
3585
- );
3586
- }
3587
-
3588
- if (!referenceStudy) {
3589
- errors.push(
3590
- "reference-runtime-clone transferMode must declare referenceStudy proving the reference was inspected and, when runnable or reconstructable, run or restored locally before implementation.",
3591
- );
3592
- } else {
3593
- if (!referenceStudyStatusValues.has(referenceStudy.status)) {
3594
- errors.push(
3595
- 'referenceStudy.status must be "ran-original", "restored-local", or "source-inspection-only".',
3596
- );
3597
- }
3598
-
3599
- if (!referenceStudy.referenceLocation.trim()) {
3600
- errors.push(
3601
- "referenceStudy.referenceLocation must name the inspected reference folder, URL, artifact, or source location.",
3602
- );
3603
- }
3604
-
3605
- if (!referenceStudy.sourceEvidence.trim()) {
3606
- errors.push(
3607
- "referenceStudy.sourceEvidence must summarize the source/runtime files, routes, schemas, or assets inspected.",
3608
- );
3609
- }
3610
-
3611
- if (!referenceStudy.behaviorEvidence.trim()) {
3612
- errors.push(
3613
- "referenceStudy.behaviorEvidence must summarize the runtime/browser behavior checked from the original or restored reference.",
3614
- );
3615
- }
3616
-
3617
- if (!referenceStudy.reproductionSteps.trim()) {
3618
- errors.push(
3619
- "referenceStudy.reproductionSteps must record how the reference was run, restored in the Toolcraft environment, or why source-only inspection was used.",
3620
- );
3621
- }
3622
-
3623
- if (referenceStudy.status === "source-inspection-only") {
3624
- if (!referenceStudy.sourceOnlyReason?.trim()) {
3625
- errors.push(
3626
- 'referenceStudy.status "source-inspection-only" requires sourceOnlyReason explaining why the reference could not be run or restored locally.',
3627
- );
3628
- } else if (
3629
- !referenceStudySourceOnlyReasonPattern.test(referenceStudy.sourceOnlyReason)
3630
- ) {
3631
- errors.push(
3632
- 'referenceStudy.sourceOnlyReason must state the concrete blocker that made running or restoring the reference unavailable.',
3633
- );
3634
- }
3635
- }
3636
- }
3637
-
3638
- if (referenceFeatureInventory.length === 0) {
3639
- errors.push(
3640
- "reference-runtime-clone transferMode must declare referenceFeatureInventory with every user-visible and output-affecting behavior from the inspected reference, mapped to Toolcraft implementation and acceptance coverage.",
3641
- );
3642
- }
3643
-
3644
- for (const [index, feature] of referenceFeatureInventory.entries()) {
3645
- const featureLabel = feature.id.trim() || `#${index + 1}`;
3646
-
3647
- if (!feature.id.trim()) {
3648
- errors.push(
3649
- `referenceFeatureInventory item ${index + 1} must include a stable id.`,
3650
- );
3651
- } else if (referenceFeatureIds.has(feature.id)) {
3652
- errors.push(
3653
- `referenceFeatureInventory id "${feature.id}" is duplicated; each reference feature must be inventoried once.`,
3654
- );
3655
- } else {
3656
- referenceFeatureIds.add(feature.id);
3657
- }
3658
-
3659
- if (!feature.featureName.trim()) {
3660
- errors.push(
3661
- `referenceFeatureInventory "${featureLabel}" must include a short featureName.`,
3662
- );
3663
- }
3664
-
3665
- if (!feature.sourceEvidence.trim()) {
3666
- errors.push(
3667
- `referenceFeatureInventory "${featureLabel}" must include sourceEvidence from the inspected reference source, runtime, UI, or browser behavior.`,
3668
- );
3669
- }
3670
-
3671
- if (!feature.behaviorEvidence.trim()) {
3672
- errors.push(
3673
- `referenceFeatureInventory "${featureLabel}" must include behaviorEvidence from the original, restored, or source-only reference study proving this feature was observed before Toolcraft mapping.`,
3674
- );
3675
- }
3676
-
3677
- if (!feature.referenceBehavior.trim()) {
3678
- errors.push(
3679
- `referenceFeatureInventory "${featureLabel}" must describe the referenceBehavior to preserve.`,
3680
- );
3681
- }
3682
-
3683
- if (!feature.toolcraftMapping.trim()) {
3684
- errors.push(
3685
- `referenceFeatureInventory "${featureLabel}" must describe the Toolcraft mapping for the preserved behavior.`,
3686
- );
3687
- }
3688
-
3689
- if (!referenceFeatureStatusValues.has(feature.status)) {
3690
- errors.push(
3691
- `referenceFeatureInventory "${featureLabel}" status must be "ported", "toolcraft-native", or "intentionally-changed".`,
3692
- );
3693
- }
3694
-
3695
- if (feature.status === "intentionally-changed") {
3696
- if (!feature.userApprovedChangeReason?.trim()) {
3697
- errors.push(
3698
- `referenceFeatureInventory "${featureLabel}" status "intentionally-changed" requires userApprovedChangeReason.`,
3699
- );
3700
- } else if (
3701
- !explicitReferenceChangeReasonPattern.test(feature.userApprovedChangeReason)
3702
- ) {
3703
- errors.push(
3704
- `referenceFeatureInventory "${featureLabel}" userApprovedChangeReason must cite explicit user approval or redesign/change-request evidence.`,
3705
- );
3706
- }
3707
- }
3708
-
3709
- const acceptanceId = feature.acceptanceId.trim();
3710
-
3711
- if (!acceptanceId) {
3712
- errors.push(
3713
- `referenceFeatureInventory "${featureLabel}" must include acceptanceId for the test proving the mapped reference behavior.`,
3714
- );
3715
- continue;
3716
- }
3717
-
3718
- referenceFeatureAcceptanceIds.add(acceptanceId);
3719
-
3720
- const entry = acceptanceById.get(acceptanceId);
3721
-
3722
- if (!entry) {
3723
- errors.push(
3724
- `referenceFeatureInventory "${featureLabel}" points to missing acceptanceId "${acceptanceId}".`,
3725
- );
3726
- continue;
3727
- }
3728
-
3729
- if (!entry.referenceCoverage && !entry.referenceTimelineCoverage) {
3730
- errors.push(
3731
- `referenceFeatureInventory "${featureLabel}" acceptanceId "${acceptanceId}" must point to an acceptance entry with referenceCoverage or referenceTimelineCoverage.`,
3732
- );
3733
- }
3734
-
3735
- if (entry.referenceCoverage) {
3736
- referenceCoverageFromInventory.add(entry.referenceCoverage);
3737
- }
3738
-
3739
- if (entry.referenceTimelineCoverage) {
3740
- referenceTimelineCoverageFromInventory.add(entry.referenceTimelineCoverage);
3741
- }
3742
-
3743
- if (!entry.automated || !entry.automatedTestName.trim()) {
3744
- errors.push(
3745
- `${acceptanceId} must have automated coverage proving inventoried reference feature "${featureLabel}".`,
3746
- );
3747
- }
3748
-
3749
- if (!entry.browser || !entry.browserTestName.trim()) {
3750
- errors.push(
3751
- `${acceptanceId} must have browser coverage proving inventoried reference feature "${featureLabel}".`,
3752
- );
3753
- }
3754
-
3755
- if (!entry.expectedObservable.trim()) {
3756
- errors.push(
3757
- `${acceptanceId} must describe the observable result for inventoried reference feature "${featureLabel}".`,
3758
- );
3759
- }
3760
- }
3761
-
3762
- for (const coverage of requiredReferenceCloneCoverage) {
3763
- if (!declaredReferenceCoverage.has(coverage)) {
3764
- errors.push(
3765
- `reference-runtime-clone transferMode must include behaviorCoverage "${coverage}".`,
3766
- );
3767
- }
3768
- }
3769
-
3770
- for (const coverage of declaredReferenceCoverage) {
3771
- const entry = acceptance.find(
3772
- (acceptanceEntry) => acceptanceEntry.referenceCoverage === coverage,
3773
- );
3774
-
3775
- if (!entry) {
3776
- errors.push(
3777
- `reference-runtime-clone behaviorCoverage "${coverage}" is missing an acceptance entry with referenceCoverage "${coverage}".`,
3778
- );
3779
- continue;
3780
- }
3781
-
3782
- if (!entry.automated || !entry.automatedTestName.trim()) {
3783
- errors.push(
3784
- `${entry.id} must have automated coverage proving reference behavior "${coverage}".`,
3785
- );
3786
- }
3787
-
3788
- if (!entry.browser || !entry.browserTestName.trim()) {
3789
- errors.push(
3790
- `${entry.id} must have browser coverage proving reference behavior "${coverage}".`,
3791
- );
3792
- }
3793
-
3794
- if (!entry.expectedObservable.trim()) {
3795
- errors.push(
3796
- `${entry.id} must describe the observable reference behavior for "${coverage}".`,
3797
- );
3798
- }
3799
- }
3800
-
3801
- for (const coverage of declaredReferenceCoverage) {
3802
- if (!referenceCoverageFromInventory.has(coverage)) {
3803
- errors.push(
3804
- `reference-runtime-clone behaviorCoverage "${coverage}" must be represented in referenceFeatureInventory by an item whose acceptanceId points to that referenceCoverage.`,
3805
- );
3806
- }
3807
- }
3808
-
3809
- if (!referenceTimeline) {
3810
- errors.push(
3811
- 'reference-runtime-clone transferMode must declare referenceTimeline with mode "none", "toolcraft-playback", "toolcraft-keyframes", or "custom-reference-timeline".',
3812
- );
3813
- } else {
3814
- const declaredReferenceTimelineCoverage = new Set(referenceTimeline.behaviorCoverage);
3815
- const declaredReferenceTransportCoverage = [...declaredReferenceCoverage].filter(
3816
- (coverage) => referenceTransportCoverage.has(coverage),
3817
- );
3818
- const declaredToolcraftTimelineCoverage = [...declaredReferenceTimelineCoverage].filter(
3819
- (coverage) => toolcraftReferenceTimelineCoverage.has(coverage),
3820
- );
3821
-
3822
- for (const coverage of declaredReferenceTimelineCoverage) {
3823
- if (!referenceTimelineCoverageFromInventory.has(coverage)) {
3824
- errors.push(
3825
- `referenceTimeline behaviorCoverage "${coverage}" must be represented in referenceFeatureInventory by an item whose acceptanceId points to that referenceTimelineCoverage.`,
3826
- );
3827
- }
3828
- }
3829
-
3830
- if (referenceTimeline.mode === "none" && declaredReferenceTimelineCoverage.size > 0) {
3831
- errors.push(
3832
- 'referenceTimeline mode "none" must not declare reference timeline behaviorCoverage.',
3833
- );
3834
- }
3835
-
3836
- if (
3837
- referenceTimeline.mode === "none" &&
3838
- declaredReferenceTransportCoverage.length > 0
3839
- ) {
3840
- errors.push(
3841
- `reference-runtime-clone transport behaviorCoverage ${declaredReferenceTransportCoverage.map((coverage) => `"${coverage}"`).join(", ")} requires referenceTimeline mode "toolcraft-playback", "toolcraft-keyframes", or "custom-reference-timeline"; mode "none" is only for references with no user-facing transport behavior.`,
3842
- );
3843
- }
3844
-
3845
- if (
3846
- (referenceTimeline.mode === "toolcraft-playback" ||
3847
- referenceTimeline.mode === "toolcraft-keyframes") &&
3848
- declaredReferenceTimelineCoverage.size === 0
3849
- ) {
3850
- errors.push(
3851
- `referenceTimeline mode "${referenceTimeline.mode}" must list the concrete timeline transport behaviors in behaviorCoverage.`,
3852
- );
3853
- }
3854
-
3855
- if (referenceTimeline.mode === "toolcraft-playback" && timelineMode !== "playback") {
3856
- errors.push(
3857
- 'referenceTimeline mode "toolcraft-playback" requires panels.timeline mode "playback".',
3858
- );
3859
- }
3860
-
3861
- if (referenceTimeline.mode === "toolcraft-keyframes" && timelineMode !== "keyframes") {
3862
- errors.push(
3863
- 'referenceTimeline mode "toolcraft-keyframes" requires panels.timeline mode "keyframes".',
3864
- );
3865
- }
3866
-
3867
- if (
3868
- referenceTimeline.mode === "toolcraft-playback" ||
3869
- referenceTimeline.mode === "toolcraft-keyframes"
3870
- ) {
3871
- const timelineDefaultDurationSeconds = schema.panels.timeline?.defaultDurationSeconds;
3872
- const loopDuration = referenceTimeline.loopDuration;
3873
-
3874
- if (!loopDuration) {
3875
- errors.push(
3876
- `referenceTimeline mode "${referenceTimeline.mode}" must declare loopDuration with source, seconds, and evidence. Do not let runtime/template fallback duration such as 8s stand in for reference loop intent.`,
3877
- );
3878
- } else {
3879
- if (!Number.isFinite(loopDuration.seconds) || loopDuration.seconds <= 0) {
3880
- errors.push(
3881
- `referenceTimeline.loopDuration.seconds must be a positive finite duration; received ${String(loopDuration.seconds)}.`,
3882
- );
3883
- }
3884
-
3885
- if (!isValidTimelineLoopDurationSource(loopDuration.source)) {
3886
- errors.push(
3887
- `referenceTimeline.loopDuration.source must be "reference", "user-request", or "product-derived"; received "${String(loopDuration.source)}". Runtime/template fallback is not a valid loop-duration source.`,
3888
- );
3889
- }
3890
-
3891
- if (!loopDuration.evidence.trim()) {
3892
- errors.push(
3893
- "referenceTimeline.loopDuration.evidence must explain where the reference loop duration came from, such as measured reference timing, an explicit user request, or a product-derived timing rule.",
3894
- );
3895
- }
3896
-
3897
- if (runtimeDefaultLoopDurationEvidencePattern.test(loopDuration.evidence)) {
3898
- errors.push(
3899
- "referenceTimeline.loopDuration.evidence must not cite the runtime/template fallback 8s default as the reference loop source. Use measured reference timing, an explicit user request, or a product-derived timing rule.",
3900
- );
3901
- }
3902
-
3903
- if (typeof timelineDefaultDurationSeconds !== "number") {
3904
- errors.push(
3905
- "Toolcraft reference playback/keyframe timelines must set panels.timeline.defaultDurationSeconds to referenceTimeline.loopDuration.seconds so the initial UI duration is not the runtime fallback.",
3906
- );
3907
- } else if (
3908
- Number.isFinite(loopDuration.seconds) &&
3909
- Math.abs(timelineDefaultDurationSeconds - loopDuration.seconds) > 0.001
3910
- ) {
3911
- errors.push(
3912
- `panels.timeline.defaultDurationSeconds (${timelineDefaultDurationSeconds}) must match referenceTimeline.loopDuration.seconds (${loopDuration.seconds}).`,
3913
- );
3914
- }
3915
- }
3916
- }
3917
-
3918
- if (
3919
- referenceTimeline.mode === "toolcraft-playback" &&
3920
- declaredReferenceTimelineCoverage.has("keyframes")
3921
- ) {
3922
- errors.push(
3923
- 'referenceTimeline behaviorCoverage "keyframes" requires referenceTimeline mode "toolcraft-keyframes".',
3924
- );
3925
- }
3926
-
3927
- if (
3928
- referenceTimeline.mode === "toolcraft-keyframes" &&
3929
- !declaredReferenceTimelineCoverage.has("keyframes")
3930
- ) {
3931
- errors.push(
3932
- 'referenceTimeline mode "toolcraft-keyframes" must include behaviorCoverage "keyframes".',
3933
- );
3934
- }
3935
-
3936
- if (
3937
- (referenceTimeline.mode === "toolcraft-playback" ||
3938
- referenceTimeline.mode === "toolcraft-keyframes") &&
3939
- declaredToolcraftTimelineCoverage.length === 0
3940
- ) {
3941
- errors.push(
3942
- `referenceTimeline mode "${referenceTimeline.mode}" must include at least one Toolcraft timeline behavior such as "playback", "restart", "scrub", "duration", "loop", "time-progress", "export-at-time", or "keyframes".`,
3943
- );
3944
- }
3945
-
3946
- if (
3947
- referenceTimeline.mode === "custom-reference-timeline" &&
3948
- declaredReferenceTimelineCoverage.size === 0
3949
- ) {
3950
- errors.push(
3951
- 'referenceTimeline mode "custom-reference-timeline" must list every reference timeline behavior in behaviorCoverage.',
3952
- );
3953
- }
3954
-
3955
- for (const coverage of declaredReferenceTimelineCoverage) {
3956
- if (
3957
- customReferenceTimelineCoverage.has(coverage) &&
3958
- referenceTimeline.mode !== "custom-reference-timeline"
3959
- ) {
3960
- errors.push(
3961
- `referenceTimeline mode "${referenceTimeline.mode}" cannot preserve custom reference timeline behavior "${coverage}". Use mode "custom-reference-timeline" and browser-backed referenceTimelineCoverage instead.`,
3962
- );
3963
- }
3964
-
3965
- const entry = acceptance.find(
3966
- (acceptanceEntry) => acceptanceEntry.referenceTimelineCoverage === coverage,
3967
- );
3968
-
3969
- if (!entry) {
3970
- errors.push(
3971
- `referenceTimeline behaviorCoverage "${coverage}" is missing an acceptance entry with referenceTimelineCoverage "${coverage}".`,
3972
- );
3973
- continue;
3974
- }
3975
-
3976
- if (entry.kind !== "runtime") {
3977
- errors.push(
3978
- `${entry.id} must be a runtime acceptance entry proving reference timeline behavior "${coverage}".`,
3979
- );
3980
- }
3981
-
3982
- if (!entry.automated || !entry.automatedTestName.trim()) {
3983
- errors.push(
3984
- `${entry.id} must have automated coverage proving reference timeline behavior "${coverage}".`,
3985
- );
3986
- }
3987
-
3988
- if (!entry.browser || !entry.browserTestName.trim()) {
3989
- errors.push(
3990
- `${entry.id} must have browser coverage proving reference timeline behavior "${coverage}".`,
3991
- );
3992
- }
3993
-
3994
- if (!entry.expectedObservable.trim()) {
3995
- errors.push(
3996
- `${entry.id} must describe the observable reference timeline behavior for "${coverage}".`,
3997
- );
3998
- }
3999
- }
4000
- }
4001
-
4002
- for (const entry of acceptance) {
4003
- if (
4004
- (entry.referenceCoverage || entry.referenceTimelineCoverage) &&
4005
- !referenceFeatureAcceptanceIds.has(entry.id)
4006
- ) {
4007
- errors.push(
4008
- `${entry.id} declares reference coverage but is not mapped from referenceFeatureInventory. Every reference acceptance row must correspond to an inventoried reference feature.`,
4009
- );
4010
- }
4011
- }
4012
- } else {
4013
- for (const entry of acceptance) {
4014
- if (entry.referenceCoverage) {
4015
- errors.push(
4016
- `${entry.id} declares referenceCoverage "${entry.referenceCoverage}" but transferMode is not "reference-runtime-clone".`,
4017
- );
4018
- }
4019
-
4020
- if (entry.referenceTimelineCoverage) {
4021
- errors.push(
4022
- `${entry.id} declares referenceTimelineCoverage "${entry.referenceTimelineCoverage}" but transferMode is not "reference-runtime-clone".`,
4023
- );
4024
- }
4025
- }
4026
- }
4027
-
4028
- if (layersEnabled) {
4029
- for (const coverage of requiredLayerCoverage) {
4030
- const entry = acceptance.find(
4031
- (acceptanceEntry) =>
4032
- acceptanceEntry.kind === "runtime" && acceptanceEntry.layerCoverage === coverage,
4033
- );
4034
-
4035
- if (!entry) {
4036
- errors.push(
4037
- `panels.layers requires a runtime acceptance entry with layerCoverage "${coverage}" proving layer ${coverage} behavior.`,
4038
- );
4039
- continue;
4040
- }
4041
-
4042
- if (!entry.automated || !entry.automatedTestName.trim()) {
4043
- errors.push(`${entry.id} must have automated coverage proving layer ${coverage}.`);
4044
- }
4045
-
4046
- if (!entry.browser || !entry.browserTestName.trim()) {
4047
- errors.push(`${entry.id} must have browser coverage proving layer ${coverage}.`);
4048
- }
4049
-
4050
- if (!entry.expectedObservable.trim()) {
4051
- errors.push(
4052
- `${entry.id} must describe the observable layer behavior for "${coverage}".`,
4053
- );
4054
- }
4055
- }
4056
- } else {
4057
- for (const entry of acceptance) {
4058
- if (entry.layerCoverage) {
4059
- errors.push(
4060
- `${entry.id} declares layerCoverage "${entry.layerCoverage}" but panels.layers is not enabled.`,
4061
- );
4062
- }
4063
- }
4064
- }
4065
-
4066
- if (timelineMode) {
4067
- const playbackEntry = acceptance.find(
4068
- (entry) => entry.kind === "runtime" && entry.timelineCoverage === "playback",
4069
- );
4070
-
4071
- if (!playbackEntry) {
4072
- errors.push(
4073
- `panels.timeline mode "${timelineMode}" requires a runtime acceptance entry with timelineCoverage "playback" proving pause, scrub, duration/loop, and rendered-frame behavior.`,
4074
- );
4075
- } else if (
4076
- !hasTimelinePlaybackCoverage(
4077
- playbackEntry.timelinePlaybackCoverage,
4078
- requiredTimelinePlaybackCoverage,
4079
- )
4080
- ) {
4081
- errors.push(
4082
- `${playbackEntry.id} timelineCoverage "playback" must declare timelinePlaybackCoverage for pause-resume, scrub, duration, loop, and rendered-frame. Duration coverage must prove renderer progress maps 0..state.timeline.durationSeconds, not a local fixed animation duration.`,
4083
- );
4084
- } else {
4085
- const playbackEvidenceText = [
4086
- playbackEntry.automatedTestName,
4087
- playbackEntry.browserTestName,
4088
- playbackEntry.expectedObservable,
4089
- playbackEntry.userAction,
4090
- ].join(" ");
4091
-
4092
- if (
4093
- hasTimelinePlaybackCoveragePart(playbackEntry.timelinePlaybackCoverage, "duration") &&
4094
- (!/\bduration\b/i.test(playbackEvidenceText) ||
4095
- !/\b(edit|change|commit|enter|set)\w*\b/i.test(playbackEvidenceText))
4096
- ) {
4097
- errors.push(
4098
- `${playbackEntry.id} timelinePlaybackCoverage "duration" must describe editing/changing the timeline duration through the UI and proving the renderer follows state.timeline.durationSeconds.`,
4099
- );
4100
- }
4101
-
4102
- if (
4103
- hasTimelinePlaybackCoveragePart(playbackEntry.timelinePlaybackCoverage, "loop") &&
4104
- !hasSeamlessForwardLoopEvidence(playbackEvidenceText)
4105
- ) {
4106
- errors.push(
4107
- `${playbackEntry.id} timelinePlaybackCoverage "loop" must prove a seamless forward-only product loop: motion advances in one direction, avoids mirror/yoyo/ping-pong/reverse fallbacks, first and last frames stitch without a visible jump, and the same seam holds after changing timeline duration.`,
4108
- );
4109
- }
4110
- }
4111
- }
4112
-
4113
- if (schema.canvas.sizing.mode === "fixed-output") {
4114
- if (schemaHasPngExportPanelAction(schema) || schemaHasVideoExportPanelAction(schema)) {
4115
- errors.push(
4116
- 'Product/output apps with export actions must use canvas.sizing mode "editable-output" so Aspect ratio, Canvas width, and Canvas height are always available. Put reference, fixed-format, or user-requested dimensions in canvas.size as the initial value instead of hiding size controls with "fixed-output".',
4117
- );
4118
- }
4119
-
4120
- const fixedCanvasSizingEntry = acceptance.find(
4121
- (entry) =>
4122
- entry.kind === "runtime" &&
4123
- entry.canvasSizingCoverage === "fixed-output-size",
4124
- );
4125
-
4126
- if (!fixedCanvasSizingEntry) {
4127
- errors.push(
4128
- 'canvas.sizing mode "fixed-output" requires a runtime acceptance entry with canvasSizingCoverage "fixed-output-size" explaining why width and height are intentionally non-editable. Product/output apps must use "editable-output"; user-provided, reference, fixed-format, or base/default sizes belong in canvas.size as editable initial values.',
4129
- );
4130
- } else {
4131
- const evidenceText = getAcceptanceEvidenceText(fixedCanvasSizingEntry);
4132
-
4133
- if (!/(fixed|locked|non-editable|not user-editable|must not edit|reference-defined|product-defined)/i.test(evidenceText)) {
4134
- errors.push(
4135
- `${fixedCanvasSizingEntry.id} canvasSizingCoverage "fixed-output-size" must explain why the output dimensions are intentionally fixed for a non-product/internal fixture, not merely initialized from a default size.`,
4136
- );
4137
- }
4138
-
4139
- if (insufficientFixedCanvasSizingReasonPattern.test(evidenceText)) {
4140
- errors.push(
4141
- `${fixedCanvasSizingEntry.id} canvasSizingCoverage "fixed-output-size" cannot be justified by the reference or previous app lacking a size editor. Exportable product apps must use editable-output; put reference, product-defined, or fixed-format dimensions in canvas.size as initial values instead of hiding size controls.`,
4142
- );
4143
- }
4144
-
4145
- if (!fixedCanvasSizingEntry.automated || !fixedCanvasSizingEntry.automatedTestName.trim()) {
4146
- errors.push(
4147
- `${fixedCanvasSizingEntry.id} must have automated coverage proving fixed output dimensions.`,
4148
- );
4149
- }
4150
-
4151
- if (!fixedCanvasSizingEntry.browser || !fixedCanvasSizingEntry.browserTestName.trim()) {
4152
- errors.push(
4153
- `${fixedCanvasSizingEntry.id} must have browser coverage proving fixed output dimensions.`,
4154
- );
4155
- }
4156
- }
4157
- }
4158
-
4159
- if (
4160
- schema.canvas.enabled &&
4161
- schema.canvas.upload &&
4162
- schema.canvas.sizing.mode === "intrinsic-media"
4163
- ) {
4164
- const intrinsicCanvasSizingEntry = acceptance.find(
4165
- (entry) =>
4166
- entry.kind === "runtime" &&
4167
- entry.canvasSizingCoverage === "intrinsic-media-size",
4168
- );
4169
-
4170
- if (!intrinsicCanvasSizingEntry) {
4171
- errors.push(
4172
- 'canvas.sizing mode "intrinsic-media" with upload requires a runtime acceptance entry with canvasSizingCoverage "intrinsic-media-size" proving the app is a true media-viewer/source-native product where imported media natural dimensions intentionally own canvas.size. Uploaded background/source images inside product canvases must use "editable-output" and keep the current canvas size.',
4173
- );
4174
- } else {
4175
- const evidenceText = getAcceptanceEvidenceText(intrinsicCanvasSizingEntry);
4176
-
4177
- if (
4178
- !/(media[- ]viewer|source[- ]native|natural dimension|natural size|intrinsic media|intrinsic size|media owns canvas|canvas\.size)/i.test(
4179
- evidenceText,
4180
- )
4181
- ) {
4182
- errors.push(
4183
- `${intrinsicCanvasSizingEntry.id} canvasSizingCoverage "intrinsic-media-size" must explain why uploaded media natural dimensions intentionally own canvas.size instead of behaving as background/source material inside the current editable output canvas.`,
4184
- );
4185
- }
4186
-
4187
- if (!intrinsicCanvasSizingEntry.automated || !intrinsicCanvasSizingEntry.automatedTestName.trim()) {
4188
- errors.push(
4189
- `${intrinsicCanvasSizingEntry.id} must have automated coverage proving intrinsic media sizing.`,
4190
- );
4191
- }
4192
-
4193
- if (!intrinsicCanvasSizingEntry.browser || !intrinsicCanvasSizingEntry.browserTestName.trim()) {
4194
- errors.push(
4195
- `${intrinsicCanvasSizingEntry.id} must have browser coverage proving intrinsic media sizing.`,
4196
- );
4197
- }
4198
- }
4199
- }
4200
-
4201
- if (schema.persistence.storage === "localStorage") {
4202
- const persistenceEntry = acceptance.find(
4203
- (entry) =>
4204
- entry.kind === "runtime" &&
4205
- entry.persistenceCoverage === "reload",
4206
- );
4207
-
4208
- if (!persistenceEntry) {
4209
- errors.push(
4210
- 'persistence.storage "localStorage" requires a runtime acceptance entry with persistenceCoverage "reload" proving user-edited persisted state restores after a real browser reload. Settings import/export is not a substitute for persistence.',
4211
- );
4212
- } else {
4213
- const evidenceText = getAcceptanceEvidenceText(persistenceEntry);
4214
-
4215
- if (!persistenceEntry.automated || !persistenceEntry.automatedTestName.trim()) {
4216
- errors.push(
4217
- `${persistenceEntry.id} must have automated coverage proving persistence reload behavior.`,
4218
- );
4219
- }
4220
-
4221
- if (!persistenceEntry.browser || !persistenceEntry.browserTestName.trim()) {
4222
- errors.push(
4223
- `${persistenceEntry.id} must have browser coverage proving persistence reload behavior.`,
4224
- );
4225
- }
4226
-
4227
- if (!persistenceEntry.expectedObservable.trim()) {
4228
- errors.push(
4229
- `${persistenceEntry.id} must describe the persisted state observable after reload.`,
4230
- );
4231
- }
4232
-
4233
- if (!/\b(reload|refresh|reopen|page\.reload)\b/i.test(evidenceText)) {
4234
- errors.push(
4235
- `${persistenceEntry.id} persistenceCoverage "reload" must describe changing a user-facing setting, reloading the browser page, and observing the restored value/output.`,
4236
- );
4237
- }
4238
- }
4239
- }
4240
-
4241
- if (timelineMode === "keyframes") {
4242
- const hasKeyframesCoverage = acceptance.some(
4243
- (entry) => entry.kind === "runtime" && entry.timelineCoverage === "keyframes",
4244
- );
4245
-
4246
- if (!hasKeyframesCoverage) {
4247
- errors.push(
4248
- 'panels.timeline mode "keyframes" requires a runtime acceptance entry with timelineCoverage "keyframes" proving expanded rows, diamonds, keyframe mutation, and renderer evaluation.',
4249
- );
4250
- }
4251
- }
4252
-
4253
- for (const { control, controlId, sectionTitle } of controls) {
4254
- const label = `${sectionTitle ? `${sectionTitle} / ` : ""}${controlId} (${control.target})`;
4255
- const entry = controlAcceptance.get(control.target);
4256
- const keyframeCapability = getToolcraftControlKeyframeCapability(control);
4257
- const isCustomControl = isCustomToolcraftControl(control);
4258
- const isSelectedLayerTarget = control.target.startsWith("selectedLayer.");
4259
- const toggleLabelError = getToggleControlLabelError(control, sectionTitle);
4260
- const singleActionsLabelError = getSingleActionsControlLabelError(control);
4261
- const codeTextareaError = getCodeTextareaControlError({
4262
- control,
4263
- controlId,
4264
- label,
4265
- });
4266
-
4267
- if (toggleLabelError) {
4268
- errors.push(`${label} ${toggleLabelError}`);
4269
- }
4270
-
4271
- if (singleActionsLabelError) {
4272
- errors.push(`${label} ${singleActionsLabelError}`);
4273
- }
4274
-
4275
- if (codeTextareaError) {
4276
- errors.push(codeTextareaError);
4277
- }
4278
-
4279
- if (
4280
- control.type === "rangeSlider" &&
4281
- Array.isArray(control.defaultValue) &&
4282
- typeof control.defaultValue[0] === "number" &&
4283
- typeof control.defaultValue[1] === "number" &&
4284
- control.defaultValue[0] === control.defaultValue[1]
4285
- ) {
4286
- errors.push(
4287
- `${label} rangeSlider defaultValue must start with different lower and upper values so the two-thumb control does not collapse into a single-value slider.`,
4288
- );
4289
- }
4290
-
4291
- if (
4292
- control.type !== "panelActions" &&
4293
- timelineTransportControlPattern.test(getTimelineTransportControlText(controlId, control))
4294
- ) {
4295
- errors.push(
4296
- `${label} looks like an app-wide timeline transport control. Play, Pause, Animate, Resume, and Restart animation belong to the top timeline; keep right-panel controls for renderer parameters, generation/apply actions, and output delivery.`,
4297
- );
4298
- }
4299
-
4300
- if (shouldUseSingleCurveVariant(controlId, control)) {
4301
- errors.push(
4302
- `${label} is a semantic single curve and must set variant: "single"; RGB/R/G/B curve tabs are reserved for color-correction or channel-specific curves.`,
4303
- );
4304
- }
4305
-
4306
- if (control.keyframeable === true && !keyframeCapability.capable) {
4307
- errors.push(
4308
- `${label} sets keyframeable true, but this control type or runtime-owned target cannot create timeline keyframes.`,
4309
- );
4310
- }
4311
-
4312
- if (
4313
- timelineMode === "keyframes" &&
4314
- keyframeCapability.capable &&
4315
- control.keyframeable === false
4316
- ) {
4317
- errors.push(
4318
- `${label} is keyframe-capable by Toolcraft control type; remove keyframeable: false and provide keyframe evaluator coverage instead of hiding the diamond.`,
4319
- );
4320
- }
4321
-
4322
- if (isSelectedLayerTarget && !layersEnabled) {
4323
- errors.push(
4324
- `${label} uses reserved selectedLayer.* target without panels.layers enabled. Use an app-specific target for single-layer apps or enable layers with layerCoverage.`,
4325
- );
4326
- }
4327
-
4328
- if (control.visibleWhen) {
4329
- errors.push(
4330
- ...getConditionValidationErrors({
4331
- condition: control.visibleWhen,
4332
- conditionName: "visibleWhen",
4333
- controlTargets,
4334
- label,
4335
- }),
4336
- );
4337
- }
4338
-
4339
- if (control.disabledWhen) {
4340
- errors.push(
4341
- ...getConditionValidationErrors({
4342
- condition: control.disabledWhen,
4343
- conditionName: "disabledWhen",
4344
- controlTargets,
4345
- label,
4346
- }),
4347
- );
4348
- }
4349
-
4350
- if (control.disabled === true) {
4351
- errors.push(
4352
- `${label} sets disabled: true. Generated product panels should show only controls usable in the current state; hide unavailable product controls with visibleWhen instead of rendering disabled controls.`,
4353
- );
4354
- }
4355
-
4356
- if (control.disabledWhen) {
4357
- errors.push(
4358
- `${label} uses disabledWhen. Generated product panels should show only controls usable in the current state; hide unavailable product controls with visibleWhen instead of rendering disabled controls.`,
4359
- );
4360
- }
4361
-
4362
- if (!entry) {
4363
- errors.push(`${label} is missing an acceptance entry.`);
4364
- continue;
4365
- }
4366
-
4367
- if (!entry.automated) {
4368
- errors.push(`${label} must have automated acceptance coverage.`);
4369
- }
4370
-
4371
- if (!entry.browser) {
4372
- errors.push(`${label} must have browser acceptance coverage.`);
4373
- }
4374
-
4375
- if (entry.browser && !entry.browserTestName.trim()) {
4376
- errors.push(`${label} must point to a browser test name.`);
4377
- }
4378
-
4379
- if (!entry.expectedObservable.trim()) {
4380
- errors.push(`${label} must describe a product-level observable.`);
4381
- }
4382
-
4383
- if (!entry.automatedTestName.trim()) {
4384
- errors.push(`${label} must point to an automated test name.`);
4385
- }
4386
-
4387
- if (entry.componentType !== control.type) {
4388
- errors.push(
4389
- `${label} acceptance componentType must be "${control.type}", received "${entry.componentType}".`,
4390
- );
4391
- }
4392
-
4393
- if (control.type === "fileDrop") {
4394
- const hasDefaultMediaAssets = schema.media.defaultAssets.some(
4395
- (asset) => asset.sourceTarget === control.target,
4396
- );
4397
-
4398
- errors.push(
4399
- ...getFileDropLifecycleCoverageErrors(
4400
- label,
4401
- control,
4402
- entry,
4403
- hasDefaultMediaAssets,
4404
- ),
4405
- );
4406
- }
4407
-
4408
- if (
4409
- isCustomControl &&
4410
- !hasCustomControlCoverage(
4411
- entry.customControlCoverage,
4412
- requiredCustomControlCoverage,
4413
- )
4414
- ) {
4415
- errors.push(
4416
- `${label} is a custom control and must declare customControlCoverage for: ${requiredCustomControlCoverage.join(", ")}.`,
4417
- );
4418
- }
4419
-
4420
- if (isCustomControl) {
4421
- errors.push(...getBuiltInFitCheckErrors(label, entry, control));
4422
- }
4423
-
4424
- if (
4425
- control.visibleWhen &&
4426
- !/\b(visible|shown|show|appears|hidden|hide|hides|not visible|disappear|unavailable)\b/i.test(
4427
- getAcceptanceEvidenceText(entry),
4428
- )
4429
- ) {
4430
- errors.push(
4431
- `${label} uses visibleWhen and acceptance must prove the control becomes visible and hidden/unavailable when ${control.visibleWhen.target} reaches the gating values.`,
4432
- );
4433
- }
4434
-
4435
- if (
4436
- schemaHasPngExportPanelAction(schema) &&
4437
- isOutputBackgroundToggleControl({ control, controlId, sectionTitle })
4438
- ) {
4439
- const evidenceText = getAcceptanceEvidenceText(entry);
4440
- const provesPngTransparency =
4441
- /\b(png|image)\b/i.test(evidenceText) &&
4442
- /\b(transparent|transparency|alpha)\b/i.test(evidenceText);
4443
- const provesPreviewTransparency =
4444
- /\b(preview|canvas)\b/i.test(evidenceText) &&
4445
- /\b(transparent|transparency|alpha|hide|hides|hidden|without background|no background|background off)\b/i.test(
4446
- evidenceText,
4447
- );
4448
- const provesVideoBackground =
4449
- /\bvideo\b/i.test(evidenceText) &&
4450
- /\b(keep|keeps|preserve|preserves|stay|stays|remain|remains|still|background)\b/i.test(
4451
- evidenceText,
4452
- );
4453
-
4454
- if (!provesPngTransparency || !provesPreviewTransparency || !provesVideoBackground) {
4455
- errors.push(
4456
- `${label} controls background inclusion and acceptance must prove disabling it makes PNG output transparent, hides the live preview product background, and keeps video output with the product background.`,
4457
- );
4458
- }
4459
- }
4460
-
4461
- const requiredControlParts =
4462
- getRequiredToolcraftControlPartCoverage(control);
4463
-
4464
- if (!hasControlPartCoverage(entry.controlPartCoverage, requiredControlParts)) {
4465
- errors.push(
4466
- `${label} must declare controlPartCoverage for every semantic value part: ${requiredControlParts.join(", ")}.`,
4467
- );
4468
- }
4469
-
4470
- if (timelineMode === "keyframes" && keyframeCapability.capable) {
4471
- if (entry.timelineCoverage !== "keyframes") {
4472
- errors.push(
4473
- `${label} is keyframe-capable by Toolcraft control type and must have acceptance timelineCoverage "keyframes" proving its diamond creates/updates a keyframe row and changes evaluated output.`,
4474
- );
4475
- }
4476
- }
4477
-
4478
- if (
4479
- isSelectedLayerTarget &&
4480
- layersEnabled &&
4481
- entry.layerCoverage !== "selected-layer-controls"
4482
- ) {
4483
- errors.push(
4484
- `${label} targets selectedLayer.* and must have acceptance layerCoverage "selected-layer-controls" proving the control edits the currently selected layer output.`,
4485
- );
4486
- }
4487
-
4488
- if (isSliderLikeControl(control)) {
4489
- const expectedMarkerCount = getStepMarkerCount(control);
4490
-
4491
- if (control.unit === "x") {
4492
- errors.push(
4493
- `${label} uses unit "x", but Toolcraft slider values do not use x suffixes. Omit unit for scale, multiplier, intensity, opacity, strength, depth, and shader amount values unless a real measurement unit applies.`,
4494
- );
4495
- }
4496
-
4497
- if (
4498
- control.variant === "discrete" &&
4499
- expectedMarkerCount &&
4500
- control.markerCount !== expectedMarkerCount
4501
- ) {
4502
- errors.push(
4503
- `${label} discrete slider must render one marker per step; expected markerCount ${expectedMarkerCount}, received ${String(control.markerCount)}.`,
4504
- );
4505
- }
4506
-
4507
- errors.push(
4508
- ...getSliderVariantClassificationErrors({
4509
- control,
4510
- controlId,
4511
- label,
4512
- }),
4513
- );
4514
- }
4515
-
4516
- if (control.type === "imagePicker") {
4517
- const itemValues = getControlOptionValues(control);
4518
-
4519
- if (!hasCoverageForValues(entry.optionCoverage, itemValues)) {
4520
- errors.push(
4521
- `${label} must cover every visible ImagePicker item: ${itemValues.join(", ")}.`,
4522
- );
4523
- }
4524
- }
4525
-
4526
- if (control.type === "select" || control.type === "segmented") {
4527
- const optionValues = getControlOptionValues(control);
4528
-
4529
- if (optionValues.length > 1 && !hasCoverageForValues(entry.optionCoverage, optionValues)) {
4530
- errors.push(`${label} must cover every visible option: ${optionValues.join(", ")}.`);
4531
- }
4532
-
4533
- const segmentedLayoutError = getSegmentedControlLayoutError(control);
4534
-
4535
- if (segmentedLayoutError) {
4536
- errors.push(`${label} ${segmentedLayoutError}`);
4537
- }
4538
- }
4539
-
4540
- if (control.type === "panelActions") {
4541
- const actionValues = control.actions?.map(getActionValue) ?? [];
4542
- const resetActionValues =
4543
- control.actions?.filter(isResetPanelAction).map(getActionValue) ?? [];
4544
-
4545
- if (resetActionValues.length > 0) {
4546
- errors.push(
4547
- `${label} must not include Reset footer actions (${resetActionValues.join(", ")}). The controls panel header owns Reset controls; sticky panelActions are only for product delivery actions such as Export, Copy, Generate, Apply, or Download.`,
4548
- );
4549
- }
4550
-
4551
- if (!hasCoverageForValues(entry.actionCoverage, actionValues)) {
4552
- errors.push(`${label} must cover every footer action: ${actionValues.join(", ")}.`);
4553
- }
4554
- }
4555
- }
4556
-
4557
- for (const entry of acceptance) {
4558
- if (
4559
- entry.kind === "control" &&
4560
- entry.target &&
4561
- !controlTargets.has(entry.target) &&
4562
- !isRuntimeSetupControlTarget(entry.target)
4563
- ) {
4564
- errors.push(`${entry.id} points to missing control target ${entry.target}.`);
4565
- }
4566
-
4567
- if (entry.kind !== "canvas-handle") {
4568
- continue;
4569
- }
4570
-
4571
- if (!entry.canvasHandle) {
4572
- errors.push(`${entry.id} canvas handle is missing canvasHandle metadata.`);
4573
- continue;
4574
- }
4575
-
4576
- if (!entry.canvasHandle.testId.trim()) {
4577
- errors.push(`${entry.id} canvas handle must provide a stable testId.`);
4578
- }
4579
-
4580
- if (!entry.canvasHandle.writesTarget.trim()) {
4581
- errors.push(`${entry.id} canvas handle must name the runtime target it writes.`);
4582
- }
4583
-
4584
- if (
4585
- entry.canvasHandle.writesTarget &&
4586
- !controlTargets.has(entry.canvasHandle.writesTarget) &&
4587
- !commandTargets.has(entry.canvasHandle.writesTarget)
4588
- ) {
4589
- errors.push(
4590
- `${entry.id} canvas handle writesTarget ${entry.canvasHandle.writesTarget} does not match a schema target or supported editor command.`,
4591
- );
4592
- }
4593
-
4594
- if (!entry.canvasHandle.outputObservable.trim()) {
4595
- errors.push(`${entry.id} canvas handle must describe the product output change.`);
4596
- }
4597
-
4598
- if (!entry.canvasHandle.exportCleanTestName.trim()) {
4599
- errors.push(`${entry.id} canvas handle must point to an export-clean test.`);
4600
- }
4601
-
4602
- if (!entry.browser || !entry.browserTestName.trim()) {
4603
- errors.push(`${entry.id} canvas handle must have browser drag coverage.`);
4604
- }
172
+ schema: inputOrSchema,
173
+ sectionInventory,
174
+ transferMode,
175
+ };
4605
176
 
4606
- if (!entry.automated || !entry.automatedTestName.trim()) {
4607
- errors.push(`${entry.id} canvas handle must have automated output coverage.`);
4608
- }
4609
- }
177
+ return validateToolcraftAcceptanceInput(input);
178
+ }
4610
179
 
4611
- return errors;
180
+ export function validateProductAcceptanceCoverage(): string[] {
181
+ return validateToolcraftAcceptanceCoverage({
182
+ acceptance: starterAcceptance,
183
+ schema: starterSchema,
184
+ sectionInventory: starterControlSectionInventory,
185
+ transferMode: starterTransferMode,
186
+ });
4612
187
  }