@pixel-point/toolcraft 0.0.7 → 0.0.9

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 (62) hide show
  1. package/package.json +1 -1
  2. package/src/generate.mjs +34 -5
  3. package/src/generate.test.mjs +12 -0
  4. package/src/package-json.mjs +15 -0
  5. package/src/package-json.test.mjs +14 -1
  6. package/templates/runtime/contracts/component-contracts.test.ts +185 -23
  7. package/templates/runtime/contracts/component-contracts.ts +91 -36
  8. package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
  9. package/templates/runtime/contracts/decision-contracts.ts +4 -4
  10. package/templates/runtime/export/export.test.ts +31 -0
  11. package/templates/runtime/export/export.ts +41 -0
  12. package/templates/runtime/react/canvas-shell.test.tsx +77 -1
  13. package/templates/runtime/react/canvas-shell.tsx +178 -23
  14. package/templates/runtime/react/control-conditions.ts +166 -0
  15. package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
  16. package/templates/runtime/react/controls-panel.test.tsx +774 -17
  17. package/templates/runtime/react/controls-panel.tsx +155 -8
  18. package/templates/runtime/react/media-file.ts +19 -0
  19. package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
  20. package/templates/runtime/schema/define-toolcraft.ts +29 -3
  21. package/templates/runtime/schema/types.ts +7 -0
  22. package/templates/runtime/state/reducer.test.ts +304 -0
  23. package/templates/runtime/state/reducer.ts +148 -9
  24. package/templates/runtime/state/types.ts +10 -2
  25. package/templates/runtime/testing/performance.test.ts +1424 -56
  26. package/templates/runtime/testing/performance.ts +710 -43
  27. package/templates/starter/AGENTS.md +10 -9
  28. package/templates/starter/docs/toolcraft/README.md +1 -1
  29. package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
  30. package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
  31. package/templates/starter/docs/toolcraft/component-rules.md +46 -16
  32. package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
  33. package/templates/starter/docs/toolcraft/performance.md +54 -6
  34. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  35. package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
  36. package/templates/starter/docs/toolcraft/workflow.md +2 -2
  37. package/templates/starter/e2e/app-performance.spec.ts +136 -3
  38. package/templates/starter/e2e/performance-helpers.ts +197 -0
  39. package/templates/starter/gitignore +1 -0
  40. package/templates/starter/package.json +5 -0
  41. package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
  42. package/templates/starter/scripts/toolcraft-port.mjs +102 -0
  43. package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
  44. package/templates/starter/src/app/starter-acceptance.test.ts +1288 -105
  45. package/templates/starter/src/app/starter-acceptance.ts +740 -24
  46. package/templates/starter/src/app/starter-performance.test.ts +66 -5
  47. package/templates/ui/components/control-layout/index.tsx +8 -3
  48. package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
  49. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  50. package/templates/ui/components/controls/color/index.ts +4 -1
  51. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  52. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  53. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
  54. package/templates/ui/components/controls/file-drop/index.ts +1 -1
  55. package/templates/ui/components/controls/index.ts +3 -0
  56. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  57. package/templates/ui/components/controls/select/select-control.tsx +9 -4
  58. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  59. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  60. package/templates/ui/components/controls/vector/index.ts +1 -0
  61. package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
  62. package/templates/ui/components/panel/panel-section.tsx +82 -6
@@ -25,7 +25,11 @@ Use workload coverage for controls that change rendering cost:
25
25
 
26
26
  Sensitive controls need min/default/max scenarios, `stressFixture`, and real product-output checks.
27
27
 
28
- `stressFixture` is the machine-checkable heavy case for a workload scenario. It must include:
28
+ `stressFixture` is the machine-checkable value for the scenario itself. For a slider drag, it is the exact slider value to apply. For a text input, it can be the long text value. For preview, zoom, animation, or export stress scenarios, it can be a combined object state.
29
+
30
+ When the scenario control is not itself the whole heavy source, add `workloadFixture` as the independent heavy baseline that must be applied before measuring. Examples: a large uploaded image before dragging an effect slider, long text before dragging a density slider, many items before dragging spacing, or `renderScale: 2` before checking high-frequency controls. `workloadFixture` must be paired with `stressFixture`: first apply the app baseline, then apply the measured scenario value. Do not encode this as a named-control rule; decide from the renderer pipeline and product workload.
31
+
32
+ Both fixture types must include:
29
33
 
30
34
  - `kind`: `large-text`, `large-canvas`, `high-density`, `many-items`, `max-value`, `media`, or `custom`;
31
35
  - `reason`: why this value is the heaviest useful product case;
@@ -33,15 +37,40 @@ Sensitive controls need min/default/max scenarios, `stressFixture`, and real pro
33
37
 
34
38
  For multiline text, prompt, code, JSON, CSS, shader, script, or template workload controls, use `kind: "large-text"`. The fixture must contain at least `50_000` characters and `1_000` lines unless the product has a stricter real-world maximum.
35
39
 
40
+ For uploaded images, videos, source media, and image-processing workloads, use `kind: "media"` with a `value` object containing numeric `width` and `height`. The fixture must be at least `1920x1080`-equivalent, and products that claim high-resolution media or export quality should test a 4K-class source when that is the realistic heavy case. Do not satisfy media import, preview, effect-slider, or image-processing performance with `640x480`, thumbnails, or toy fixtures.
41
+
36
42
  Choose heavy fixtures from this app's real controls, not from generic examples. Use the largest useful product canvas, longest useful text, highest density, largest media, highest item count, fastest animation, highest export quality, or strongest effect setting that the product exposes.
37
43
 
38
- Browser performance tests for workload scenarios must read the heavy value through:
44
+ Browser performance tests for workload and stress scenarios must read the scenario value through:
39
45
 
40
46
  ```ts
41
47
  getToolcraftPerformanceStressValue(appPerformance, "scenario-id")
42
48
  ```
43
49
 
44
- Do not type a separate short value in the Playwright test. If a test uses a toy value while `app-performance.ts` claims a heavy fixture, `pnpm verify:perf` must fail.
50
+ For workload sliders, use `dragToolcraftSliderToPerformanceStressValue(page, label, appPerformance, "scenario-id")` so the test applies the exact numeric value through the real slider min/max range. Do not divide a stress value by the slider max, type a separate short value, or hardcode a ratio in the Playwright test. If a test uses a toy value while `app-performance.ts` claims a heavy fixture, `pnpm verify:perf` must fail.
51
+
52
+ Slider and range slider performance scenarios must preserve live product feedback. The browser test should drag the real thumb and verify the canvas/product output changes during drag. If that drag misses budget, keep the live control semantics and optimize the renderer path first: cache expensive inputs, update uniforms or stable buffers, coalesce preview work to `requestAnimationFrame`, cancel stale async renders, move work off React renders, reuse GPU resources, or change renderer strategy. Do not pass performance by making the slider update only after pointer release or Apply.
53
+
54
+ When a scenario declares `workloadFixture`, apply it first with `getToolcraftPerformanceWorkloadValue` or `applyToolcraftPerformanceWorkloadFixture`, then apply `stressFixture`, then measure. A control-drag scenario that only sets its own slider value while leaving the source media, text, item count, render scale, or dense scene at defaults is invalid.
55
+
56
+ For combined worst cases, put every relevant independent baseline value in `workloadFixture.value` for control scenarios, such as `{ sourceMedia: { width: 3840, height: 2160 }, renderScale: 2 }`, and put the tested control value in `stressFixture.value`. For preview, zoom, drag, animation, or export scenarios that stress the entire state instead of one control, use `stressFixture.value`, such as `{ detail: 96, scale: 0.6, renderScale: 2 }`. Testing workload controls one-by-one is not enough when the product exposes combinations that multiply render cost.
57
+
58
+ Use `kind: "custom"` only for combined object fixtures. Single numeric/string heavy values should use a semantic kind such as `max-value`, `high-density`, `large-canvas`, `many-items`, or `large-text`. Browser tests for custom object fixtures must call:
59
+
60
+ ```ts
61
+ await applyToolcraftPerformanceStressFixture(page, appPerformance, "scenario-id", {
62
+ detail: async (value) => {
63
+ await dragToolcraftSliderToValue(page, "Detail", Number(value));
64
+ },
65
+ scale: async (value) => {
66
+ await dragToolcraftSliderToValue(page, "Scale", Number(value));
67
+ },
68
+ });
69
+ ```
70
+
71
+ The applier object must contain exactly one entry for each key in `stressFixture.value`. Missing keys and stale extra keys are test failures, so every heavy-state part is intentionally mapped through the real UI before measurement.
72
+
73
+ For `workloadFixture.value` object fixtures, use `applyToolcraftPerformanceWorkloadFixture` with the same exact-key rule. `workloadFixture` is the app baseline; `stressFixture` is the action under test.
45
74
 
46
75
  ## Responsiveness Coverage
47
76
 
@@ -64,16 +93,32 @@ Custom renderers should:
64
93
  - initialize contexts, programs, shaders, pipelines, textures, and large buffers once;
65
94
  - update uniforms or stable buffers when controls change;
66
95
  - cache decoded media;
67
- - debounce, coalesce, or defer heavy preview work;
96
+ - coalesce high-frequency preview work and split lightweight live feedback from heavier refinement when needed;
68
97
  - cancel stale async renders;
69
98
  - avoid re-decoding media on every control change;
70
99
  - cancel scheduled frames during cleanup.
71
100
 
101
+ Coalescing may reduce redundant renders during high-frequency slider drags, but it must not make the slider feel deferred or leave the canvas unchanged until release.
102
+
103
+ Custom renderers must declare `rendererPipeline` in `src/app/app-performance.ts`. This is the machine-checkable Render Pipeline Inventory:
104
+
105
+ - every render pass has an `id`, `kind`, `runsOn`, `output`, `quality`, `inputs`, and `invalidatedBy`;
106
+ - cache-sensitive passes such as `decode`, `preprocess`, `pixel-transform`, `text-layout`, `rasterize`, and `composite` include `cacheKey`;
107
+ - `interactionInvalidation` maps controls and high-frequency interactions to the passes they invalidate;
108
+ - animation frames, drag, pan, zoom, timeline playback, timeline scrub, and mask movement must not invalidate upstream decode/preprocess/pixel-transform work unless that runtime target truly changes the upstream result;
109
+ - each `workloadTargets` entry appears in `interactionInvalidation.targets` so tests can prove which control changes renderer cost.
110
+
111
+ Write this inventory before custom renderer code. If the inventory says a slider only updates a shader uniform, the implementation should update a uniform or stable buffer, not rebuild media decode, glyph layout, or raster caches. If the inventory says media import invalidates source decode, browser performance must include a realistic `media-import` scenario.
112
+
72
113
  Pixel-output renderers may use a capped preview pixel budget, but export/copy must render final product output at `state.canvas.size`.
73
114
 
74
115
  Text-output and vector-output previews must preserve native output fidelity. Do not render low-resolution text/vector output into an offscreen canvas and upscale it.
75
116
 
76
- Renderer strategy is not final until the heavy scenarios pass. When stress preview, animation, drag, zoom, or export tests exceed budget, first decide whether the chosen renderer is wrong for this workload. Move heavy work to WebGL/WebGPU, split semantic foreground from heavy backgrounds, cache atlases/buffers, or change the rendering layer model before reducing product quality or relaxing budgets.
117
+ For heavy bitmap-media, shader-like, noise/texture, filters, halftone, mesh, and per-pixel image-processing paths, WebGL/WebGPU is a required candidate before choosing CPU Canvas 2D. Canvas 2D is allowed only when `rendererTechnique.whyNotAlternativeStrategies` or `performanceRisks` records measured stress evidence that the CPU path passes the real media fixture and GPU would not improve the product. A worker can protect the UI thread, but it is not a substitute for GPU acceleration when the workload is fundamentally per-pixel.
118
+
119
+ Renderer strategy is not final until the heavy scenarios pass. When stress preview, animation, drag, zoom, or export tests exceed budget, first decide whether the chosen renderer is wrong for this workload. Move heavy work to WebGL/WebGPU, split semantic foreground from heavy backgrounds, cache atlases/buffers, coalesce high-frequency preview updates, or change the rendering layer model before reducing product quality or relaxing budgets.
120
+
121
+ Do not pass performance by lowering output quality, render scale, canvas backing pixels, export resolution, media fidelity, maximum detail, item count, or animation fidelity. If all reasonable optimizations are exhausted and the app still cannot meet the target budget, document the measured ceiling in the worklog with: the exact stress fixture, attempted optimizations, before/after measurements, why further changes would harm product quality or require a different product scope, and the remaining user-visible risk.
77
122
 
78
123
  ## Required Browser Checks
79
124
 
@@ -82,6 +127,7 @@ Use real interactions for:
82
127
  - `preview-render`;
83
128
  - `control-change`;
84
129
  - `control-drag`;
130
+ - `mask-drag` when canvas handles, masks, pins, or on-canvas anchors affect output;
85
131
  - `media-import` when upload exists;
86
132
  - `export-copy` for product export actions and clipboard actions; measure retina output dimensions, not CSS preview size;
87
133
  - `timeline-playback` or `timeline-scrub` when timeline exists;
@@ -89,9 +135,11 @@ Use real interactions for:
89
135
  - `viewport-zoom-stress` for detail-heavy or animated custom renderers;
90
136
  - `viewport-stability`.
91
137
 
138
+ For `slider` and `rangeSlider`, the required performance scenario is `control-drag`. A `control-change` scenario can cover selects, inputs, toggles, and other non-drag controls, but it does not prove live canvas feedback or drag smoothness for sliders.
139
+
92
140
  Animated custom renderers also need `animation-viewport-drag`. Animation-only frame sampling and viewport-only stability are not enough: the browser test must sample frames while physically dragging or panning the canvas viewport. If SVG/DOM cannot pass that combined budget, choose a different renderer strategy from evidence instead of loosening the budget.
93
141
 
94
- Detail-heavy or animated custom renderers also need `viewport-zoom-stress`. This test must use the real toolbar zoom controls while sampling frame gaps and long tasks. Do not satisfy it by calling `canvas.zoom`, mutating runtime state directly, or checking only the final zoom value.
142
+ Detail-heavy or animated custom renderers also need `viewport-zoom-stress`. This test must apply the combined worst-case stress fixture first, then use the real toolbar zoom controls while sampling frame gaps and long tasks. Do not satisfy it by calling `canvas.zoom`, mutating runtime state directly, checking only the final zoom value, or zooming a default/lightweight output.
95
143
 
96
144
  Detail-heavy custom renderers also need a stress `preview-render` or `animation-frame` scenario with a `maxLongTaskMs` budget. A high-count Canvas 2D layer must carry that evidence before delivery. If it fails, revise renderer strategy from the measured failure instead of keeping Canvas 2D by default.
97
145
 
@@ -14,6 +14,8 @@ The initial renderer choice is provisional. It becomes accepted only after the a
14
14
 
15
15
  Dense backgrounds may use Canvas 2D, WebGL, or WebGPU when the spec names primitive count and performance reason. A dense raster background does not justify rasterizing low-count foreground geometry or text.
16
16
 
17
+ Heavy bitmap-media, shader-like, noise/texture, filter, halftone, mesh, and per-pixel image-processing layers must evaluate WebGL/WebGPU before delivery. Keeping the pixel work on Canvas 2D requires measured worst-case evidence in `whyNotAlternativeStrategies` or `performanceRisks`, using the real media/canvas stress fixture. Do not keep CPU Canvas 2D by default and then pass budgets by downsampling, lowering render scale, or testing a small upload.
18
+
17
19
  Do not force WebGL only because an app is visually rich, and do not keep Canvas 2D only because the primitive is text or vector. Use the stress results. High-count text, vectors, particles, grids, media, or procedural layers can stay on Canvas/SVG/DOM only when worst-case preview or animation tests prove they remain responsive. If they do not, split layers or move the heavy product renderer to WebGL/WebGPU.
18
20
 
19
21
  ## Required Matrix
@@ -32,6 +34,8 @@ Custom renderer specs and `src/app/app-performance.ts` must mirror the decision:
32
34
 
33
35
  If text or vector output is intentionally rasterized, include `intentionalRasterizationReason`. If preview and export renderers differ, include `previewExportDifferenceReason`. If a reference runtime renderer changes, include `referenceRendererChangeReason`.
34
36
 
37
+ `rendererTechnique` chooses the technology. `rendererPipeline` proves the architecture. Do not start a custom renderer until both are written: technology without pass invalidation still lets an app recompute too much work.
38
+
35
39
  For heavy custom renderers, specs and `app-performance.ts` must also include stress preview or animation evidence using real maximum values from the app: max density, max text length, max item count, max canvas size, max animation speed, max export quality, max media size, or the nearest real heavy fixture for the product.
36
40
 
37
41
  ## Layer Inventory
@@ -45,7 +45,7 @@ export: {
45
45
  - `appearance.background` or `scene.background` as a `color` control;
46
46
  - `export.includeBackground` as a boolean/options control.
47
47
 
48
- PNG exporters should call `createToolcraftPngExportCanvas({ background, includeBackground, resolution, state, render })`, where `background`, `includeBackground`, and `resolution` come from runtime state. `includeBackground` controls only PNG alpha; live preview, workspace canvas backing, and video export keep the product background. For every app with `Export PNG`, `resolution` comes from `export.image.resolution`: `2k`, `4k`, and `8k` render actual 2048/4096/8192px long-edge PNGs. `current` or omitted resolution falls back to retina sizing. Video export always includes the product background, uses `getToolcraftRetinaExportSize`, and must prove exported metadata duration matches the runtime timeline duration.
48
+ PNG exporters should call `createToolcraftPngExportCanvas({ background, includeBackground, resolution, state, render })`, where `background`, `includeBackground`, and `resolution` come from runtime state. Live preview renderers should call `shouldIncludeToolcraftPreviewBackground(state)` and hide only the product-rendered background when it returns false; do not hide or replace the Toolcraft canvas shell/backing. For every app with `Export PNG`, `resolution` comes from `export.image.resolution`: `2k`, `4k`, and `8k` render actual 2048/4096/8192px long-edge PNGs. `current` or omitted resolution falls back to retina sizing. Video export always includes the product background, uses `getToolcraftRetinaExportSize`, and must prove exported metadata duration matches the runtime timeline duration.
49
49
 
50
50
  Every app with `Export PNG` exposes a separate `Image Export` controls section. For still-output apps it sits directly above sticky footer actions. For animated apps with both `Export PNG` and `Export Video`, it sits immediately before `Video Export`:
51
51
 
@@ -129,18 +129,18 @@ Use `MediaRecorder.isTypeSupported(...)` or an explicit encoder/transcoder capab
129
129
  Choose sizing from product context:
130
130
 
131
131
  - `intrinsic-media`: a single uploaded or generated source defines `canvas.size`.
132
- - `editable-output`: exportable output where users should edit width and height.
133
- - `fixed-output`: product-defined output size that users must not edit.
132
+ - `editable-output`: product/export output where users always see aspect ratio, width, and height.
133
+ - `fixed-output`: non-product/internal output size that users must not edit.
134
134
 
135
- For product output, export, copy, download, shader rendering, procedural rendering, or no single intrinsic source image, use `editable-output` unless the product explicitly needs `fixed-output`.
135
+ For product output, export, copy, download, shader rendering, procedural rendering, reference clones, or no single intrinsic source image, use `editable-output`.
136
136
 
137
- A prompt-provided base/default size is only the initial `canvas.size`. It must not remove the runtime Aspect ratio, Canvas width, and Canvas height controls. Aspect presets use canonical output sizes (`16:9` is `1920x1080`; the other presets are derived around a 1080px short edge or matching portrait long edge). When no explicit product size is provided, runtime defaults to `16:9` / `1920x1080`; choose another preset only when the product meaning calls for it. Use `fixed-output` only when the reference or product explicitly locks dimensions, and add runtime acceptance with `canvasSizingCoverage: "fixed-output-size"`.
137
+ A prompt-provided, reference, fixed-format, or base/default size is only the initial `canvas.size`. It must not remove the runtime Aspect ratio, Canvas width, and Canvas height controls. Aspect presets use canonical output sizes (`16:9` is `1920x1080`; the other presets are derived around a 1080px short edge or matching portrait long edge). When no explicit product size is provided, runtime defaults to `16:9` / `1920x1080`; choose another preset only when the product meaning calls for it. Generated product/export apps do not use `fixed-output` to preserve a reference baseline; fixed dimensions stay visible as editable defaults.
138
138
 
139
- Resolved `canvas.size` exists for every canvas app, but visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are mandatory only for `editable-output` sizing. They do not depend on `settingsTransfer`: when settings transfer is off, the runtime prepends a technical `Setup` canvas size section without a visible heading; when settings transfer is on, the controls merge into the first technical `Setup` runtime settings section without a visible heading. Do not hand-build a duplicate size selector.
139
+ Resolved `canvas.size` exists for every canvas app, but visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are mandatory through `editable-output` sizing for product/export apps. They do not depend on `settingsTransfer`: when settings transfer is off, the runtime prepends a technical `Setup` canvas size section without a visible heading; when settings transfer is on, the controls merge into the first technical `Setup` runtime settings section without a visible heading. Do not hand-build a duplicate size selector.
140
140
 
141
141
  When the user manually edits `Canvas width` or `Canvas height`, the runtime treats that as an exact custom output size. It 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. Only selecting an aspect preset may resize both dimensions from a canonical preset.
142
142
 
143
- For non-vector raster, Canvas 2D, WebGL, or WebGPU previews, set `canvas.renderScale: true`. Runtime appends `Resolution scale` after canvas sizing in the first technical `Setup` section. The slider ranges from `1x` to `2x`, defaults to `2x`, and changes the renderer backing pixels without changing the visible CSS size or product output dimensions. Adding or enabling this slider requires targeted browser evidence that the canvas stays responsive while dragging sliders or other high-frequency controls at the selected scale. Full `pnpm verify:perf` is required only for the first working app version or explicit performance complaints. Performance fixes must preserve the selected scale. Diagnose whether lag comes from renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, stale async renders, or animation scheduling before reducing quality. Do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp `canvas.renderScale` below the user's chosen value. Do not enable it for DOM/SVG/vector-native previews; use native vector rendering for those.
143
+ For non-vector raster, Canvas 2D, WebGL, or WebGPU previews, set `canvas.renderScale: true`. Runtime appends `Resolution scale` after canvas sizing in the first technical `Setup` section. The slider ranges from `1` to `2`, defaults to `2`, and changes the renderer backing pixels without changing the visible CSS size or product output dimensions. Adding or enabling this slider requires targeted browser evidence that the canvas stays responsive while dragging sliders or other high-frequency controls at the selected scale. Full `pnpm verify:perf` is required only for the first working app version or explicit performance complaints. Performance fixes must preserve the selected scale. Diagnose whether lag comes from renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, stale async renders, or animation scheduling before reducing quality. Do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp `canvas.renderScale` below the user's chosen value. Do not enable it for DOM/SVG/vector-native previews; use native vector rendering for those.
144
144
 
145
145
  ## Panels
146
146
 
@@ -157,7 +157,7 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
157
157
 
158
158
  | `type` | Renders | Key fields |
159
159
  | --- | --- | --- |
160
- | `actions` | Inline local action buttons for the current section or nearby entity | `actions`, `target`, `label` |
160
+ | `actions` | Local action buttons for the current section or nearby entity, rendered below the label in a two-column grid | `actions`, `target`, `label` |
161
161
  | `anchorGrid` | Anchor picker | `defaultValue`, `target` |
162
162
  | `channelMixer` | RGB-only channel matrix mixer with R/G/B tabs and Red/Green/Blue source sliders | `defaultValue`, `target`, `label` |
163
163
  | `checkbox` | Checkbox field | `defaultValue`, `target`, `label` |
@@ -166,8 +166,8 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
166
166
  | `color` | Hex color picker | `defaultValue: { hex }`, `target`, `label` |
167
167
  | `colorOpacity` | Hex color picker plus opacity percent input | `defaultValue: { hex, opacity }`, `target`, `label` |
168
168
  | `curves` | RGB or single curve editor | `defaultValue`, `target`, `variant: "single"`, `interpolation: "smooth" \| "monotone"` |
169
- | `fileDrop` | Upload/drop input | `accept`, `multiple`, `target` |
170
- | `fontPicker` | Font preview select with popup, category search, weight, size, text case, text color/opacity, letter spacing, and line height; product text must consume `fontId`, `fontWeight`, `fontSize`, `letterSpacing`, `lineHeight`, `textCase`, `color`, and `opacity` | `defaultValue: { fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, opacity }`, `target` |
169
+ | `fileDrop` | Upload/drop input; `assetKind: "image"` owns image previews and `assetKind: "file"` owns sortable arbitrary file lists | `assetKind`, `accept`, `multiple`, `defaultValue`, `target` |
170
+ | `fontPicker` | Font preview select with popup, category search, weight, size, text case, text color/opacity, letter spacing, and line height; product text must consume `fontId`, `fontWeight`, `fontSize`, `letterSpacing`, `lineHeight`, `textCase`, `color`, and `opacity`; default text color is `#FFFFFF` at `100` opacity | `defaultValue: { fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, opacity }`, `target` |
171
171
  | `gradient` | Gradient editor | `defaultValue: { angle, gradientType, stops }`, `target` |
172
172
  | `imagePicker` | Image choice grid | `items`, `defaultValue`, `target` |
173
173
  | `palette` | Constrained design-token palette picker for family + shade | `defaultValue: { family, shade }`, `target` |
@@ -179,13 +179,19 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
179
179
  | `slider` | Single-value slider | `defaultValue`, `min`, `max`, `step`, `unit`, `variant` |
180
180
  | `switch` | Binary switch | `defaultValue`, `target`, `label` |
181
181
  | `text` | Single-line input | `defaultValue`, `target`, `label`, `commitMode` |
182
- | `vector` | X/Y vector pad and fields | `defaultValue: { x, y }`, `xLabel`, `yLabel`, `variant` |
182
+ | `vector` | X/Y vector pad and fields | `defaultValue: { x, y }`, `xLabel`, `yLabel`, `variant`, `coordinateMode` |
183
183
 
184
- `text` defaults to `commitMode: "content"` and applies while typing for real content such as prompts, names, titles, tokens, and short text. Use `commitMode: "setting"` for text inputs that edit settings such as font size, numeric-like style values, dimensions, ids, or configuration fields; setting text commits on blur or Enter. Canvas width and Canvas height always commit on blur or Enter. `code` / `CodeTextarea` is a content editor, applies while typing, and is capped at 12 visible lines. Long content scrolls inside the textarea instead of making the controls panel taller.
184
+ `text` defaults to `commitMode: "content"` and applies while typing for short real content such as button labels, canvas labels, names, titles, captions, tokens, compact prompts, and other one-line text. Use `commitMode: "setting"` for text inputs that edit settings such as font size, numeric-like style values, dimensions, ids, or configuration fields; setting text commits on blur or Enter. Canvas width and Canvas height always commit on blur or Enter. `code` / `CodeTextarea` is a content editor for long, multiline, or structured values, applies while typing, and is capped at 12 visible lines. Long content scrolls inside the textarea instead of making the controls panel taller. Do not use `code` for short single-line button/canvas text unless `description` explicitly proves the field is intended for long or structured input.
185
185
 
186
- For `slider` and `rangeSlider`, `unit` is a measurement or scale suffix, not the entity being counted. Use units such as `%`, `px`, `°`, `x`, `s`, `ms`, `fps`, `rows`, or `cols` only when they clarify the number. Do not add repeated nouns such as `letters`, `shapes`, `words`, `symbols`, `items`, `particles`, or `layers` when the label or section already names that entity. If the value needs a noun, rename the label or section. Word or acronym units, when truly needed, render with a space (`5 cols`, `17 fps`); compact symbol/CSS units stay tight (`70%`, `24px`).
186
+ For `slider` and `rangeSlider`, `unit` is a real measurement suffix, not the entity being counted and not a generic multiplier. Use units such as `%`, `px`, `°`, `s`, `ms`, `fps`, `rows`, or `cols` only when they clarify the number. Do not use `unit: "x"`; scale, multiplier, intensity, opacity, strength, depth, and shader amount values display plain numbers unless a real measurement unit applies. Do not add repeated nouns such as `letters`, `shapes`, `words`, `symbols`, `items`, `particles`, or `layers` when the label or section already names that entity. If the value needs a noun, rename the label or section. Word or acronym units, when truly needed, render with a space (`5 cols`, `17 fps`); compact symbol/CSS units stay tight (`70%`, `24px`).
187
187
 
188
- Use `collectionActions` when the product owns a growable/shrinkable item list. `minItems` protects the smallest valid output, `recommendedMaxItems` is only a design recommendation, and `hardMaxItems` is valid only for a real product or technical limit. Adding/removing items must update the runtime array and the renderer/export must consume that same array. Do not pair a count slider with hidden fixed item controls when the user needs to add or remove actual entities. The collection label is on the left and remove/add buttons stay on the right. Homogeneous repeated items do not show visible per-item labels when the collection label already names the group. `itemControl.type` supports normal item built-ins such as `color`, `colorOpacity`, `text`, `select`, `segmented`, `slider`, `switch`, `checkbox`, and `rangeInput`; item controls still follow normal density rules, so plain colors use equal 50% columns when they fit.
188
+ `slider` and `rangeSlider` are live controls. Dragging must update runtime state and product output while the drag is in progress, not only on pointer release, blur, Apply, or a final commit. Treat a non-live slider as a broken product mapping unless an extreme measured performance ceiling is documented; even then, keep immediate lightweight canvas feedback and refine the heavy output after coalescing/caching.
189
+
190
+ For `vector`, the default/spatial variant uses `coordinateMode: "screen"` by default: dragging left/up lowers `x` and `y`, so canvas objects move left/up without renderer-side Y inversion. Use `coordinateMode: "cartesian"` only for intentional mathematical Y-up coordinates. Color variants keep their color-axis semantics by default.
191
+
192
+ Use `collectionActions` when the product owns a growable/shrinkable item list. `minItems` protects the smallest valid output, `recommendedMaxItems` is only a design recommendation, and `hardMaxItems` is valid only for a real product or technical limit. Adding/removing items must update the runtime array and the renderer/export must consume that same array. Do not pair a count slider with hidden fixed item controls when the user needs to add or remove actual entities. The collection label is on the left and remove/add buttons stay on the right. Homogeneous repeated items do not show visible per-item labels when the collection label already names the group. `itemControl.type` supports normal item built-ins such as `color`, `colorOpacity`, `text`, `select`, `segmented`, `slider`, `switch`, `checkbox`, `rangeInput`, and `fontPicker`; item controls still follow normal density rules, so plain colors use equal 50% columns when they fit. Use `fontPicker` as the item control when each repeated item is a text style or typography entity; do not split its font, weight, size, case, color/opacity, letter spacing, or line height into sibling collection fields.
193
+
194
+ Segmented controls are full-width compact choices. Do not place `segmented` in inline half-width rows beside Switch, Color, Select, or another control; use `select` when a finite choice must occupy a half-width column.
189
195
 
190
196
  ## Control Selection Inventory
191
197
 
@@ -221,15 +227,38 @@ If the user asks for product animation and does not explicitly say it is decorat
221
227
 
222
228
  ## Control Section Inventory
223
229
 
224
- Before editing `panels.controls.sections`, write a short inventory in the spec or plan:
230
+ Before editing `panels.controls.sections`, define and export `starterControlSectionInventory` beside `starterAcceptance` in `src/app/starter-acceptance.ts`. This is the machine-checkable version of the section plan.
225
231
 
226
232
  - section title;
227
233
  - product entity or workflow stage;
228
234
  - included schema targets;
229
235
  - reason these controls belong together or reason for a real workflow split.
230
236
 
237
+ ```ts
238
+ export const starterControlSectionInventory = [
239
+ {
240
+ entity: "Text block",
241
+ groupingReason:
242
+ "These controls edit the text content, typography, and visible text fill together.",
243
+ targets: ["text.content", "text.font"],
244
+ title: "Text",
245
+ },
246
+ {
247
+ entity: "Object shape",
248
+ groupingReason: "Structure controls tune the physical footprint of the object.",
249
+ splitReason:
250
+ "Structure and density are separate workflow stages in this editor.",
251
+ targets: ["object.shape.size"],
252
+ title: "Shape Structure",
253
+ workflowStage: "structure",
254
+ },
255
+ ] as const;
256
+ ```
257
+
231
258
  Group controls by product meaning, not by component type. Do not create sections named `Controls`, `Settings`, `Options`, `Sliders`, `Inputs`, `Buttons`, `Color`, or `Colors`.
232
259
 
260
+ The inventory must match the rendered schema: every product control target in every product section appears exactly once, and every inventory target renders in the section named by `title`. Runtime technical `Setup` controls, sticky footer `Export` actions, `settingsTransfer`, and runtime canvas sizing controls do not need inventory entries. If one target entity is split across sections, every split section must declare `workflowStage` and a concrete `splitReason`; otherwise the validator treats the split as accidental section drift.
261
+
233
262
  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.
234
263
 
235
264
  Every visible section title renders through the standard 36px collapsible header row with vertically centered text and the runtime collapse icon. Do not hand-build section headers in generated apps.
@@ -242,7 +271,7 @@ Ordinary section headers expose the runtime section reset action before the coll
242
271
 
243
272
  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.
244
273
 
245
- Large built-in compound controls inside mixed sections render content-width internal dividers with 18px between each rendered divider and the control content. If a compound control is the first item in that section, render only its bottom internal divider and remove the top internal padding. If a section contains exactly one control, whether simple or compound, only the parent section dividers render. Single `curves` are not compound for dividers; RGB `curves` are compound.
274
+ Large built-in compound controls inside mixed sections render content-width internal dividers with 18px between each rendered divider and the control content. If a compound control is the first item in that section, render only its bottom internal divider and remove the top internal padding. If it is the last item in that section, render only its top internal divider and remove the bottom internal padding. If a section contains exactly one control, whether simple or compound, only the parent section dividers render. Single `curves` are not compound for dividers; RGB `curves` are compound.
246
275
 
247
276
  Controls for the same product entity stay in the same section. For example, `squares.right.connections`, `squares.right.hoverRadius`, and `squares.right.color` belong in `Square 1 (Right)` with `Color` as the field label. A standalone color section is only valid when the color is the whole product entity, such as `Background`, `Accent`, `Connector`, or `Brand`.
248
277
 
@@ -254,7 +283,7 @@ If a target prefix has to be split across sections, the spec must name the workf
254
283
 
255
284
  Switch and checkbox labels name the setting context only. Do not prefix them with `Enable` or `Disable`; use `CRT`, `Glow`, `Loop`, or `Guides` instead. If the nearest section title already names the context, do not duplicate it as the visible toggle label. Use a short contextual label such as `Include` or, only for icon-only visual toggles, `label: false` with the product meaning in `target` and `description`.
256
285
 
257
- Inline two-column groups are preferred when controls tune one close product meaning and labels/values fit. Short numeric text pairs can be inline. Related short `select` pairs can be inline, especially workflow pairs such as `Format` + `Resolution`, `Codec` + `Profile`, or `Width unit` + `Height unit`. Use stacked one-control rows only as a fit fallback when a label, selected value, or option text would clip, truncate, or lose padding; record that fallback reason in the spec or worklog. A short numeric/text field may also pair with one related plain `color` field when both configure the same entity, such as `Mask size` and `Color` inside `Mask`. `colorOpacity` never renders in inline two-column groups; if either color control has opacity, keep the controls stacked. Color controls show visible field labels in mixed sections that contain any non-color control. Omit visible color labels only in color-only sections. Mixed inline rows require visible labels on every field, except the required Background row where the color control uses `label: false` because the section title owns the context. Color fields in other mixed rows must not be unlabeled. Two adjacent `switch` or `checkbox` controls for the same product entity must share one inline row when both visible labels fit without truncation; the runtime auto-pairs safe adjacent toggles by target entity, and schemas should stack them only when either label is too long. A single `switch` or `checkbox` may share an inline row with one related parameter control when the toggle label fits and both controls edit the same entity; shorten the toggle label when the section title already supplies context. Toggle plus parameter rows are equal-width two-column rows: each control occupies one half, never intrinsic toggle width plus remaining space. The required Background row uses `Include` plus unlabeled background color. Schema `slider` and `rangeSlider` controls always stay stacked at full width; the only built-in exception is the paired letter-spacing and line-height footer sliders inside `fontPicker`. Do not place sibling controls for case, color, opacity, size, weight, letter spacing, or line height when the same text entity already uses `fontPicker`.
286
+ Inline two-column groups are preferred when controls tune one close product meaning and labels/values fit. Short numeric text pairs can be inline. Related short `select` pairs can be inline, especially workflow pairs such as `Format` + `Resolution`, `Codec` + `Profile`, or `Width unit` + `Height unit`. Use stacked one-control rows only as a fit fallback when a label, selected value, or option text would clip, truncate, or lose padding; record that fallback reason in the spec or worklog. A short numeric/text field may also pair with one related plain `color` field when both configure the same entity, such as `Mask size` and `Color` inside `Mask`. `colorOpacity` never renders in inline two-column groups; if either color control has opacity, keep the controls stacked. Color labels are semantic, not automatic: decide once for the whole color group, omit per-item labels such as `Color 1` for palette variation banks like `Accent Shades` or `Bead Colors`, and do not mix labeled and unlabeled items inside that bank. Sibling controls like `Spread` or `Randomness` do not force item labels; keep visible labels only when colors edit distinct roles such as `Fill`, `Stroke`, `Background`, `Connector`, or `Object`. Related plain color banks render two per row, and an odd trailing plain color remains half-width instead of stretching to a full row. Mixed inline rows usually require visible labels on every field, except toggle-plus-parameter rows, the required Background row, and palette variation color banks whose group/section label already names the bank. All 50/50 inline rows use the same horizontal column gap as paired `select` controls; do not create a wider or narrower gap for toggle-plus-parameter rows. Two adjacent `switch` or `checkbox` controls for the same product entity must share one inline row when both visible labels fit without truncation; the runtime auto-pairs safe adjacent toggles by target entity, and schemas should stack them only when either label is too long. A single `switch` or `checkbox` may share an inline row with one related parameter control when the toggle label fits and both controls edit the same entity; the non-toggle parameter uses `label: false`, and if that label is needed, stack the controls instead. Toggle plus parameter rows are equal-width two-column rows: each control occupies one half, never intrinsic toggle width plus remaining space. The required Background row uses `Include` plus unlabeled background color. Schema `slider` and `rangeSlider` controls always stay stacked at full width; the only built-in exception is the paired letter-spacing and line-height footer sliders inside `fontPicker`. Do not place sibling controls for case, color, opacity, size, weight, letter spacing, or line height when the same text entity already uses `fontPicker`.
258
287
 
259
288
  `rangeSlider` is always a full-width two-thumb control. Do not include it in `layoutGroups`. Its `defaultValue` must start with different lower and upper values, such as `[20, 80]`, so the two handles do not collapse into one apparent slider. Manual range labels accept built-in separators such as slash, hyphen, spaces, and dashes.
260
289
 
@@ -266,7 +295,7 @@ Order controls by decision flow inside each section:
266
295
  - `mode`: mode, type, filter, blend, style, and preset selectors;
267
296
  - `primary`, `spatial`, `color`: core product parameters;
268
297
  - `strength`: intensity, opacity, scale, depth;
269
- - `detail`: grain, noise, blur, density, radius, quality;
298
+ - `detail`: noise, texture, blur, density, radius, quality;
270
299
  - `advanced`: secondary tuning;
271
300
  - `action`: footer actions.
272
301
 
@@ -309,6 +338,6 @@ After adding, removing, or reorganizing controls, sections, timeline, or layers,
309
338
 
310
339
  When settings transfer is enabled and the canvas uses `editable-output` sizing, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and optional `Resolution scale` in that order. Do not split these into separate sections or recreate them manually.
311
340
 
312
- A settings-transfer section with only `Export Settings` and `Import Settings` means the canvas is not `editable-output` or the app already declares its own `canvas.size.width` / `canvas.size.height` controls. For product-output apps, treat that as a schema decision to review.
341
+ A settings-transfer section with only `Export Settings` and `Import Settings` means the canvas is not `editable-output` or the app already declares its own `canvas.size.width` / `canvas.size.height` controls. For product-output apps, treat that as a schema error to fix, not as a layout variant.
313
342
 
314
343
  Do not hand-write `settings-transfer.ts`, hidden file inputs, route handlers, or `panelActions` for settings import/export. Sticky footer `panelActions` remain product delivery only.
@@ -19,7 +19,7 @@ Do not edit implementation files until this preflight is complete.
19
19
 
20
20
  ## Task Routing
21
21
 
22
- Use the smallest reading set that covers the changed surface.
22
+ Use the smallest reading set that covers the changed surface. If a task touches multiple surfaces, combine every matching row instead of picking only the closest one.
23
23
 
24
24
  | Task type | Read before editing |
25
25
  | --- | --- |
@@ -29,7 +29,7 @@ Use the smallest reading set that covers the changed surface.
29
29
  | Renderer, canvas output, visual technique | `renderer-technique.md`, `performance.md`, `acceptance-testing.md` |
30
30
  | Timeline, keyframes, animation transport | `decision-contract.md`, `component-rules.md`, `acceptance-testing.md`, `performance.md` |
31
31
  | Layers | `decision-contract.md`, `component-rules.md`, `acceptance-testing.md` |
32
- | Export, copy, media, background | `schema-reference.md`, `component-rules.md`, `acceptance-testing.md` |
32
+ | Export, copy, media, background | `schema-reference.md`, `component-rules.md`, `acceptance-testing.md`, `performance.md` |
33
33
  | Broken control, visual mismatch, failed build, export bug, performance issue | `decision-contract.md`, the relevant component/runtime doc, and the failing test/log first |
34
34
  | Figma implementation | Use Figma MCP/design context, then `assembly-workflow.md` and the relevant component docs |
35
35
 
@@ -110,12 +110,49 @@ function getScenarioBudgetAssertionPattern(scenarioId: string): RegExp {
110
110
 
111
111
  function getScenarioStressFixturePattern(scenarioId: string): RegExp {
112
112
  return new RegExp(
113
- `getToolcraftPerformanceStressValue(?:\\s*<[^>]+>)?\\s*\\(\\s*(?:starterPerformance|appPerformance)\\s*,\\s*(["'\`])${escapeRegExp(
113
+ `(?:getToolcraftPerformanceStressValue(?:\\s*<[^>]+>)?\\s*\\(\\s*(?:starterPerformance|appPerformance)|dragToolcraftSliderToPerformanceStressValue\\s*\\([\\s\\S]*?(?:starterPerformance|appPerformance)|applyToolcraftPerformanceStressFixture\\s*\\(\\s*page\\s*,\\s*(?:starterPerformance|appPerformance))\\s*,\\s*(["'\`])${escapeRegExp(
114
114
  scenarioId,
115
115
  )}\\1`,
116
116
  );
117
117
  }
118
118
 
119
+ function getScenarioWorkloadFixturePattern(scenarioId: string): RegExp {
120
+ return new RegExp(
121
+ `(?:getToolcraftPerformanceWorkloadValue(?:\\s*<[^>]+>)?\\s*\\(\\s*(?:starterPerformance|appPerformance)|applyToolcraftPerformanceWorkloadFixture\\s*\\(\\s*page\\s*,\\s*(?:starterPerformance|appPerformance))\\s*,\\s*(["'\`])${escapeRegExp(
122
+ scenarioId,
123
+ )}\\1`,
124
+ );
125
+ }
126
+
127
+ function getScenarioCustomStressFixturePattern(scenarioId: string): RegExp {
128
+ return new RegExp(
129
+ `applyToolcraftPerformanceStressFixture\\s*\\(\\s*page\\s*,\\s*(?:starterPerformance|appPerformance)\\s*,\\s*(["'\`])${escapeRegExp(
130
+ scenarioId,
131
+ )}\\1`,
132
+ );
133
+ }
134
+
135
+ function getScenarioCustomWorkloadFixturePattern(scenarioId: string): RegExp {
136
+ return new RegExp(
137
+ `applyToolcraftPerformanceWorkloadFixture\\s*\\(\\s*page\\s*,\\s*(?:starterPerformance|appPerformance)\\s*,\\s*(["'\`])${escapeRegExp(
138
+ scenarioId,
139
+ )}\\1`,
140
+ );
141
+ }
142
+
143
+ function getScenarioSliderStressValuePattern(scenarioId: string): RegExp {
144
+ return new RegExp(
145
+ `dragToolcraftSliderToPerformanceStressValue\\s*\\([\\s\\S]*?(?:starterPerformance|appPerformance)\\s*,\\s*(["'\`])${escapeRegExp(
146
+ scenarioId,
147
+ )}\\1`,
148
+ );
149
+ }
150
+
151
+ function getFirstMatchIndex(source: string, pattern: RegExp): number {
152
+ const match = pattern.exec(source);
153
+ return match?.index ?? -1;
154
+ }
155
+
119
156
  test("browser performance matrix points at real Playwright tests", () => {
120
157
  const browserTestSources = readSiblingBrowserTestSources();
121
158
 
@@ -154,15 +191,71 @@ test("browser performance tests use real Toolcraft interactions", () => {
154
191
  continue;
155
192
  }
156
193
 
157
- if (scenario.workload) {
194
+ if (scenario.workload || scenario.stress === true || scenario.stressFixture) {
158
195
  expect(
159
196
  scenario.stressFixture,
160
- `${scenario.id} workload scenario must declare stressFixture so browser tests cannot use toy inputs.`,
197
+ `${scenario.id} stress scenario must declare stressFixture so browser tests cannot use toy inputs.`,
161
198
  ).toBeDefined();
162
199
  expect(
163
200
  browserTestSource,
164
201
  `${scenario.id} must read the declared stress fixture with getToolcraftPerformanceStressValue(appPerformance, "${scenario.id}") before measuring performance.`,
165
202
  ).toMatch(getScenarioStressFixturePattern(scenario.id));
203
+
204
+ if (
205
+ scenario.stressFixture?.kind === "custom" &&
206
+ typeof scenario.stressFixture.value === "object" &&
207
+ scenario.stressFixture.value !== null &&
208
+ !Array.isArray(scenario.stressFixture.value)
209
+ ) {
210
+ expect(
211
+ browserTestSource,
212
+ `${scenario.id} custom stress fixture must be applied with applyToolcraftPerformanceStressFixture so every heavy-state key is mapped through the real UI before measurement.`,
213
+ ).toMatch(getScenarioCustomStressFixturePattern(scenario.id));
214
+ }
215
+ }
216
+
217
+ if (scenario.workloadFixture) {
218
+ const workloadFixtureIndex = getFirstMatchIndex(
219
+ browserTestSource,
220
+ getScenarioWorkloadFixturePattern(scenario.id),
221
+ );
222
+ const stressFixtureIndex = getFirstMatchIndex(
223
+ browserTestSource,
224
+ getScenarioStressFixturePattern(scenario.id),
225
+ );
226
+ const budgetAssertionIndex = getFirstMatchIndex(
227
+ browserTestSource,
228
+ getScenarioBudgetAssertionPattern(scenario.id),
229
+ );
230
+
231
+ expect(
232
+ browserTestSource,
233
+ `${scenario.id} must apply the declared workloadFixture with getToolcraftPerformanceWorkloadValue(appPerformance, "${scenario.id}") or applyToolcraftPerformanceWorkloadFixture before measuring performance.`,
234
+ ).toMatch(getScenarioWorkloadFixturePattern(scenario.id));
235
+ expect(
236
+ workloadFixtureIndex,
237
+ `${scenario.id} must apply workloadFixture before stressFixture so the measured control runs inside the heavy app baseline.`,
238
+ ).toBeGreaterThanOrEqual(0);
239
+ expect(
240
+ stressFixtureIndex,
241
+ `${scenario.id} must read/apply stressFixture after workloadFixture.`,
242
+ ).toBeGreaterThan(workloadFixtureIndex);
243
+ expect(
244
+ budgetAssertionIndex,
245
+ `${scenario.id} must assert budget only after workloadFixture is applied.`,
246
+ ).toBeGreaterThan(workloadFixtureIndex);
247
+
248
+ if (
249
+ scenario.workloadFixture.kind === "custom" &&
250
+ typeof scenario.workloadFixture.value === "object" &&
251
+ scenario.workloadFixture.value !== null &&
252
+ !Array.isArray(scenario.workloadFixture.value)
253
+ ) {
254
+ expect(
255
+ browserTestSource,
256
+ `${scenario.id} custom workload fixture must be applied with applyToolcraftPerformanceWorkloadFixture so every baseline-state key is mapped through the real UI before measurement.`,
257
+ ).toMatch(getScenarioCustomWorkloadFixturePattern(scenario.id));
258
+ }
166
259
  }
167
260
 
168
261
  if (scenario.interaction === "control-drag") {
@@ -190,6 +283,13 @@ test("browser performance tests use real Toolcraft interactions", () => {
190
283
  `${scenario.id} drags discrete slider "${scenario.controlLabel}" and must verify Toolcraft marker-budget behavior plus smooth drag.`,
191
284
  ).toMatch(/expectToolcraftDiscreteSliderDragSmoothness\s*\(/);
192
285
  }
286
+
287
+ if (scenario.workload && scenario.controlLabel) {
288
+ expect(
289
+ browserTestSource,
290
+ `${scenario.id} workload slider drags must apply the exact stressFixture.value with dragToolcraftSliderToPerformanceStressValue instead of hand-dividing by max or typing a ratio.`,
291
+ ).toMatch(getScenarioSliderStressValuePattern(scenario.id));
292
+ }
193
293
  }
194
294
 
195
295
  if (scenario.interaction === "control-change") {
@@ -284,6 +384,39 @@ test("browser performance tests use real Toolcraft interactions", () => {
284
384
  ).not.toMatch(/canvas\.setOffset|canvas\.panBy|canvas\.zoom|Zoom in|Zoom out|Center canvas/);
285
385
  }
286
386
 
387
+ if (scenario.interaction === "timeline-playback") {
388
+ expect(
389
+ browserTestSource,
390
+ `${scenario.id} must use the real timeline playback UI while measuring frames.`,
391
+ ).toMatch(/measureToolcraftInteraction\s*\([\s\S]*(Play playback|Pause playback|timeline-playback|data-slot=["']timeline-playback)/);
392
+ expect(
393
+ browserTestSource,
394
+ `${scenario.id} must not satisfy timeline playback performance by dispatching timeline state directly.`,
395
+ ).not.toMatch(/timeline\.setCurrentTime|timeline\.setPlaying|timeline\.play|timeline\.pause/);
396
+ }
397
+
398
+ if (scenario.interaction === "timeline-scrub") {
399
+ expect(
400
+ browserTestSource,
401
+ `${scenario.id} must scrub the real timeline UI while measuring interaction responsiveness.`,
402
+ ).toMatch(/measureToolcraftInteraction\s*\([\s\S]*(Playback position|timeline-playback-handle|data-slot=["']timeline-playback-handle|page\.mouse\.(?:down|move|up))/);
403
+ expect(
404
+ browserTestSource,
405
+ `${scenario.id} must not satisfy timeline scrub performance by setting timeline time directly.`,
406
+ ).not.toMatch(/timeline\.setCurrentTime|canvas\.setCurrentTime/);
407
+ }
408
+
409
+ if (scenario.interaction === "mask-drag") {
410
+ expect(
411
+ browserTestSource,
412
+ `${scenario.id} must measure a real canvas handle/mask drag.`,
413
+ ).toMatch(/measureToolcraftInteraction\s*\([\s\S]*dragCanvasHandle\s*\(/);
414
+ expect(
415
+ browserTestSource,
416
+ `${scenario.id} must not fake mask drag coverage with direct runtime state mutation.`,
417
+ ).not.toMatch(/mask\.(?:set|update|move)|canvas\.setOffset|canvas\.panBy/);
418
+ }
419
+
287
420
  if (scenario.interaction === "viewport-zoom-stress") {
288
421
  expect(
289
422
  browserTestSource,