@pixel-point/toolcraft 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/package.json +1 -1
  2. package/src/generate-test-doc-assertions.mjs +58 -68
  3. package/src/generate-test-doc-assertions.test.mjs +43 -0
  4. package/src/generate-test-doc-policy-assertions.mjs +92 -0
  5. package/src/generate-test-file-assertions.mjs +26 -75
  6. package/src/generate-test-product-iteration.test.mjs +17 -0
  7. package/src/generate-test-runtime-boundary-assertions.mjs +158 -0
  8. package/src/generate-test-standalone-assertions.mjs +10 -0
  9. package/templates/runtime/contracts/component-contracts.choices.test.ts +47 -3
  10. package/templates/runtime/contracts/component-contracts.choices.ts +48 -3
  11. package/templates/runtime/contracts/component-contracts.inputs.test.ts +6 -6
  12. package/templates/runtime/contracts/component-contracts.inputs.ts +6 -6
  13. package/templates/runtime/contracts/component-contracts.media-custom.test.ts +8 -2
  14. package/templates/runtime/contracts/component-contracts.media-custom.ts +8 -2
  15. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +4 -4
  16. package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +7 -1
  17. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +3 -0
  18. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +3 -0
  19. package/templates/runtime/contracts/component-contracts.runtime.ts +12 -8
  20. package/templates/runtime/contracts/component-contracts.test.ts +2 -0
  21. package/templates/runtime/contracts/decision-contracts.test.ts +18 -0
  22. package/templates/runtime/contracts/decision-contracts.ts +10 -10
  23. package/templates/runtime/export/export-background.ts +0 -19
  24. package/templates/runtime/export/export.test.ts +45 -42
  25. package/templates/runtime/export/index.ts +44 -16
  26. package/templates/runtime/index.ts +2 -0
  27. package/templates/runtime/model-import/model-import-limits.ts +15 -4
  28. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +11 -1
  29. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +29 -0
  30. package/templates/runtime/react/app-shell/toolcraft-app.tsx +18 -8
  31. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +76 -0
  32. package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +13 -1
  33. package/templates/runtime/react/canvas/canvas-shell.tsx +16 -1
  34. package/templates/runtime/react/canvas/product-scene-surface.test.tsx +195 -0
  35. package/templates/runtime/react/canvas/product-scene-surface.tsx +155 -0
  36. package/templates/runtime/react/canvas/runtime-scene-export.ts +1 -1
  37. package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +61 -0
  38. package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +89 -0
  39. package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +40 -19
  40. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +1 -1
  41. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-collection.test.tsx +62 -0
  42. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-color-bank.test.tsx +67 -0
  43. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +2 -4
  44. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +10 -4
  45. package/templates/runtime/react/controls-panel/conditions/control-conditions.ts +32 -119
  46. package/templates/runtime/react/controls-panel/layout/controls-panel-control-group.tsx +5 -2
  47. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +10 -8
  48. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +3 -3
  49. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.test.tsx +63 -0
  50. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.tsx +138 -0
  51. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-items.tsx +104 -0
  52. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +115 -236
  53. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.test.tsx +95 -0
  54. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.tsx +82 -0
  55. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.test.tsx +73 -1
  56. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +50 -1
  57. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +1 -1
  58. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +2 -1
  59. package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +38 -6
  60. package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +1 -0
  61. package/templates/runtime/react/index.ts +4 -0
  62. package/templates/runtime/react/model-rendering/model-export.ts +1 -1
  63. package/templates/runtime/react/orientation-gizmo/orientation-gizmo-selection.ts +5 -5
  64. package/templates/runtime/react/orientation-gizmo/use-toolcraft-orientation-control-selection.ts +2 -2
  65. package/templates/runtime/scene/scene-bounds.test.ts +23 -0
  66. package/templates/runtime/scene/scene-bounds.ts +31 -17
  67. package/templates/runtime/schema/app-capabilities.test.ts +4 -0
  68. package/templates/runtime/schema/collection-item-controls.ts +29 -0
  69. package/templates/runtime/schema/control-applicability.test.ts +127 -0
  70. package/templates/runtime/schema/control-applicability.ts +433 -0
  71. package/templates/runtime/schema/control-schema-normalization.ts +177 -11
  72. package/templates/runtime/schema/controls-panel-actions.ts +2 -2
  73. package/templates/runtime/schema/controls-panel-section-layout.ts +14 -9
  74. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +59 -0
  75. package/templates/runtime/schema/define-toolcraft.collection-actions-item-controls.test.ts +127 -0
  76. package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +10 -4
  77. package/templates/runtime/schema/define-toolcraft.file-drop-item-controls.test.ts +98 -0
  78. package/templates/runtime/schema/define-toolcraft.file-drop-normalization.test.ts +27 -0
  79. package/templates/runtime/schema/define-toolcraft.segmented-control-fit.test.ts +20 -2
  80. package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +16 -3
  81. package/templates/runtime/schema/define-toolcraft.source-collection.test.ts +79 -0
  82. package/templates/runtime/schema/runtime-setup-section.ts +24 -5
  83. package/templates/runtime/schema/types.ts +68 -13
  84. package/templates/starter/AGENTS.md +12 -10
  85. package/templates/starter/docs/toolcraft/acceptance-testing.md +17 -31
  86. package/templates/starter/docs/toolcraft/agent-worklog.md +15 -0
  87. package/templates/starter/docs/toolcraft/assembly-workflow.md +10 -3
  88. package/templates/starter/docs/toolcraft/component-rules.md +8 -6
  89. package/templates/starter/docs/toolcraft/core/control-selection.md +9 -3
  90. package/templates/starter/docs/toolcraft/core/layout.md +7 -7
  91. package/templates/starter/docs/toolcraft/core/media-upload.md +2 -0
  92. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +13 -2
  93. package/templates/starter/docs/toolcraft/core/setup-export.md +33 -7
  94. package/templates/starter/docs/toolcraft/custom-controls.md +6 -4
  95. package/templates/starter/docs/toolcraft/decision-contract.md +3 -1
  96. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  97. package/templates/starter/docs/toolcraft/schema-reference.md +21 -19
  98. package/templates/starter/e2e/app-browser-control-applicability-requirements.spec.ts +163 -0
  99. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +7 -2
  100. package/templates/starter/e2e/app-browser-orientation-evidence.spec.ts +174 -21
  101. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +1 -33
  102. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +61 -15
  103. package/templates/starter/e2e/{browser-conditional-output-evidence-helpers.ts → browser-background-output-evidence.ts} +0 -68
  104. package/templates/starter/e2e/browser-control-applicability-evidence.ts +114 -0
  105. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +20 -11
  106. package/templates/starter/e2e/browser-orientation-gizmo-evidence-helpers.ts +4 -0
  107. package/templates/starter/e2e/browser-orientation-gizmo-live-preconditions.ts +86 -0
  108. package/templates/starter/e2e/browser-runtime-applicability-requirements.ts +84 -0
  109. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +9 -2
  110. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +29 -26
  111. package/templates/starter/e2e/browser-semantic-evidence-test-helpers.ts +25 -0
  112. package/templates/starter/scripts/toolcraft-contract-manifest.json +1 -0
  113. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +85 -115
  114. package/templates/starter/scripts/toolcraft-product-boundary-module-policy.mjs +101 -0
  115. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +0 -72
  116. package/templates/starter/scripts/toolcraft-product-control-bindings.mjs +182 -0
  117. package/templates/starter/scripts/toolcraft-product-control-boundary.mjs +146 -0
  118. package/templates/starter/scripts/toolcraft-product-control-boundary.test.mjs +186 -0
  119. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +6 -17
  120. package/templates/starter/scripts/toolcraft-product-export-boundary.test.mjs +196 -0
  121. package/templates/starter/scripts/toolcraft-typescript-source-evidence-structure.test.mjs +20 -0
  122. package/templates/starter/scripts/toolcraft-typescript-source-evidence.mjs +1 -0
  123. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +2 -0
  124. package/templates/starter/src/app/acceptance/artifact-export-intent.ts +130 -0
  125. package/templates/starter/src/app/acceptance/conditions.ts +1 -1
  126. package/templates/starter/src/app/acceptance/control-acceptance-context.ts +2 -2
  127. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +6 -20
  128. package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +24 -5
  129. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +2 -13
  130. package/templates/starter/src/app/acceptance/control-applicability-cases.test.ts +321 -0
  131. package/templates/starter/src/app/acceptance/control-applicability-cases.ts +290 -0
  132. package/templates/starter/src/app/acceptance/control-applicability.ts +332 -0
  133. package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +20 -9
  134. package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +4 -6
  135. package/templates/starter/src/app/acceptance/control-layout-model.ts +4 -4
  136. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +3 -10
  137. package/templates/starter/src/app/acceptance/control-parts.ts +8 -0
  138. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.test.ts +215 -0
  139. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.ts +132 -0
  140. package/templates/starter/src/app/acceptance/control-section-inventory.test.ts +87 -0
  141. package/templates/starter/src/app/acceptance/control-section-inventory.ts +74 -44
  142. package/templates/starter/src/app/acceptance/custom-controls.ts +11 -3
  143. package/templates/starter/src/app/acceptance/inline-layout.ts +64 -9
  144. package/templates/starter/src/app/acceptance/media-upload.ts +5 -1
  145. package/templates/starter/src/app/acceptance/orientation-gizmo.ts +18 -11
  146. package/templates/starter/src/app/acceptance/output-background-rules.ts +2 -2
  147. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +18 -10
  148. package/templates/starter/src/app/acceptance/output-export-model.ts +54 -11
  149. package/templates/starter/src/app/acceptance/output-export.ts +22 -10
  150. package/templates/starter/src/app/acceptance/output-video-export-rules.ts +99 -0
  151. package/templates/starter/src/app/acceptance/types.ts +23 -8
  152. package/templates/starter/src/app/acceptance/validate-coverage.ts +19 -2
  153. package/templates/starter/src/app/starter-acceptance.artifact-export-intent.test.ts +239 -0
  154. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +33 -0
  155. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +16 -1
  156. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +65 -0
  157. package/templates/starter/src/app/starter-acceptance.contract-fixtures.test.ts +41 -0
  158. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +112 -16
  159. package/templates/starter/src/app/starter-acceptance.control-applicability.test.ts +224 -0
  160. package/templates/starter/src/app/starter-acceptance.control-order.test.ts +58 -3
  161. package/templates/starter/src/app/starter-acceptance.control-state.test.ts +0 -111
  162. package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +3 -1
  163. package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +1 -1
  164. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +22 -0
  165. package/templates/starter/src/app/{starter-acceptance.image-video-export-settings.test.ts → starter-acceptance.image-export-settings.test.ts} +37 -9
  166. package/templates/starter/src/app/starter-acceptance.interaction-ownership.test.ts +12 -0
  167. package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +11 -1
  168. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +28 -0
  169. package/templates/starter/src/app/starter-acceptance.output-export-intent.test.ts +384 -0
  170. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +4 -0
  171. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +112 -105
  172. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +12 -0
  173. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +1 -1
  174. package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +35 -24
  175. package/templates/starter/src/app/starter-acceptance.section-size.test.ts +72 -0
  176. package/templates/starter/src/app/starter-acceptance.ts +11 -2
  177. package/templates/starter/src/app/starter-acceptance.video-export-settings.test.ts +336 -0
  178. package/templates/starter/src/app/starter-acceptance.view-interaction.test.ts +8 -0
  179. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -2
  180. package/templates/starter/src/routes/index.tsx +1 -0
  181. package/templates/ui/components/composites/tabs.tsx +2 -2
  182. package/templates/ui/components/controls/collection-actions/collection-item-groups.test.tsx +38 -0
  183. package/templates/ui/components/controls/collection-actions/collection-item-groups.tsx +36 -0
  184. package/templates/ui/components/controls/collection-actions/index.ts +2 -0
  185. package/templates/ui/components/controls/file-drop/file-drop-collection-control.tsx +137 -0
  186. package/templates/ui/components/controls/file-drop/file-drop-control.test.tsx +189 -0
  187. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +81 -88
  188. package/templates/ui/components/controls/file-drop/file-drop-presentation.tsx +131 -9
  189. package/templates/ui/components/controls/file-drop/file-drop-types.ts +36 -22
  190. package/templates/ui/components/controls/file-drop/index.ts +1 -0
  191. package/templates/ui/components/controls/index.ts +3 -0
  192. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +1 -1
  193. package/templates/ui/components/controls/range-slider/range-slider-value.test.ts +14 -0
  194. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -2
  195. package/templates/starter/src/app/acceptance/condition-exclusivity.test.ts +0 -60
  196. package/templates/starter/src/app/acceptance/condition-exclusivity.ts +0 -221
@@ -42,14 +42,14 @@ Custom product output declares bounds on the composition:
42
42
  ```tsx
43
43
  export const appComposition: ToolcraftAppComposition = {
44
44
  canvasContent: <ProductCanvas />,
45
- sceneBoundsProvider: ({ state, timeRange }) => [
46
- getVisibleProductSceneRect(state, timeRange),
45
+ sceneBoundsProvider: ({ state }) => [
46
+ getVisibleProductSceneRect(state),
47
47
  ],
48
48
  schema: appSchema,
49
49
  };
50
50
  ```
51
51
 
52
- The provider returns world-space `{ x, y, width, height }` rectangles. Runtime unions them once with visible image/model frames. It is called only for infinite export; video passes the requested time range so the provider can return one complete animation envelope. Product code does not read DOM bounds or the dormant finite size.
52
+ The provider returns world-space `{ x, y, width, height }` rectangles for one exact state. Runtime uses their union for the live infinite product scene, unions them with visible image/model frames for export, and calls the provider for every scheduled video state before forming one stable envelope. Canvas 2D, WebGL, and WebGPU product output calls `useToolcraftProductSceneFrame()` inside `canvasContent` for active backing size and world-to-local translation. Product code does not read DOM bounds, author a time-range envelope, or reuse dormant finite size in infinite mode.
53
53
 
54
54
  Use `canvas.renderScale: true` or `canvas.renderScale: { step }` only for non-vector raster previews such as Canvas 2D, WebGL, or WebGPU. For this field, product code may customize only the slider step; authored schema cannot set `enabled`, `min`, `defaultValue`, or `max`. Runtime resolves enabled render scale to `{ enabled: true, min: 1, defaultValue: 2, max: 2, step }`, using `0.25` when no step is authored. A custom step must be finite, between `0.01` and `1`, and evenly partition the canonical `1..2` range so `2` remains reachable; invalid input fails fast. Do not enable it for DOM/SVG/vector-native previews. Enabling it requires one browser runtime acceptance row targeting `canvas.renderScale` with `renderScaleCoverage.kind: "selected-backing-pixels"` and exact sorted states `["interaction", "steady"]`, plus `"playback"` when timeline is enabled. The fixed `canvas-render-scale-backing` recipe proves actual backing pixels rather than timing.
55
55
 
@@ -89,7 +89,7 @@ media: {
89
89
 
90
90
  `sourceTarget` must match a compatible `fileDrop` control target. Runtime shows the asset as an attached file, users can remove it, and Reset restores it. Model defaults require `assetKind: "model"`, a root `fileName`, and complete local `sourceFiles`; each source uses a serializable `dataUrl` plus its bundle-relative `path`. Runtime restores default models through the same validation, analysis, repair, repository, and rendering pipeline as a user upload. Persisted empty media remains empty until Reset and must not silently resurrect defaults.
91
91
 
92
- For a multi-item `fileDrop`, `recommendedMaxItems` is advisory. `hardMaxItems`, when present, must be a finite nonnegative safe integer and is enforced before decode or storage allocation. Additive imports count current assets with the same `sourceTarget` plus the incoming logical batch; replacement imports count only the incoming batch.
92
+ For a multi-item `fileDrop`, `recommendedMaxItems` is advisory. `hardMaxItems`, when present, must be a finite nonnegative safe integer and is enforced before decode or storage allocation. Additive imports count current assets with the same `sourceTarget` plus the incoming logical batch; replacement imports count only the incoming batch. A `fileDrop` with `assetKind: "file"`, `multiple: true`, and `variant: "collection-actions"` may declare `itemControls`; each entry uses a supported built-in collection item control and declares `defaultValue`. Attached files render these controls directly beneath their upload row, values persist at the parent target as records keyed by `mediaId`, and file bytes remain runtime-owned media.
93
93
 
94
94
  ## Model FileDrop
95
95
 
@@ -158,9 +158,9 @@ Common control fields:
158
158
  | `defaultValue` | Initial value and reset value. |
159
159
  | `label` | Short UI label, `false`, or omitted. |
160
160
  | `description` | Product-specific help text only when it adds meaning beyond the label. |
161
- | `visibleWhen` | Mode/type/source/include/variant/count visibility. Hidden values are preserved. |
161
+ | `applicability` | Required product claim: `{ mode: "always" }` or `{ mode: "conditional", all: [...] }`. Hidden values are preserved. |
162
162
  | `orderRole` | Makes section order testable. |
163
- | `semanticGroup` | Language-independent product sub-entity/workflow grouping, required on every control in sections larger than seven controls. |
163
+ | `semanticGroup` | Language-independent product sub-entity/workflow grouping, required on every control in sections with eight to ten controls. |
164
164
  | `sliderValueKind` | `slider` intent: `"continuous"` or `"discrete"`. |
165
165
  | `textValueKind` | `text`/`code` intent: `"single-line"`, `"multiline"`, or `"structured"`. |
166
166
  | `curveIntent` | `curves` composition: `"single-value-map"` or `"color-channels"`. |
@@ -170,11 +170,9 @@ Common control fields:
170
170
  | `keyframeable` | Timeline/keyframe capability override when structurally needed. |
171
171
  | `variant` | Component-specific variant. |
172
172
 
173
- Use `visibleWhen` for product availability. Do not use `disabled`, `disabledWhen`, or inert visible controls for generated product branches.
173
+ Every authored product control declares `applicability`: use `{ mode: "always" }` only when it affects its accepted outcome in every finite sibling-selector branch, or `{ mode: "conditional", all: [...] }` when every predicate must match. Inactive controls are absent while values remain preserved. Do not use `disabled`, `disabledWhen`, or inert visible controls for availability. Legacy control `visibleWhen` remains readable only at the low-level compatibility boundary; product acceptance rejects it and omitted applicability. Section `visibleWhen` remains a separate legacy section-layout feature.
174
174
 
175
- Conditions support `equals`, `notEquals`, `oneOf`, `notOneOf`, `greaterThan`, `greaterThanOrEqual`, `lessThan`, and `lessThanOrEqual`.
176
-
177
- Reserved runtime targets include `runtime.settingsTransfer`, `canvas.infinity`, `canvas.aspectRatio`, `canvas.size.width`, `canvas.size.height`, `canvas.renderScale`, and `panels.timeline.extended`. Product sections must not declare those controls.
175
+ Conditions support `equals`, `notEquals`, `oneOf`, `notOneOf`, `greaterThan`, `greaterThanOrEqual`, `lessThan`, and `lessThanOrEqual`. Reserved runtime targets include `runtime.settingsTransfer`, `canvas.infinity`, `canvas.aspectRatio`, `canvas.size.width`, `canvas.size.height`, `canvas.renderScale`, and `panels.timeline.extended`; product sections must not declare them.
178
176
 
179
177
  ## Built-In Control Types
180
178
 
@@ -194,6 +192,7 @@ Reserved runtime targets include `runtime.settingsTransfer`, `canvas.infinity`,
194
192
  | `checkbox` | `Checkbox` |
195
193
  | `actions` | `Actions` |
196
194
  | `collectionActions` | `CollectionActions` |
195
+ | `sourceCollection` | `ControlsPanelCollectionItems` |
197
196
  | `panelActions` | `PanelActions` |
198
197
  | `colorOpacity` | `ColorOpacity` |
199
198
  | `palette` | `Palette` |
@@ -210,7 +209,7 @@ Reserved runtime targets include `runtime.settingsTransfer`, `canvas.infinity`,
210
209
  | `settingsTransfer` | `SettingsTransfer` |
211
210
  [//]: # (toolcraft-contract:built-in-control-table:end)
212
211
 
213
- Use `component-rules.md` for component-specific fit, labels, variants, units, parser behavior, and exceptions. Use `core/control-selection.md` before deciding a custom control is needed.
212
+ Use `component-rules.md` for component-specific fit, labels, variants, units, parser behavior, and exceptions. `sourceCollection` renders a supported built-in `itemControl` for a source-owned array without add/remove UI; `collectionActions` owns user cardinality and its limits. Use `itemControl` for one homogeneous repeated value. For one logical repeated entity made from two or more built-in fields, use `itemControls`; for example, `{ type: "collectionActions", target: "surface.layers", defaultValue: [{ strength: 0.4, invert: false }], itemControls: { strength: { type: "slider", defaultValue: 0.5 }, invert: { type: "switch", defaultValue: false } } }`. Every field requires `defaultValue`; `itemControl` and `itemDefaultValue` are invalid competing template sources. `+` appends the complete defaults, `−` removes the final record, edits preserve sibling/product keys, and runtime places a line only between compound records without `Item N` headings. Standalone color `itemControl` remains a divider-free two-column grid. Use `core/control-selection.md` before deciding a custom control is needed.
214
213
 
215
214
  `orientationGizmo` uses a non-degenerate `{ position: [x, y, z], up: [x, y, z] }` default, `label: false`, and `keyframeable: false`. Declare one target for the active/selected model and keep it beside at least one visible product control in the semantic model/view section; runtime renders the handle on the canvas rather than in the controls panel. Multiple declarations require statically provable mutually exclusive combined section/control visibility conditions, because runtime permits at most one active orientation handle.
216
215
 
@@ -221,6 +220,7 @@ before schema controls or renderer code:
221
220
 
222
221
  ```ts
223
222
  export const starterProductReadiness: ToolcraftProductReadiness = {
223
+ exportIntent: { image: { mode: "toolcraft-default" }, video: { mode: "not-requested" } },
224
224
  interactionOwnership: [],
225
225
  mode: "product",
226
226
  productName: "Model Studio",
@@ -290,22 +290,22 @@ exactly match schema `orientationGizmo` targets.
290
290
  ## Control Section Inventory
291
291
 
292
292
  Before writing `panels.controls.sections`, export `starterControlSectionInventory` beside `starterAcceptance`:
293
-
294
293
  ```ts
295
294
  export const starterControlSectionInventory = [
296
295
  {
297
296
  entity: "Text block",
298
- groupingReason:
299
- "These controls edit the text content, typography, and visible text fill together.",
297
+ entityId: "text-block",
298
+ groupingReason: "These controls edit the text content, typography, and visible text fill together.",
299
+ id: "text",
300
300
  targets: ["text.content", "text.font"],
301
301
  title: "Text",
302
302
  },
303
303
  ] as const;
304
304
  ```
305
305
 
306
- Every product control target appears exactly once in the inventory. Runtime `Setup`, sticky footer `Export`, settings transfer, and runtime canvas sizing targets do not need entries.
307
-
308
- For sections larger than seven controls, each control also declares `semanticGroup`. Controls editing one tightly scoped product sub-entity share the same value; mixed groups make a broad section fail and should be split. This fact is structural and may not be inferred from English labels.
306
+ - Every product control target appears exactly once in the inventory. Runtime-owned `Setup`, sticky footer `Export`, settings transfer, and runtime canvas sizing targets do not need entries. The product targets authored in the standard `Background` source section remain in its `Background` inventory entry after runtime relocates those controls into `Setup`. Stable `entityId` is the primary grouping authority; target namespaces are only secondary diagnostics.
307
+ - Keep one logical entity in one section through ten controls. One to seven controls is the normal size; sections with eight to ten controls require `semanticGroup` on every control. Controls editing one tightly scoped product sub-entity share the same group, and this fact may not be inferred from English labels.
308
+ - When one entity has more than ten controls, split it into balanced workflow sections containing two to ten controls. Every split section keeps the same `entityId` and `entity`, declares a unique `workflowStage`, and provides a concrete `splitReason`. Do not create a one-control tail.
309
309
 
310
310
  ## Transfer Metadata
311
311
 
@@ -313,13 +313,15 @@ Reference and motion metadata lives in `starterTransferMode`.
313
313
 
314
314
  Use `transferMode: "reference-runtime-clone"` when porting an existing app unless the user explicitly asks for redesign. Reference clones declare `referenceStudy`, `referenceFeatureInventory`, and acceptance mapping; detailed evidence requirements live in `core/reference-study.md`.
315
315
 
316
+ Every product readiness declaration includes required `exportIntent`. Its resolved image/video capabilities must correspond exactly to schema actions and settings sections. Use `core/setup-export.md` for the authoritative modes, evidence requirements, and decision sequence.
317
+
316
318
  Video references declare `videoReferenceStudy` before implementation. Animated products declare `animationIntent`, and playback/keyframe timeline apps declare a proven loop duration when known. Detailed animation rules live in `core/timeline-animation.md`.
317
319
 
318
320
  ## Export And Actions
319
321
 
320
- Product apps expose delivery through sticky `panelActions`, not canvas UI or ordinary body controls. Every product exposes `Export PNG`; products whose requested delivery includes animation also expose `Export Video`. A playback or keyframe timeline may drive interactive preview without inventing video delivery.
322
+ Product apps expose enabled artifact delivery through sticky `panelActions`, not canvas UI or ordinary body controls. `productReadiness.exportIntent` is required: image export defaults on and is absent only with explicit removal evidence; video export exists only with explicit user-request evidence. Animation, playback, keyframes, and timeline presence never change that intent.
321
323
 
322
- Every app with `Export PNG` includes `Image Export` controls with `export.image.format` and `export.image.resolution`. Apps with `Export Video` also include `Video Export` controls with `export.video.format` and `export.video.resolution`.
324
+ Every app with `Export PNG` includes `Image Export` controls with `export.image.format` and `export.image.resolution`. Apps with `Export Video` include `Video Export` controls with `export.video.format` and `export.video.resolution`. Image-only, image-plus-video, video-only, and explicit no-export layouts must match `core/setup-export.md` exactly.
323
325
 
324
326
  Use the standard runtime path: declare typed `export-image`/`export-video` panel actions and their settings, provide one `ToolcraftAppComposition.exportRenderer` when product code contributes pixels, and draw one deterministic frame from the supplied state/time in scene coordinates. Call `shouldIncludeToolcraftPreviewBackground(state)` only for bounded live preview; runtime owns artifact composition, sizing, encoding, download, progress, and errors.
325
327
 
@@ -0,0 +1,163 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { defineToolcraft } from "@repo/toolcraft-runtime";
3
+
4
+ import { deriveToolcraftBrowserRuntimeRequirements } from "./browser-runtime-evidence-requirements";
5
+
6
+ const inventory = [
7
+ {
8
+ entity: "Feature",
9
+ entityId: "feature",
10
+ groupingReason: "Mode and amount describe one feature.",
11
+ id: "feature",
12
+ targets: ["feature.mode", "feature.amount"],
13
+ title: "Feature",
14
+ },
15
+ ] as const;
16
+
17
+ test("conditional controls derive case-specific visibility and outcome evidence", () => {
18
+ const schema = defineToolcraft({
19
+ canvas: { enabled: true },
20
+ panels: {
21
+ controls: {
22
+ sections: [
23
+ {
24
+ controls: {
25
+ amount: {
26
+ applicability: {
27
+ all: [{ equals: "advanced", target: "feature.mode" }],
28
+ mode: "conditional",
29
+ },
30
+ defaultValue: 1,
31
+ target: "feature.amount",
32
+ type: "slider",
33
+ },
34
+ mode: {
35
+ applicability: { mode: "always" },
36
+ defaultValue: "simple",
37
+ options: [
38
+ { label: "Simple", value: "simple" },
39
+ { label: "Advanced", value: "advanced" },
40
+ ],
41
+ target: "feature.mode",
42
+ type: "segmented",
43
+ },
44
+ },
45
+ id: "feature",
46
+ title: "Feature",
47
+ },
48
+ ],
49
+ title: "Controls",
50
+ },
51
+ },
52
+ });
53
+ const requirements = deriveToolcraftBrowserRuntimeRequirements(
54
+ [
55
+ {
56
+ browser: true,
57
+ browserTestName: "browser: conditional amount",
58
+ evidence: "product-output",
59
+ id: "feature.amount",
60
+ kind: "control",
61
+ target: "feature.amount",
62
+ },
63
+ ],
64
+ schema,
65
+ inventory,
66
+ );
67
+
68
+ expect(requirements.map(({ evidenceType }) => evidenceType)).toEqual(
69
+ expect.arrayContaining([
70
+ "control-applicability-hidden",
71
+ "control-applicability-visible",
72
+ "product-observable-change",
73
+ ]),
74
+ );
75
+ expect(
76
+ requirements.some(
77
+ (requirement) => requirement.requirementId === "feature.amount",
78
+ ),
79
+ ).toBe(false);
80
+ });
81
+
82
+ test("finite siblings expand outcomes even when always-applicability omits the selector", () => {
83
+ const schema = defineToolcraft({
84
+ canvas: { enabled: true },
85
+ panels: {
86
+ controls: {
87
+ sections: [
88
+ {
89
+ controls: {
90
+ amount: {
91
+ applicability: { mode: "always" },
92
+ defaultValue: 1,
93
+ target: "feature.amount",
94
+ type: "slider",
95
+ },
96
+ mode: {
97
+ applicability: { mode: "always" },
98
+ defaultValue: "simple",
99
+ options: [
100
+ { label: "Simple", value: "simple" },
101
+ { label: "Advanced", value: "advanced" },
102
+ ],
103
+ target: "feature.mode",
104
+ type: "segmented",
105
+ },
106
+ },
107
+ id: "feature",
108
+ title: "Feature",
109
+ },
110
+ ],
111
+ title: "Controls",
112
+ },
113
+ },
114
+ });
115
+ const acceptance = {
116
+ browser: true,
117
+ browserTestName: "browser: feature amount",
118
+ evidence: "product-output" as const,
119
+ id: "feature.amount",
120
+ kind: "control" as const,
121
+ target: "feature.amount",
122
+ };
123
+ const requirements = deriveToolcraftBrowserRuntimeRequirements(
124
+ [acceptance],
125
+ schema,
126
+ inventory,
127
+ );
128
+
129
+ expect(requirements).toHaveLength(4);
130
+ expect(
131
+ requirements.filter(
132
+ ({ evidenceType }) => evidenceType === "control-applicability-visible",
133
+ ),
134
+ ).toHaveLength(2);
135
+ expect(
136
+ requirements.filter(
137
+ ({ evidenceType }) => evidenceType === "product-observable-change",
138
+ ),
139
+ ).toHaveLength(2);
140
+ expect(
141
+ requirements.every(({ requirementId }) =>
142
+ requirementId.includes("#applicability:feature.mode="),
143
+ ),
144
+ ).toBe(true);
145
+
146
+ expect(() =>
147
+ deriveToolcraftBrowserRuntimeRequirements(
148
+ [
149
+ {
150
+ ...acceptance,
151
+ renderScaleCoverage: {
152
+ kind: "selected-backing-pixels",
153
+ states: [],
154
+ },
155
+ },
156
+ ],
157
+ schema,
158
+ inventory,
159
+ ),
160
+ ).toThrow(
161
+ "Visible applicability case for feature.amount has no product outcome requirement",
162
+ );
163
+ });
@@ -17,13 +17,18 @@ test("toolcraft Infinity canvas observation reads runtime-owned geometry", async
17
17
  await page.setContent(`
18
18
  <div data-toolcraft-canvas-world data-toolcraft-canvas-offset-x="12" data-toolcraft-canvas-offset-y="-8">
19
19
  <div data-toolcraft-canvas-mode="infinite" style="overflow: visible">
20
- <div data-scene style="position:absolute;left:-320px;top:-200px;width:640px;height:400px"></div>
20
+ <div data-scene style="position:absolute;left:1px;top:2px;width:3px;height:4px"></div>
21
+ <div
22
+ data-toolcraft-product-scene
23
+ data-toolcraft-product-scene-status="ready"
24
+ style="position:absolute;left:-320px;top:-200px;width:640px;height:400px"
25
+ ></div>
21
26
  </div>
22
27
  </div>
23
28
  `);
24
29
 
25
30
  expectInfiniteCanvasObservation(
26
- await observeInfinityCanvas(page, "[data-scene]"),
31
+ await observeInfinityCanvas(page),
27
32
  { height: 400, width: 640, x: -320, y: -200 },
28
33
  );
29
34
  });
@@ -14,6 +14,81 @@ import {
14
14
  setProofState,
15
15
  } from "./browser-semantic-evidence-test-helpers";
16
16
 
17
+ const orientationBaseline: ToolcraftOrientationBrowserObservation = {
18
+ outputSignature: "model-front",
19
+ pixelSignature: "pixels-front",
20
+ pose: { position: [0, 0, 5], up: [0, 1, 0] },
21
+ poseTarget: "view.orbit",
22
+ presentationCacheKey: "appearance:document-model:v2",
23
+ presentationDocumentId: "document-model",
24
+ viewportOffsetX: 0,
25
+ viewportOffsetY: 0,
26
+ };
27
+
28
+ const orientationDragged: ToolcraftOrientationBrowserObservation = {
29
+ ...orientationBaseline,
30
+ outputSignature: "model-dragged",
31
+ pixelSignature: "pixels-dragged",
32
+ pose: { position: [1, 1, 4.8], up: [0, 1, 0] },
33
+ };
34
+
35
+ const orientationOptions = {
36
+ requirementId: "model.orientation",
37
+ stabilityIntervalMs: 0,
38
+ target: "view.orbit",
39
+ } as const;
40
+
41
+ type OrientationProofUiState = Readonly<{
42
+ playbackActions?: readonly ("pause" | "play")[];
43
+ renderScales?: readonly Readonly<{
44
+ maximum: string;
45
+ value: string;
46
+ }>[];
47
+ }>;
48
+
49
+ async function installOrientationProofUiState(
50
+ page: Page,
51
+ state: OrientationProofUiState,
52
+ ): Promise<void> {
53
+ await page.locator('[data-slot="toolcraft-runtime-app"]').evaluate(
54
+ (root, proofState) => {
55
+ root.querySelector('[data-orientation-proof-state]')?.remove();
56
+ const fixture = document.createElement("div");
57
+ fixture.dataset.orientationProofState = "";
58
+
59
+ for (const action of proofState.playbackActions ?? []) {
60
+ const button = document.createElement("button");
61
+ const label = action === "play" ? "Play playback" : "Pause playback";
62
+ button.type = "button";
63
+ button.textContent = label;
64
+ button.setAttribute("aria-label", label);
65
+ fixture.append(button);
66
+ }
67
+
68
+ for (const [index, renderScale] of (
69
+ proofState.renderScales ?? []
70
+ ).entries()) {
71
+ const field = document.createElement("div");
72
+ field.dataset.toolcraftControlTarget = "canvas.renderScale";
73
+ field.dataset.orientationProofRenderScale = String(index);
74
+ const slider = document.createElement("div");
75
+ slider.dataset.slot = "slider";
76
+ slider.setAttribute("aria-valuemax", renderScale.maximum);
77
+ slider.setAttribute("aria-valuemin", "1");
78
+ slider.setAttribute("aria-valuenow", renderScale.value);
79
+ slider.setAttribute("role", "slider");
80
+ Object.assign(slider.style, { height: "20px", width: "120px" });
81
+ slider.textContent = "Render scale";
82
+ field.append(slider);
83
+ fixture.append(field);
84
+ }
85
+
86
+ root.append(fixture);
87
+ },
88
+ state,
89
+ );
90
+ }
91
+
17
92
  async function installOrientationGizmoPointerProbe(
18
93
  page: Page,
19
94
  initial: ToolcraftOrientationBrowserObservation,
@@ -123,33 +198,107 @@ async function installModelOrbitPointerProbe(
123
198
  );
124
199
  }
125
200
 
201
+ test("orientation axis drag validates paused maximum-quality proof state", async ({
202
+ page,
203
+ }, testInfo) => {
204
+ await page.goto("/");
205
+ const session = await createToolcraftBrowserProofSession(page);
206
+ const attachmentCount = testInfo.attachments.length;
207
+ const observation = session.observe((root) =>
208
+ JSON.parse(root.getAttribute("data-proof-orientation") ?? "null"),
209
+ );
210
+ const invalidStateCases: readonly Readonly<{
211
+ expectedError: RegExp;
212
+ state: OrientationProofUiState;
213
+ }>[] = [
214
+ {
215
+ expectedError: /must be paused.*Play playback/iu,
216
+ state: {
217
+ playbackActions: ["pause"],
218
+ renderScales: [{ maximum: "2", value: "2" }],
219
+ },
220
+ },
221
+ {
222
+ expectedError: /exactly one standard playback action/iu,
223
+ state: { playbackActions: ["play", "pause"] },
224
+ },
225
+ {
226
+ expectedError: /canvas\.renderScale.*maximum/iu,
227
+ state: {
228
+ playbackActions: ["play"],
229
+ renderScales: [{ maximum: "2", value: "1" }],
230
+ },
231
+ },
232
+ {
233
+ expectedError: /exactly one canvas\.renderScale owner/iu,
234
+ state: {
235
+ playbackActions: ["play"],
236
+ renderScales: [
237
+ { maximum: "2", value: "2" },
238
+ { maximum: "2", value: "2" },
239
+ ],
240
+ },
241
+ },
242
+ {
243
+ expectedError: /finite aria-valuenow/iu,
244
+ state: {
245
+ playbackActions: ["play"],
246
+ renderScales: [{ maximum: "2", value: "" }],
247
+ },
248
+ },
249
+ {
250
+ expectedError: /finite aria-valuemax/iu,
251
+ state: {
252
+ playbackActions: ["play"],
253
+ renderScales: [{ maximum: "invalid", value: "2" }],
254
+ },
255
+ },
256
+ ];
257
+
258
+ for (const { expectedError, state } of invalidStateCases) {
259
+ await setProofState(page, "orientation", orientationBaseline);
260
+ await installOrientationGizmoPointerProbe(page, orientationBaseline, {
261
+ outcomes: { drag: orientationDragged },
262
+ });
263
+ await installOrientationProofUiState(page, state);
264
+ await expect(
265
+ expectToolcraftOrientationAxisDrag(observation, session, {
266
+ ...orientationOptions,
267
+ dragDelta: { x: 18, y: 12 },
268
+ }),
269
+ ).rejects.toThrow(expectedError);
270
+ expect(testInfo.attachments).toHaveLength(attachmentCount);
271
+ }
272
+ });
273
+
274
+ test("orientation axis drag retains proof for products without optional playback or render scale", async ({
275
+ page,
276
+ }) => {
277
+ await page.goto("/");
278
+ const session = await createToolcraftBrowserProofSession(page);
279
+ await setProofState(page, "orientation", orientationBaseline);
280
+ const observation = session.observe((root) =>
281
+ JSON.parse(root.getAttribute("data-proof-orientation") ?? "null"),
282
+ );
283
+ await installOrientationGizmoPointerProbe(page, orientationBaseline, {
284
+ outcomes: { drag: orientationDragged },
285
+ });
286
+
287
+ await expectToolcraftOrientationAxisDrag(observation, session, {
288
+ ...orientationOptions,
289
+ dragDelta: { x: 18, y: 12 },
290
+ });
291
+ });
292
+
126
293
  test("orientation recipes require shared pose/output changes and correct canvas ownership", async ({
127
294
  page,
128
295
  }, testInfo) => {
129
296
  await page.goto("/");
130
297
  const session = await createToolcraftBrowserProofSession(page);
131
298
  const attachmentCount = testInfo.attachments.length;
132
- const baseline: ToolcraftOrientationBrowserObservation = {
133
- outputSignature: "model-front",
134
- pixelSignature: "pixels-front",
135
- pose: { position: [0, 0, 5], up: [0, 1, 0] },
136
- poseTarget: "view.orbit",
137
- presentationCacheKey: "appearance:document-model:v2",
138
- presentationDocumentId: "document-model",
139
- viewportOffsetX: 0,
140
- viewportOffsetY: 0,
141
- };
142
- const dragged: ToolcraftOrientationBrowserObservation = {
143
- ...baseline,
144
- outputSignature: "model-dragged",
145
- pixelSignature: "pixels-dragged",
146
- pose: { position: [1, 1, 4.8], up: [0, 1, 0] },
147
- };
148
- const options = {
149
- requirementId: "model.orientation",
150
- stabilityIntervalMs: 0,
151
- target: "view.orbit",
152
- } as const;
299
+ const baseline = orientationBaseline;
300
+ const dragged = orientationDragged;
301
+ const options = orientationOptions;
153
302
 
154
303
  await setProofState(page, "orientation", baseline);
155
304
  const observation = session.observe((root) =>
@@ -175,6 +324,10 @@ test("orientation recipes require shared pose/output changes and correct canvas
175
324
  expect(testInfo.attachments).toHaveLength(attachmentCount);
176
325
 
177
326
  await setProofState(page, "orientation", baseline);
327
+ await installOrientationProofUiState(page, {
328
+ playbackActions: ["play"],
329
+ renderScales: [{ maximum: "2", value: "2" }],
330
+ });
178
331
  await installOrientationGizmoPointerProbe(page, baseline, {
179
332
  dragCommit: "pointerup",
180
333
  outcomes: { drag: dragged },
@@ -133,7 +133,7 @@ test("control schema derives segmented and discrete runtime evidence", () => {
133
133
  );
134
134
  });
135
135
 
136
- test("typed visibility and background coverage derive dedicated semantic evidence", () => {
136
+ test("typed background coverage derives dedicated semantic evidence", () => {
137
137
  const schema = defineToolcraft({
138
138
  canvas: { enabled: true },
139
139
  panels: {
@@ -141,19 +141,6 @@ test("typed visibility and background coverage derive dedicated semantic evidenc
141
141
  sections: [
142
142
  {
143
143
  controls: {
144
- conditional: {
145
- defaultValue: 1,
146
- label: "Conditional",
147
- target: "feature.conditional",
148
- type: "slider",
149
- visibleWhen: { equals: true, target: "feature.enabled" },
150
- },
151
- enabled: {
152
- defaultValue: false,
153
- label: "Enabled",
154
- target: "feature.enabled",
155
- type: "switch",
156
- },
157
144
  includeBackground: {
158
145
  defaultValue: true,
159
146
  label: "Include",
@@ -181,14 +168,6 @@ test("typed visibility and background coverage derive dedicated semantic evidenc
181
168
  });
182
169
  const requirements = deriveToolcraftBrowserRuntimeRequirements(
183
170
  [
184
- {
185
- browser: true,
186
- browserTestName: "browser: conditional visibility",
187
- evidence: "product-output",
188
- id: "feature.conditional",
189
- target: "feature.conditional",
190
- visibilityCoverage: ["hidden", "visible"],
191
- },
192
171
  {
193
172
  backgroundOutputCoverage: [
194
173
  "preview-hidden-when-excluded",
@@ -205,17 +184,6 @@ test("typed visibility and background coverage derive dedicated semantic evidenc
205
184
  ],
206
185
  schema,
207
186
  );
208
-
209
- expect(
210
- requirements
211
- .filter((requirement) => requirement.requirementId === "feature.conditional")
212
- .map((requirement) => requirement.evidenceType),
213
- ).toEqual(
214
- expect.arrayContaining([
215
- "conditional-control-hidden",
216
- "conditional-control-visible",
217
- ]),
218
- );
219
187
  expect(
220
188
  requirements
221
189
  .filter((requirement) => requirement.requirementId === "export.includeBackground")