@pixel-point/toolcraft 0.0.8 → 0.0.11

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 (88) hide show
  1. package/README.md +42 -9
  2. package/package.json +1 -1
  3. package/src/generate.mjs +42 -5
  4. package/src/generate.test.mjs +40 -0
  5. package/src/package-json.mjs +15 -0
  6. package/src/package-json.test.mjs +14 -1
  7. package/templates/runtime/contracts/component-contracts.test.ts +251 -47
  8. package/templates/runtime/contracts/component-contracts.ts +130 -57
  9. package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
  10. package/templates/runtime/contracts/decision-contracts.ts +22 -9
  11. package/templates/runtime/export/export.test.ts +63 -0
  12. package/templates/runtime/export/export.ts +55 -0
  13. package/templates/runtime/index.ts +1 -0
  14. package/templates/runtime/react/canvas-shell.test.tsx +58 -4
  15. package/templates/runtime/react/canvas-shell.tsx +31 -9
  16. package/templates/runtime/react/controls-panel.test.tsx +916 -108
  17. package/templates/runtime/react/controls-panel.tsx +136 -30
  18. package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
  19. package/templates/runtime/react/settings-transfer.test.ts +4 -0
  20. package/templates/runtime/react/settings-transfer.ts +6 -1
  21. package/templates/runtime/react/timeline-panel.test.tsx +14 -0
  22. package/templates/runtime/react/timeline-panel.tsx +44 -7
  23. package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
  24. package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
  25. package/templates/runtime/react/toolcraft-app.tsx +56 -37
  26. package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
  27. package/templates/runtime/schema/define-toolcraft.ts +140 -246
  28. package/templates/runtime/schema/runtime-targets.ts +21 -0
  29. package/templates/runtime/schema/types.ts +44 -0
  30. package/templates/runtime/state/create-template-state.test.ts +156 -0
  31. package/templates/runtime/state/create-template-state.ts +38 -8
  32. package/templates/runtime/state/media-defaults.ts +105 -0
  33. package/templates/runtime/state/persistence.test.ts +58 -0
  34. package/templates/runtime/state/persistence.ts +105 -1
  35. package/templates/runtime/state/reducer.test.ts +280 -4
  36. package/templates/runtime/state/reducer.ts +195 -9
  37. package/templates/runtime/state/timeline-loop.test.ts +71 -0
  38. package/templates/runtime/state/timeline-loop.ts +35 -0
  39. package/templates/runtime/state/types.ts +27 -0
  40. package/templates/runtime/testing/performance.test.ts +810 -21
  41. package/templates/runtime/testing/performance.ts +823 -53
  42. package/templates/starter/AGENTS.md +24 -18
  43. package/templates/starter/docs/toolcraft/README.md +8 -4
  44. package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
  45. package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
  46. package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
  47. package/templates/starter/docs/toolcraft/component-rules.md +75 -42
  48. package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
  49. package/templates/starter/docs/toolcraft/performance.md +37 -11
  50. package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
  51. package/templates/starter/docs/toolcraft/workflow.md +5 -2
  52. package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
  53. package/templates/starter/e2e/app-performance.spec.ts +55 -2
  54. package/templates/starter/e2e/performance-helpers.ts +45 -0
  55. package/templates/starter/gitignore +1 -0
  56. package/templates/starter/index.html +1 -0
  57. package/templates/starter/package.json +3 -0
  58. package/templates/starter/playwright.config.ts +1 -1
  59. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  60. package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
  61. package/templates/starter/scripts/toolcraft-port.mjs +280 -0
  62. package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
  63. package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
  64. package/templates/starter/src/app/starter-acceptance.ts +1453 -97
  65. package/templates/starter/src/app/starter-performance.test.ts +111 -7
  66. package/templates/starter/src/app/starter-performance.ts +5 -0
  67. package/templates/starter/src/app/starter-schema.test.ts +32 -7
  68. package/templates/starter/src/app/starter-schema.ts +6 -2
  69. package/templates/starter/vite.config.ts +58 -2
  70. package/templates/ui/components/control-layout/index.tsx +8 -3
  71. package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
  72. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  73. package/templates/ui/components/controls/color/index.ts +4 -1
  74. package/templates/ui/components/controls/color/palette-control.tsx +34 -4
  75. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  76. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  77. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
  78. package/templates/ui/components/controls/file-drop/index.ts +6 -1
  79. package/templates/ui/components/controls/index.ts +4 -0
  80. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  81. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
  82. package/templates/ui/components/controls/select/select-control.tsx +8 -25
  83. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  84. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  85. package/templates/ui/components/controls/vector/index.ts +1 -0
  86. package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
  87. package/templates/ui/components/panel/panel-actions.tsx +1 -1
  88. package/templates/ui/components/panel/panel-section.tsx +29 -5
@@ -82,12 +82,24 @@ describe("Toolcraft template component contracts", () => {
82
82
 
83
83
  expect(segmented?.strictness).toBe("best-fit");
84
84
  expect(segmented?.acceptableAlternatives?.join(" ")).toMatch(/Select/i);
85
+ expect(segmented?.layoutConstraints).toContain(
86
+ "Segmented controls are full-width controls and must not be placed in two-column inline or half-width layout groups.",
87
+ );
88
+ expect(getToolcraftComponentContract("segmented").aiUsageRules).toContain(
89
+ "Do not place Segmented beside Switch, Color, Select, or another control in an inline row; use Select when a finite choice must occupy a half-width column.",
90
+ );
85
91
  expect(select?.useWhen.join(" ")).toMatch(/long labels|many options/i);
92
+ expect(select?.useWhen).toContain(
93
+ "Use Select when a dropdown choice is more readable than a row of segmented cells.",
94
+ );
95
+ expect(select?.layoutConstraints).toContain(
96
+ "Standalone Select controls render stacked full-width with the label above the dropdown; do not use the compact side-label row with label left and dropdown right.",
97
+ );
86
98
  expect(select?.layoutConstraints).toContain(
87
- "Prefer compact two-column inline layout for related short Select pairs that tune one workflow or entity.",
99
+ "Prefer compact two-column inline layout only for related short Select pairs that tune one workflow or entity.",
88
100
  );
89
101
  expect(select?.layoutConstraints).toContain(
90
- "Use vertical one-select-per-row layout only as a fallback when a label, selected value, or option text would clip, truncate, or lose internal padding in the compact row.",
102
+ "Use vertical one-select-per-row layout for single Select controls and as the fallback when a Select pair label, selected value, or option text would clip, truncate, or lose internal padding in the compact row.",
91
103
  );
92
104
  expect(select?.layoutConstraints).toContain(
93
105
  "If a compact Select pair falls back to vertical layout, record the fit reason in the spec or worklog.",
@@ -150,6 +162,21 @@ describe("Toolcraft template component contracts", () => {
150
162
  expect(contract.aiUsageRules).toContain(
151
163
  'Use the default vector variant for spatial values such as position, offset, direction, focus, anchor, and light direction.',
152
164
  );
165
+ expect(contract.aiUsageRules).toContain(
166
+ 'Default spatial vector pads use coordinateMode: "screen": dragging left/up makes vector.x and vector.y smaller so canvas objects move left/up without renderer-side Y inversion.',
167
+ );
168
+ expect(contract.aiUsageRules).toContain(
169
+ "Vector pad value labels render compact rounded coordinates. Do not expose raw floating-point strings such as -0.07070312499999998 in the controls panel.",
170
+ );
171
+ expect(contract.aiUsageRules).toContain(
172
+ "Double-clicking a vector pad resets both axes to the control default through the normal runtime value update, matching section header reset semantics; if no default is defined, the fallback is 0,0. Do not add a separate custom reset UI for this basic pad reset behavior.",
173
+ );
174
+ expect(contract.aiUsageRules).toContain(
175
+ "Holding Shift while dragging a vector pad locks movement to the dominant axis and must not select text or page content; do not build a custom pad just to support axis-constrained movement.",
176
+ );
177
+ expect(contract.aiUsageRules).toContain(
178
+ 'Use coordinateMode: "cartesian" only when the product intentionally exposes mathematical Y-up coordinates instead of canvas/screen movement.',
179
+ );
153
180
  expect(contract.aiUsageRules).toContain(
154
181
  "Vector is a compound control; acceptance must prove vector.x and vector.y both affect the product output.",
155
182
  );
@@ -192,7 +219,7 @@ describe("Toolcraft template component contracts", () => {
192
219
  "When the nearest section title already names the switch context, do not duplicate that title as the visible switch label. Use label false for a visual-only toggle and keep the meaning in target/description.",
193
220
  );
194
221
  expect(switchContract.aiUsageRules).toContain(
195
- 'A Switch may share an inline row with one related parameter control when the visible switch label is short enough to fit. That row uses equal-width columns; never shrink the switch column to intrinsic width. In section-owned rows, use a short visible label such as "Include" instead of repeating the section title, such as "Include background" inside Background.',
222
+ 'A Switch may share an inline row with one related parameter control when the visible switch label is short enough to fit. That row uses equal-width columns and the same horizontal column gap as paired Select controls; never shrink the switch column to intrinsic width. The non-switch parameter uses label false in that row; if its label is needed, stack the controls instead. In section-owned rows, use a short visible switch label such as "Include" instead of repeating the section title, such as "Include background" inside Background.',
196
223
  );
197
224
  expect(checkboxContract.aiUsageRules).toContain(
198
225
  'Checkbox labels name the setting context only; do not prefix labels with "Enable" or "Disable" because the checkbox already communicates enabled/selected state.',
@@ -207,7 +234,7 @@ describe("Toolcraft template component contracts", () => {
207
234
  "Two adjacent Checkbox controls for the same product entity must share one inline row when every visible label fits without truncation. Keep paired labels to short one- or two-word names; the runtime auto-pairs safe adjacent checkboxes by target entity, and generated schemas should stack checkboxes only when any label would truncate.",
208
235
  );
209
236
  expect(checkboxContract.aiUsageRules).toContain(
210
- "A Checkbox may share an inline row with one related parameter control when the visible checkbox label is short enough to fit. That row uses equal-width columns; never shrink the checkbox column to intrinsic width. Hide the checkbox label when the section title provides the visible context.",
237
+ "A Checkbox may share an inline row with one related parameter control when the visible checkbox label is short enough to fit. That row uses equal-width columns and the same horizontal column gap as paired Select controls; never shrink the checkbox column to intrinsic width. The non-checkbox parameter uses label false in that row; if its label is needed, stack the controls instead. Hide the checkbox label when the section title provides the visible context.",
211
238
  );
212
239
  });
213
240
 
@@ -261,10 +288,28 @@ describe("Toolcraft template component contracts", () => {
261
288
  "Do not use Actions for animation transport; Play, Pause, Resume, Restart, and Scrub belong to the top timeline when timeline behavior exists.",
262
289
  );
263
290
  expect(contract.decisionCatalog?.layoutConstraints).toContain(
264
- "When an Actions button group wraps below its label, the wrapped button row starts at the left edge of the control content instead of centering or staying right-aligned.",
291
+ "Do not set an Actions control label to the exact same visible text as its only button; use a short one- or two-word context label such as Ink wash, Palette action, or Current layer while the button keeps the command verb.",
292
+ );
293
+ expect(contract.decisionCatalog?.layoutConstraints).toContain(
294
+ "Actions never use a side-label layout. If a visible label exists, it sits above the buttons.",
295
+ );
296
+ expect(contract.decisionCatalog?.layoutConstraints).toContain(
297
+ "Actions buttons render as a two-column grid. One visible button occupies the left half of the section; two buttons occupy one half each; more than two buttons wrap into additional 50% cells.",
298
+ );
299
+ expect(contract.decisionCatalog?.layoutConstraints).toContain(
300
+ "Do not center or right-align a partial final Actions row; an odd trailing button stays in the left 50% cell.",
301
+ );
302
+ expect(contract.aiUsageRules).toContain(
303
+ "For a single visible Actions button, the control label and button label must not be identical; make the control label a concise context and the button label the command.",
304
+ );
305
+ expect(contract.aiUsageRules).toContain(
306
+ "Render the Actions label above the buttons; do not put the label on the left with buttons on the right.",
307
+ );
308
+ expect(contract.aiUsageRules).toContain(
309
+ "Render Actions buttons in 50% cells: one button uses the left half, two buttons fill one row, and larger groups continue in two columns.",
265
310
  );
266
311
  expect(contract.aiUsageRules).toContain(
267
- "When an Actions button group no longer fits beside its label and wraps to a second row, align the wrapped button row to the left.",
312
+ "Do not stretch an odd trailing Actions button full-width.",
268
313
  );
269
314
  expect(contract.aiUsageRules).toContain(
270
315
  'For local reset-like actions, use product-specific values such as "reset-current-layer" or "reset-palette" and handle them through ToolcraftApp onPanelAction; do not use a bare "reset" value unless the action intentionally runs controls.reset.',
@@ -328,6 +373,12 @@ describe("Toolcraft template component contracts", () => {
328
373
  expect(contract.aiUsageRules).toContain(
329
374
  "Do not use Palette when opacity belongs to the same color entity; use ColorOpacity instead.",
330
375
  );
376
+ expect(contract.decisionCatalog?.requiredAcceptance).toContain(
377
+ "Prove Palette family and shade selections update runtime state immediately, before delayed persistence/commit settles.",
378
+ );
379
+ expect(contract.aiUsageRules).toContain(
380
+ "Palette is a live control like Color and Slider: family and shade changes must update runtime state immediately so the next canvas interaction uses the selected token without waiting for delayed commit or persistence timers.",
381
+ );
331
382
  expect(contract.aiUsageRules).toContain(
332
383
  "Do not use Palette for gradients or color transitions; use Gradient instead.",
333
384
  );
@@ -369,27 +420,37 @@ describe("Toolcraft template component contracts", () => {
369
420
  expect(contract.commands).toContain("canvas.panBy");
370
421
  expect(contract.commands).toContain("canvas.setOffset");
371
422
  expect(contract.commands).toContain("media.import");
423
+ expect(contract.commands).toContain("media.transform");
372
424
  expect(contract.capabilities).toContain("editable-size");
373
425
  expect(contract.aiUsageRules).toContain(
374
- "Use intrinsic-media for single-layer upload/generation apps so imported media natural size becomes canvas.size.",
426
+ "Do not let canvas.upload choose intrinsic-media by default. Upload without explicit sizing resolves to editable-output so source/background images do not own product output size.",
427
+ );
428
+ expect(contract.aiUsageRules).toContain(
429
+ "Use intrinsic-media only for true media-viewer or source-native apps where the natural uploaded/generated media size is the product output; record the reason and prove it with intrinsic-media-size acceptance.",
375
430
  );
376
431
  expect(contract.aiUsageRules).toContain(
377
- "Use editable-output by default for generated, exportable, shader, poster, badge, wall, banner, thumbnail, and product-output apps where users should see or edit width and height.",
432
+ "Use editable-output for generated, exportable, shader, poster, badge, wall, banner, thumbnail, procedural, reference-clone, and product-output apps so users always see Aspect ratio, Canvas width, and Canvas height.",
378
433
  );
379
434
  expect(contract.aiUsageRules).toContain(
380
- "A user-provided base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output unless the prompt or reference explicitly locks output dimensions.",
435
+ "When an uploaded image is a background/source inside the product canvas, keep the current canvas.size, keep Setup/canvas controls visible, and render the image as cover/crop inside the current canvas bounds without letterbox or aspect distortion.",
381
436
  );
382
437
  expect(contract.aiUsageRules).toContain(
383
- "Use fixed-output only when the product output size must not be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.",
438
+ "A user-provided, reference, fixed-format, or base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output so the size is an initial value, not a hidden lock.",
384
439
  );
385
440
  expect(contract.aiUsageRules).toContain(
386
- "Resolved canvas.size exists for every canvas app, but visible Canvas width and Canvas height controls are mandatory only for editable-output sizing and do not depend on settingsTransfer.",
441
+ "Do not use fixed-output for generated product/output apps with export actions. Reserve fixed-output for non-product internal fixtures where width and height truly must never be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.",
442
+ );
443
+ expect(contract.aiUsageRules).toContain(
444
+ "A reference or previous app lacking a size editor, or defining a fixed-size baseline, is not a fixed-output reason for a generated product app; product-output clones still use editable-output.",
445
+ );
446
+ expect(contract.aiUsageRules).toContain(
447
+ "Resolved canvas.size exists for every canvas app, but visible Canvas width and Canvas height controls are mandatory only for editable-output sizing and live in the mandatory runtime Setup section.",
387
448
  );
388
449
  expect(contract.aiUsageRules).toContain(
389
450
  "If canvas.size is provided without an explicit sizing mode, defineToolcraft treats it as editable-output and adds Canvas width and Canvas height controls.",
390
451
  );
391
452
  expect(contract.aiUsageRules).toContain(
392
- "The runtime Canvas width and Canvas height block uses the technical Setup section and renders without a visible section heading; do not add a separate Canvas section label above these fields.",
453
+ "The runtime Canvas width and Canvas height block uses the mandatory headerless Setup controls block; do not add a separate Canvas section label above these fields.",
393
454
  );
394
455
  expect(contract.aiUsageRules).toContain(
395
456
  "When the user manually edits Canvas width or Canvas height, the runtime keeps the typed dimension, keeps the other dimension unchanged, switches Aspect ratio to Custom, and shows the reduced current ratio in the custom ratio inputs.",
@@ -398,7 +459,7 @@ describe("Toolcraft template component contracts", () => {
398
459
  "Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
399
460
  );
400
461
  expect(contract.aiUsageRules).toContain(
401
- "For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1x to 2x without changing visible canvas size, and adding/enabling it requires targeted browser evidence that the canvas stays responsive at the selected scale.",
462
+ "For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1 to 2 without changing visible canvas size, and adding/enabling it requires targeted browser evidence that the canvas stays responsive at the selected scale.",
402
463
  );
403
464
  expect(contract.aiUsageRules).toContain(
404
465
  "After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
@@ -422,7 +483,10 @@ describe("Toolcraft template component contracts", () => {
422
483
  "Use schema persistence policy for app state that should survive reload.",
423
484
  );
424
485
  expect(contract.aiUsageRules).toContain(
425
- "Persistence may include values, canvas, panels, timeline, and layers; history and media blobs are not persisted.",
486
+ "Persistence may include values, canvas, panels, timeline, layers, and media; history is not persisted.",
487
+ );
488
+ expect(contract.aiUsageRules).toContain(
489
+ "Use persistence include: [\"media\"] only when runtime media state must survive reload, such as predefined attached files that users can delete, reorder, or transform. Do not use ad hoc storage for media state.",
426
490
  );
427
491
  expect(contract.aiUsageRules).toContain(
428
492
  'Apps with visible runtime panels and localStorage persistence must include "panels" so dragged panel positions survive reload.',
@@ -431,32 +495,35 @@ describe("Toolcraft template component contracts", () => {
431
495
  'Apps with localStorage persistence must include acceptance coverage for changing a user setting, reloading the browser page, and seeing the restored value or product output.',
432
496
  );
433
497
  expect(contract.aiUsageRules).toContain(
434
- "Settings import/export is a preset transfer feature for complex apps; it must not be used to hide or replace broken persistence reload behavior.",
498
+ "Settings import/export is a mandatory runtime preset transfer feature; it must not be used to hide or replace broken persistence reload behavior.",
435
499
  );
436
500
  });
437
501
 
438
- it("documents settings transfer as a runtime-owned complex-app feature", () => {
502
+ it("documents settings transfer as a mandatory runtime-owned feature", () => {
439
503
  const contract = getToolcraftComponentContract("settingsTransfer");
440
504
 
441
505
  expect(contract.kind).toBe("settings");
442
506
  expect(contract.stateMode).toBe("runtime-owned");
443
507
  expect(contract.aiUsageRules).toContain(
444
- 'Use schema settingsTransfer: "auto" for complex apps unless the prompt explicitly disables settings import/export.',
508
+ "Generated apps keep a controls panel so runtime Setup is visible from the first run; product controls are added after that mandatory runtime section.",
445
509
  );
446
510
  expect(contract.aiUsageRules).toContain(
447
- "After adding, removing, or reorganizing controls, sections, timeline, or layers, recalculate settings-transfer eligibility. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18.",
511
+ "Do not gate Export Settings / Import Settings behind complexity thresholds, app size, or prompt wording.",
448
512
  );
449
513
  expect(contract.aiUsageRules).toContain(
450
514
  "Do not hand-roll settings import/export through app routes, hidden file inputs, or panelActions.",
451
515
  );
452
516
  expect(contract.aiUsageRules).toContain(
453
- "Settings transfer appears as the first technical Setup controls-panel section when enabled and renders without a visible section heading; it imports and exports control values, canvas size, and timeline state.",
517
+ "Settings transfer appears in the first visible headerless Setup controls-panel block; it imports and exports control values, canvas size, and timeline state.",
518
+ );
519
+ expect(contract.aiUsageRules).toContain(
520
+ "App-authored sections must not declare runtime Setup targets such as runtime.settingsTransfer, canvas.aspectRatio, canvas.size.width, canvas.size.height, canvas.renderScale, or panels.timeline.extended; those controls never suppress the mandatory runtime Setup controls.",
454
521
  );
455
522
  expect(contract.aiUsageRules).toContain(
456
- "A settings-transfer section with only Export Settings and Import Settings means canvas sizing is not editable-output or canvas size controls already exist elsewhere.",
523
+ "When editable-output canvas sizing is enabled, the first visible Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, and optional Resolution scale in that order.",
457
524
  );
458
525
  expect(contract.aiUsageRules).toContain(
459
- "When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, and optional Resolution scale in that order and renders without a visible section heading.",
526
+ "When panels.timeline is enabled, Setup appends the Timeline switch as the last control; when panels.timeline is omitted, the Timeline switch must not appear.",
460
527
  );
461
528
  });
462
529
 
@@ -464,6 +531,24 @@ describe("Toolcraft template component contracts", () => {
464
531
  const slider = getToolcraftComponentContract("slider");
465
532
  const rangeSlider = getToolcraftComponentContract("rangeSlider");
466
533
 
534
+ expect(slider.decisionCatalog.requiredAcceptance).toContain(
535
+ "Prove dragging the slider changes product output or the intended runtime side effect while the drag is in progress, not only after pointer release, blur, Apply, or a final commit.",
536
+ );
537
+ expect(slider.aiUsageRules).toContain(
538
+ "Sliders are live canvas controls: dragging must update runtime state and product output in real time by default.",
539
+ );
540
+ expect(slider.aiUsageRules).toContain(
541
+ "Do not implement slider values as deferred local drafts, Apply-only updates, pointer-up-only commits, or renderer changes that appear only after the user asks again.",
542
+ );
543
+ expect(slider.aiUsageRules).toContain(
544
+ "Slider performance coverage must use a real control-drag scenario; control-change coverage is not enough to prove live canvas feedback or drag smoothness.",
545
+ );
546
+ expect(slider.aiUsageRules).toContain(
547
+ "If a live slider causes jank, optimize the renderer path first: update uniforms or stable buffers, cache expensive inputs, coalesce preview work to requestAnimationFrame, cancel stale async renders, move heavy work off React, or switch renderer strategy.",
548
+ );
549
+ expect(slider.aiUsageRules).toContain(
550
+ "Only in an extreme documented performance ceiling may a slider use a degraded live preview or delayed heavy refinement; the user must still see immediate canvas feedback while dragging and the worklog must record the measured reason.",
551
+ );
467
552
  expect(slider.aiUsageRules).toContain(
468
553
  "Slider step means numeric snapping only; it does not make the slider visually discrete by itself.",
469
554
  );
@@ -480,16 +565,19 @@ describe("Toolcraft template component contracts", () => {
480
565
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
481
566
  );
482
567
  expect(slider.aiUsageRules).toContain(
483
- "Use slider unit only for measurement or scale suffixes such as %, px, °, x, s, ms, fps, rows/cols, or similar domain units.",
568
+ "Use slider unit only for real measurement suffixes such as %, px, °, s, ms, fps, rows/cols, or similar domain units.",
484
569
  );
485
570
  expect(slider.aiUsageRules).toContain(
486
571
  "Do not use unit for repeated entity nouns already named by the section or label, such as Letters + letters, Shape Density / Count + shapes, Words + words, Symbols + symbols, Items + items, Particles + particles, or Layers + layers.",
487
572
  );
573
+ expect(slider.aiUsageRules).toContain(
574
+ 'Do not use unit: "x"; scale, multiplier, intensity, opacity, strength, depth, and shader amount sliders display plain numbers unless a real measurement unit applies.',
575
+ );
488
576
  expect(slider.aiUsageRules).toContain(
489
577
  "When the value needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
490
578
  );
491
579
  expect(slider.aiUsageRules).toContain(
492
- "Compact symbol/CSS units render tight, such as 70%, 24px, 1.2x, and 8s; word units render with a space, such as 5 cols, when they are truly needed.",
580
+ "Compact symbol/CSS units render tight, such as 70%, 24px, and 8s; word units render with a space, such as 5 cols, when they are truly needed.",
493
581
  );
494
582
  expect(slider.aiUsageRules).toContain(
495
583
  "Slider valueLabel is editable only when it contains a numeric value; textual state labels such as Normal are display-only and must not expose hover or click editing affordances.",
@@ -507,13 +595,31 @@ describe("Toolcraft template component contracts", () => {
507
595
  "Visual discrete sliders must still drag smoothly; browser performance tests should use expectToolcraftDiscreteSliderDragSmoothness for real pointer drag.",
508
596
  );
509
597
  expect(slider.aiUsageRules).toContain(
510
- "Use schema disabled: true for unavailable sliders; do not recreate a disabled-looking slider with custom markup.",
598
+ "Use visibleWhen for sliders that are meaningful only in some mode/type/source/include/count states; inactive branches should disappear so the panel shows only controls usable in the current state.",
511
599
  );
512
600
  expect(slider.aiUsageRules).toContain(
513
- "Use disabledWhen for sliders that are only meaningful in some mode values, such as Fill level and Islands being disabled when Fill mode is Full.",
601
+ "Do not use schema disabled: true or disabledWhen for product sliders; product panels should show only controls usable in the current state. Use visibleWhen for unavailable product states instead of rendering disabled controls.",
514
602
  );
515
603
  expect(slider.aiUsageRules).toContain(
516
- "Do not leave a mode-dependent slider active while making the renderer ignore it; the UI must expose the unavailable state.",
604
+ "Do not leave an inactive conditional slider visible while making the renderer ignore it; hide it with visibleWhen.",
605
+ );
606
+ expect(rangeSlider.decisionCatalog.requiredAcceptance).toContain(
607
+ "Prove dragging rangeSlider.lower and rangeSlider.upper both affect product output while the drag is in progress, not only after pointer release, blur, Apply, or a final commit.",
608
+ );
609
+ expect(rangeSlider.aiUsageRules).toContain(
610
+ "Range sliders are live canvas controls: dragging either thumb must update runtime state and product output in real time by default.",
611
+ );
612
+ expect(rangeSlider.aiUsageRules).toContain(
613
+ "Do not implement range slider values as deferred local drafts, Apply-only updates, pointer-up-only commits, or renderer changes that appear only after the user asks again.",
614
+ );
615
+ expect(rangeSlider.aiUsageRules).toContain(
616
+ "Range slider performance coverage must use a real control-drag scenario; control-change coverage is not enough to prove live canvas feedback or drag smoothness.",
617
+ );
618
+ expect(rangeSlider.aiUsageRules).toContain(
619
+ "If a live range slider causes jank, optimize the renderer path first: update uniforms or stable buffers, cache expensive inputs, coalesce preview work to requestAnimationFrame, cancel stale async renders, move heavy work off React, or switch renderer strategy.",
620
+ );
621
+ expect(rangeSlider.aiUsageRules).toContain(
622
+ "Only in an extreme documented performance ceiling may a range slider use a degraded live preview or delayed heavy refinement; the user must still see immediate canvas feedback while dragging and the worklog must record the measured reason.",
517
623
  );
518
624
  expect(rangeSlider.aiUsageRules).toContain(
519
625
  "Range slider step means numeric snapping only; it does not make the range slider visually discrete by itself.",
@@ -531,7 +637,7 @@ describe("Toolcraft template component contracts", () => {
531
637
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
532
638
  );
533
639
  expect(rangeSlider.aiUsageRules).toContain(
534
- "Use rangeSlider unit only for measurement or scale suffixes; do not use it for repeated entity nouns already named by the section or label.",
640
+ "Use rangeSlider unit only for real measurement suffixes; do not use it for repeated entity nouns already named by the section or label, and do not use x as a unit.",
535
641
  );
536
642
  expect(rangeSlider.aiUsageRules).toContain(
537
643
  "When a range label needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
@@ -552,13 +658,13 @@ describe("Toolcraft template component contracts", () => {
552
658
  "Visual discrete sliders must still drag smoothly; browser performance tests should use expectToolcraftDiscreteSliderDragSmoothness for real pointer drag.",
553
659
  );
554
660
  expect(rangeSlider.aiUsageRules).toContain(
555
- "Use schema disabled: true for unavailable range sliders; do not recreate a disabled-looking range slider with custom markup.",
661
+ "Use visibleWhen for range sliders that are meaningful only in some mode/type/source/include/count states; inactive branches should disappear so the panel shows only controls usable in the current state.",
556
662
  );
557
663
  expect(rangeSlider.aiUsageRules).toContain(
558
- "Use disabledWhen for range sliders that are only meaningful in some mode values; keep the value so it returns when the mode becomes relevant again.",
664
+ "Do not use schema disabled: true or disabledWhen for product range sliders; product panels should show only controls usable in the current state. Use visibleWhen for unavailable product states instead of rendering disabled controls.",
559
665
  );
560
666
  expect(rangeSlider.aiUsageRules).toContain(
561
- "Do not leave a mode-dependent range slider active while making the renderer ignore it; the UI must expose the unavailable state.",
667
+ "Do not leave an inactive conditional range slider visible while making the renderer ignore it; hide it with visibleWhen.",
562
668
  );
563
669
  expect(rangeSlider.aiUsageRules).toContain(
564
670
  "Acceptance must prove both rangeSlider.lower and rangeSlider.upper change the product output; testing one handle is not enough.",
@@ -590,7 +696,7 @@ describe("Toolcraft template component contracts", () => {
590
696
  "When one short numeric/text field and one Color field configure the same entity, keep them in one two-column inline layout group.",
591
697
  );
592
698
  expect(color.aiUsageRules).toContain(
593
- 'Mixed inline rows require visible labels on both controls. The required Background row is the section-title-owned exception: use the Switch label "Include" and set the background Color control label to false. Palette variation color banks are the other exception when the group/section label already names the color bank.',
699
+ 'Mixed inline rows usually require visible labels on both controls. Toggle-plus-parameter rows are the section-owned exception: keep the Switch/Checkbox label visible and set the non-toggle parameter label to false; if the parameter label is needed, stack the controls instead. All 50/50 inline rows use the same horizontal column gap as paired Select controls. The required Background row uses the Switch label "Include" and sets the background Color control label to false. Palette variation color banks are the other exception when the group/section label already names the color bank.',
594
700
  );
595
701
  expect(color.aiUsageRules).toContain(
596
702
  "Plain Color popovers must not show opacity controls. If opacity is editable, use ColorOpacity instead.",
@@ -728,6 +834,9 @@ describe("Toolcraft template component contracts", () => {
728
834
  expect(contract.aiUsageRules).toContain(
729
835
  "Every app entity introduced by the AI must have an acceptance test that proves its product responsibility.",
730
836
  );
837
+ expect(contract.aiUsageRules).toContain(
838
+ "Any supplied video, GIF, screen-recording, contact-sheet, or extracted-frame reference requires a Video Reference Study before implementation: storyboard frames, frame-to-frame transition analysis, behavior decomposition, and acceptance mapping. Do not implement video references from a single screenshot or high-level summary.",
839
+ );
731
840
  expect(contract.aiUsageRules).toContain(
732
841
  "Compound control browser tests must explicitly exercise each required value part, not only one visible sub-control.",
733
842
  );
@@ -747,16 +856,19 @@ describe("Toolcraft template component contracts", () => {
747
856
  "Conditional entities require fixtures that make the condition observable.",
748
857
  );
749
858
  expect(contract.aiUsageRules).toContain(
750
- "Use visibleWhen for mode-, type-, variant-, or count-exclusive sections or controls that do not belong to the current selected state.",
859
+ "Use visibleWhen for mode-, type-, source-, include-, variant-, or count-exclusive sections or controls that do not belong to the current selected state.",
751
860
  );
752
861
  expect(contract.aiUsageRules).toContain(
753
862
  "When a count/quantity control determines how many sibling controls are available, hide unavailable siblings with visibleWhen; do not render all possible controls while the renderer reads only the first N.",
754
863
  );
755
864
  expect(contract.aiUsageRules).toContain(
756
- "Use disabledWhen for controls that belong to the current entity but are temporarily unavailable for the selected mode; the disabled value must be preserved.",
865
+ "If a switch/select/segmented/imagePicker/checkbox chooses a branch for the same product entity, controls outside the current branch use visibleWhen, not disabledWhen.",
866
+ );
867
+ expect(contract.aiUsageRules).toContain(
868
+ "Do not use schema disabled: true or disabledWhen for generated product controls; the panel should show only controls usable in the current state. Runtime primitives may still have disabled styling internally, but app schemas should model product availability with visibleWhen.",
757
869
  );
758
870
  expect(contract.aiUsageRules).toContain(
759
- "Do not leave inactive conditional controls visible and enabled while making the renderer ignore them.",
871
+ "Do not leave inactive conditional controls visible while making the renderer ignore them.",
760
872
  );
761
873
  });
762
874
 
@@ -792,7 +904,7 @@ describe("Toolcraft template component contracts", () => {
792
904
  "Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
793
905
  );
794
906
  expect(contract.aiUsageRules).toContain(
795
- "A full performance checkpoint must run with pnpm verify:perf only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance.",
907
+ "A full performance checkpoint must run only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance; use the agent-controlled browser first and pnpm verify:perf only as the Playwright fallback.",
796
908
  );
797
909
  expect(contract.aiUsageRules).toContain(
798
910
  "Renderer, canvas, animation, export, timeline, layers, canvas.renderScale, bug fixes, and performance-sensitive control changes use targeted functional/browser checks first and targeted performance scenarios only for touched workload, viewport, or export paths.",
@@ -917,12 +1029,21 @@ describe("Toolcraft template component contracts", () => {
917
1029
  expect(contract.aiUsageRules).toContain(
918
1030
  "Reference clone specs must list every detected transport behavior explicitly, including pause-resume, restart, time-progress, export-at-time, playback, scrub, duration, loop, and keyframes when present.",
919
1031
  );
1032
+ expect(contract.aiUsageRules).toContain(
1033
+ 'Toolcraft reference timelines must declare referenceTimeline.loopDuration with source "reference", "user-request", or "product-derived", plus seconds and evidence; runtime/template fallback 8s is not a valid source.',
1034
+ );
1035
+ expect(contract.aiUsageRules).toContain(
1036
+ "panels.timeline.defaultDurationSeconds must match referenceTimeline.loopDuration.seconds for referenceTimeline.mode toolcraft-playback/toolcraft-keyframes.",
1037
+ );
920
1038
  expect(contract.aiUsageRules).toContain(
921
1039
  'Do not downgrade custom reference timelines to panels.timeline mode "playback". State buttons, trim handles, selected-range playback, or range export require referenceTimeline.mode "custom-reference-timeline" and dedicated acceptance.',
922
1040
  );
923
1041
  expect(contract.aiUsageRules).toContain(
924
1042
  "Reference clone acceptance must include referenceCoverage rows for canvas sizing, control mapping, renderer state, and any renderer loop, spawn/update cadence, pause/resume, export/copy, or media lifecycle behavior in the reference.",
925
1043
  );
1044
+ expect(contract.aiUsageRules).toContain(
1045
+ "Video reference assets used in a reference clone also require starterTransferMode.videoReferenceStudy with storyboard frames, frame-to-frame transition analysis, behavior decomposition, and acceptance mapping.",
1046
+ );
926
1047
  });
927
1048
 
928
1049
  it("documents concise control label rules", () => {
@@ -949,22 +1070,22 @@ describe("Toolcraft template component contracts", () => {
949
1070
  "Controls-panel sections should stay discrete: two to seven product controls is the normal size, and larger sections must split by product sub-entity or workflow stage.",
950
1071
  );
951
1072
  expect(contract.aiUsageRules).toContain(
952
- "Every app-authored controls-panel body section must have a short meaningful visible title. Runtime-created setup/settings sections use the technical title Setup but render without a visible heading; sticky footer action sections use the technical title Export but render without a visible heading.",
1073
+ "Every app-authored controls-panel body section must have a short meaningful visible title. Runtime-created Setup renders as the first visible headerless controls block with no title, reset action, collapse button, or collapsed state; sticky footer action sections use the technical title Export but render without a visible heading.",
953
1074
  );
954
1075
  expect(contract.aiUsageRules).toContain(
955
- "Every visible controls-panel section title renders through the standard 36px collapsible header row with vertically centered text and the runtime collapse icon; generated apps must not hand-build section headers.",
1076
+ "Every visible app-authored controls-panel section title renders through the standard 36px collapsible header row with vertically centered text and the runtime collapse icon; generated apps must not hand-build section headers.",
956
1077
  );
957
1078
  expect(contract.aiUsageRules).toContain(
958
1079
  "Controls-panel section expand and collapse uses the standard runtime height/opacity animation; generated apps must not replace it with instant custom section visibility.",
959
1080
  );
960
1081
  expect(contract.aiUsageRules).toContain(
961
- "Ordinary controls-panel section collapsed/expanded state persists as a runtime UI preference per app. It is not undo/redo state, not settings import/export state, and Reset controls must not clear it. Runtime technical Setup/settings sections and sticky footer Export sections are not collapsible.",
1082
+ "Controls-panel section collapsed/expanded state persists as a runtime UI preference per app. It is not undo/redo state, not settings import/export state, and Reset controls must not clear it. Runtime Setup is not collapsible; sticky footer Export sections are not collapsible.",
962
1083
  );
963
1084
  expect(contract.aiUsageRules).toContain(
964
1085
  "Ordinary controls-panel section headers expose the runtime section reset action before the collapse button; it dispatches controls.resetTargets and restores only that section's control targets to their schema defaultValue.",
965
1086
  );
966
1087
  expect(contract.aiUsageRules).toContain(
967
- "Ordinary controls-panel body sections use 8px top spacing and 24px bottom spacing for their control content. Runtime technical Setup/settings sections use 12px top and bottom spacing to match side padding. Sticky footer action sections keep their dedicated spacing.",
1088
+ "Runtime Setup and ordinary controls-panel body sections use 8px top spacing and 24px bottom spacing for their control content. Sticky footer action sections keep their dedicated spacing.",
968
1089
  );
969
1090
  expect(contract.aiUsageRules).toContain(
970
1091
  "Broad section titles such as Flow, Icon, Shapes, Scene, Text, Typography, or Motion are only valid for small cohesive groups; use specific titles such as Flow Motion, Flow Geometry, Letter Burst, Shape Colors, Logo Glow, Logo Plate, or Text Block for larger groups.",
@@ -1054,21 +1175,51 @@ describe("Toolcraft template component contracts", () => {
1054
1175
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.commands).toContain(
1055
1176
  "timeline.togglePlayback",
1056
1177
  );
1178
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.commands).toContain(
1179
+ "panels.setHidden",
1180
+ );
1057
1181
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.capabilities).toContain("keyframes");
1058
1182
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1059
- "Do not enable the timeline panel just because a renderer is animated.",
1183
+ "Any product output animation must enable the top Toolcraft timeline; do not ship animated product output with only local requestAnimationFrame playback.",
1060
1184
  );
1061
1185
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1062
1186
  "Before choosing no timeline for any animated product, write an Animation Intent Inventory: product transport, editable keyframes, or autonomous decorative output, plus the user-facing time behaviors present or intentionally absent.",
1063
1187
  );
1064
1188
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1065
- 'User-requested product animation defaults to panels.timeline mode "playback" unless the spec explicitly declares autonomous decorative/self-running output with no play, pause, scrub, duration, loop, or export-at-time behavior.',
1189
+ 'User-requested product animation defaults to panels.timeline mode "playback" unless the spec explicitly declares autonomous decorative/self-running output with no play, pause, scrub, duration, loop, export-at-time behavior, or video export.',
1190
+ );
1191
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1192
+ 'Any product app with Export Video must enable the top Toolcraft timeline: use panels.timeline mode "playback" for product animation transport, or mode "keyframes" when exported animation is driven by keyframes.',
1193
+ );
1194
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1195
+ "Video export renderers must render deterministic frames from runtime timeline timestamps and duration; do not treat autonomous wall-clock time or captureStream recording time as the source of product duration.",
1066
1196
  );
1067
1197
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1068
1198
  "Playback renderers must consume runtime timeline state; pause freezes output, scrubbing renders a deterministic frame, and the full animation cycle maps to state.timeline.durationSeconds instead of a local fixed duration.",
1069
1199
  );
1070
1200
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1071
- "Playback renderers may compute an initial default duration, but must not watch state.timeline.durationSeconds and dispatch timeline.setDuration back to a computed local duration. User-edited timeline duration is the source of truth after initialization or reset.",
1201
+ 'When panels.timeline is enabled for a new Toolcraft app, appTransferMode.animationIntent must match it: mode "timeline-playback" for playback, or mode "timeline-keyframes" for keyframes.',
1202
+ );
1203
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1204
+ "When the product has a known loop duration, declare it as panels.timeline.defaultDurationSeconds; the runtime timeline duration starts from that loop duration instead of an unrelated 8s default.",
1205
+ );
1206
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1207
+ 'Timeline animation intent must declare loopDuration with source "reference", "user-request", or "product-derived", plus seconds and evidence; runtime/template fallback 8s is not a valid source.',
1208
+ );
1209
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1210
+ "panels.timeline.defaultDurationSeconds must match animationIntent.loopDuration.seconds for playback/keyframe animation, so the initial timeline UI shows the declared product loop instead of a generic default.",
1211
+ );
1212
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1213
+ "Playback renderers may compute an initial loop duration default during app initialization or reset, but must not watch state.timeline.durationSeconds and dispatch timeline.setDuration back to a computed local duration. User-edited timeline duration becomes the loop duration source of truth after initialization or reset.",
1214
+ );
1215
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1216
+ "Playback renderers should use getToolcraftTimelineLoopTime or getToolcraftTimelineLoopProgress to derive product loop phase from state.timeline.currentTimeSeconds and state.timeline.durationSeconds; do not hand-roll wall-clock, fixed-duration, mirror, yoyo, ping-pong, or reverse phase math.",
1217
+ );
1218
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1219
+ "Product animation loop means a seamless forward-only cycle by default: motion advances in one direction, first and last frames stitch without a visible jump, and mirror/yoyo/ping-pong/reverse loops require explicit user request.",
1220
+ );
1221
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1222
+ "Changing timeline duration must preserve seamless forward-loop semantics: one full product animation cycle maps from 0 to state.timeline.durationSeconds, first and last frames still stitch, direction does not reverse, and changing duration must not switch the renderer to wall-clock or fixed local speed.",
1072
1223
  );
1073
1224
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1074
1225
  "When non-looping playback reaches the end, pressing Play again restarts playback from time 0.",
@@ -1112,6 +1263,12 @@ describe("Toolcraft template component contracts", () => {
1112
1263
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1113
1264
  'Custom timeline UI is allowed only for explicit referenceTimeline.mode "custom-reference-timeline" transfers with browser-backed referenceTimelineCoverage.',
1114
1265
  );
1266
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1267
+ 'When panels.timeline is enabled, runtime inserts a Setup switch labeled "Timeline" as the last runtime setup control after Resolution scale.',
1268
+ );
1269
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1270
+ "The Timeline Setup switch controls only compact versus extended runtime presentation: off shows the compact Play-only transport, on shows the extended timeline with scrubber, duration, loop, and keyframe UI. It must not stop playback, change keyframes, affect export, enter product values, or be reset by Reset controls/settings transfer.",
1271
+ );
1115
1272
  expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
1116
1273
  "If timeline verification fails, wire the renderer to runtime timeline state or remove panels.timeline.",
1117
1274
  );
@@ -1140,6 +1297,9 @@ describe("Toolcraft template component contracts", () => {
1140
1297
  expect(contract.aiUsageRules).toContain(
1141
1298
  "Product-output apps must always include export in panelActions.",
1142
1299
  );
1300
+ expect(contract.aiUsageRules).toContain(
1301
+ 'Export-labeled panelActions use icon "upload-simple", matching the Setup "Export Settings" action; do not use "download", "download-simple", or "export" icons for Export PNG or Export Video.',
1302
+ );
1143
1303
  expect(contract.aiUsageRules).toContain(
1144
1304
  "Static or still-output apps include Export PNG as the primary footer action.",
1145
1305
  );
@@ -1161,6 +1321,9 @@ describe("Toolcraft template component contracts", () => {
1161
1321
  expect(contract.aiUsageRules).toContain(
1162
1322
  "Animated apps include Export Video as the primary footer action and Export PNG as a secondary footer action.",
1163
1323
  );
1324
+ expect(contract.aiUsageRules).toContain(
1325
+ "Any app with Export Video must enable the top Toolcraft timeline; video duration, loop, and rendered timestamps come from runtime timeline state.",
1326
+ );
1164
1327
  expect(contract.aiUsageRules).toContain(
1165
1328
  'Animated apps with Export Video must expose a separate "Video Export" controls section.',
1166
1329
  );
@@ -1191,6 +1354,12 @@ describe("Toolcraft template component contracts", () => {
1191
1354
  expect(contract.aiUsageRules).toContain(
1192
1355
  "Video export code must choose the actual MIME/container through MediaRecorder.isTypeSupported or an equivalent encoder capability check, then fall back safely.",
1193
1356
  );
1357
+ expect(contract.aiUsageRules).toContain(
1358
+ "Video export must use getToolcraftVideoExportSize for current and 4K dimensions. Current video export uses the current canvas/output size with even encoder-safe rounding; 4K video fits inside an encoder-safe 3840x2160 box, preserves canvas aspect ratio, and uses even pixel dimensions. Do not hand-roll 4096px long-edge video sizing.",
1359
+ );
1360
+ expect(contract.aiUsageRules).toContain(
1361
+ "Video export must set recording canvas dimensions before captureStream, MediaRecorder, VideoEncoder, or equivalent encoder setup, and must reject recorder/encoder errors instead of returning corrupt blobs.",
1362
+ );
1194
1363
  expect(contract.aiUsageRules).toContain(
1195
1364
  "Offline video export duration must be encoded from runtime timeline timestamps. Do not rely on canvas.captureStream plus MediaRecorder wall-clock recording time as the only duration mechanism for rendered-frame export.",
1196
1365
  );
@@ -1213,7 +1382,7 @@ describe("Toolcraft template component contracts", () => {
1213
1382
  "PNG export must use createToolcraftPngExportCanvas so background transparency and selected image dimensions or retina fallback are applied consistently; turning Include off makes preview product background and PNG alpha transparent without hiding the Toolcraft canvas backing or video background.",
1214
1383
  );
1215
1384
  expect(contract.aiUsageRules).toContain(
1216
- "Video export must keep product background and use getToolcraftRetinaExportSize for retina dimensions.",
1385
+ "Video export must keep product background and use getToolcraftVideoExportSize for current/4K output dimensions.",
1217
1386
  );
1218
1387
  expect(contract.aiUsageRules).toContain(
1219
1388
  "Copy PNG can be a secondary action when clipboard output is useful, but copy does not replace export.",
@@ -1235,18 +1404,41 @@ describe("Toolcraft template component contracts", () => {
1235
1404
  it("documents file upload ownership across single-layer and multi-layer apps", () => {
1236
1405
  const contract = getToolcraftComponentContract("fileDrop");
1237
1406
 
1238
- expect(contract.commands).toEqual(["media.delete", "media.import", "media.reorder"]);
1407
+ expect(contract.commands).toEqual([
1408
+ "media.delete",
1409
+ "media.import",
1410
+ "media.reorder",
1411
+ "media.transform",
1412
+ ]);
1239
1413
  expect(contract.aiUsageRules).toContain(
1240
1414
  'Use fileDrop with assetKind: "image" for image-only source media and assetKind: "file" for arbitrary uploaded files.',
1241
1415
  );
1416
+ expect(contract.aiUsageRules).toContain(
1417
+ "If an app ships with predefined source files or background images, declare them as schema media.defaultAssets with sourceTarget matching the fileDrop control. They must render as ordinary attached files in fileDrop, not as hidden renderer constants or canvas placeholder artwork.",
1418
+ );
1419
+ expect(contract.aiUsageRules).toContain(
1420
+ "Predefined media files are default runtime state: users can remove them to get an empty source/canvas state, persistence may keep that removal with include: [\"media\"], and global or section Reset restores the default attached files.",
1421
+ );
1422
+ expect(contract.aiUsageRules).toContain(
1423
+ "When uploaded/imported content is part of the source-material flow, the canvas must not show agent-invented artwork, CTA text, fake sample output, decorative placeholders, or preset source designs before real content exists; keep the canvas neutral/runtime-backed and put upload affordance in fileDrop.",
1424
+ );
1425
+ expect(contract.aiUsageRules).toContain(
1426
+ "Do not add procedural Source Preset modes only to avoid an empty canvas. A default procedural or reference source is allowed only when the prompt/reference explicitly defines it and the worklog records that evidence.",
1427
+ );
1242
1428
  expect(contract.aiUsageRules).toContain(
1243
1429
  "In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
1244
1430
  );
1431
+ expect(contract.aiUsageRules).toContain(
1432
+ "In image mode, the runtime owns image transform actions: 90° Right, Flip horizontal, and Flip vertical. These actions render through the built-in actions-control in one three-column row with compact visible labels: 90°, Flip H, Flip V; keep a 6px vertical gap between the uploader and action row. Do not create a custom image action button grid. They update runtime mediaAssets transform metadata, and product preview/export must consume that metadata instead of keeping a separate transform state.",
1433
+ );
1434
+ expect(contract.aiUsageRules).toContain(
1435
+ "When exactly one uploaded image is present, image transform actions are visible immediately. When multiple images are present, users select a thumbnail first; no transform actions render until a thumbnail is selected, and the action applies only to that selected image.",
1436
+ );
1245
1437
  expect(contract.aiUsageRules).toContain(
1246
1438
  "In file mode, the runtime shows uploaded files as a sortable list with paperclip icons, file names, remove buttons, and --border/5 separators.",
1247
1439
  );
1248
1440
  expect(contract.aiUsageRules).toContain(
1249
- "In single-layer apps, global Reset controls and section reset must remove uploaded fileDrop source media and return the fileDrop target to defaultValue.",
1441
+ "In single-layer apps, global Reset controls and section reset must restore fileDrop source media to schema media.defaultAssets for that target; when no default asset exists, Reset removes uploaded media and returns the fileDrop target to defaultValue.",
1250
1442
  );
1251
1443
  expect(contract.aiUsageRules).toContain(
1252
1444
  "Use fileDrop with multiple: true when the app needs several uploaded images as one source set; do not build a custom thumbnail uploader for this.",
@@ -1260,6 +1452,9 @@ describe("Toolcraft template component contracts", () => {
1260
1452
  expect(contract.aiUsageRules).toContain(
1261
1453
  "Dragging thumbnails reorders runtime mediaAssets; preview, export, and renderer mapping must consume that media order instead of maintaining a separate product-only order.",
1262
1454
  );
1455
+ expect(contract.aiUsageRules).toContain(
1456
+ "When uploaded images are used as canvas/background source material, draw them with cover/crop behavior: scale proportionally until the current canvas bounds are fully covered, keep the canvas size/settings unchanged, and crop overflow at the canvas bounds.",
1457
+ );
1263
1458
  expect(contract.aiUsageRules).toContain(
1264
1459
  "Do not create custom upload buttons, file lists, or file sorting for generic source uploads when fileDrop can represent the source set.",
1265
1460
  );
@@ -1310,10 +1505,16 @@ describe("Toolcraft template component contracts", () => {
1310
1505
  "CodeTextarea is the multiline text input for any potentially long value, not only source code.",
1311
1506
  );
1312
1507
  expect(contract.aiUsageRules).toContain(
1313
- "Use text for short single-line strings such as names, small numeric values, compact prompts, titles, and short tokens.",
1508
+ "Do not use CodeTextarea for short single-line canvas text, button labels, names, titles, captions, badges, or short tokens; use TextInput.",
1509
+ );
1510
+ expect(contract.aiUsageRules).toContain(
1511
+ "Use text for short single-line strings such as names, button labels, small numeric values, compact prompts, titles, captions, and short tokens.",
1314
1512
  );
1315
1513
  expect(contract.aiUsageRules).toContain(
1316
- "Use code when the user may enter long prompts, multiline text, JSON, CSS, shader code, scripts, templates, or other long structured data.",
1514
+ "Use code only when the user may enter long prompts, multiline text, instructions, JSON, CSS, shader code, scripts, templates, or other long structured data.",
1515
+ );
1516
+ expect(contract.aiUsageRules).toContain(
1517
+ "If CodeTextarea has a short single-line default value, the schema description must make the long, multiline, or structured-content reason explicit.",
1317
1518
  );
1318
1519
  expect(contract.aiUsageRules).toContain(
1319
1520
  "CodeTextarea is a content editor and applies values while typing; do not wait for blur, Enter, or Cmd/Ctrl+Enter to update runtime state.",
@@ -1329,6 +1530,9 @@ describe("Toolcraft template component contracts", () => {
1329
1530
  expect(contract.visualComponent).toBe("TextInput");
1330
1531
  expect(contract.defaultSectionLayout).toBe("grouped");
1331
1532
  expect(contract.labelPolicy).toBe("required");
1533
+ expect(contract.aiUsageRules).toContain(
1534
+ "TextInput owns short single-line product text: button labels, labels on the canvas, names, titles, captions, badges, short tokens, and compact prompts.",
1535
+ );
1332
1536
  expect(contract.aiUsageRules).toContain(
1333
1537
  'TextInput commitMode defaults to "content": text content, prompts, names, tokens, titles, and instructions apply while typing.',
1334
1538
  );