@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
@@ -76,6 +76,34 @@ const ordinarySliderSchema = defineToolcraft({
76
76
  },
77
77
  });
78
78
 
79
+ const rangeSliderSchema = defineToolcraft({
80
+ canvas: {
81
+ enabled: true,
82
+ sizing: { mode: "intrinsic-media" },
83
+ },
84
+ panels: {
85
+ controls: {
86
+ sections: [
87
+ {
88
+ controls: {
89
+ band: {
90
+ defaultValue: [20, 80],
91
+ label: "Band",
92
+ max: 100,
93
+ min: 0,
94
+ performanceReason: "Band changes a lightweight bounded interval.",
95
+ performanceRole: "responsiveness",
96
+ target: "render.band",
97
+ type: "rangeSlider",
98
+ },
99
+ },
100
+ },
101
+ ],
102
+ title: "Runtime Controls",
103
+ },
104
+ },
105
+ });
106
+
79
107
  const largeTextSchema = defineToolcraft({
80
108
  canvas: {
81
109
  enabled: true,
@@ -102,6 +130,34 @@ const largeTextSchema = defineToolcraft({
102
130
  },
103
131
  });
104
132
 
133
+ const mediaUploadSchema = defineToolcraft({
134
+ canvas: {
135
+ enabled: true,
136
+ sizing: { mode: "intrinsic-media" },
137
+ upload: true,
138
+ },
139
+ panels: {
140
+ controls: {
141
+ sections: [
142
+ {
143
+ controls: {
144
+ source: {
145
+ defaultValue: null,
146
+ label: "Source image",
147
+ performanceReason: "Uploaded media dimensions change renderer workload.",
148
+ performanceRole: "responsiveness",
149
+ target: "source.image",
150
+ type: "fileDrop",
151
+ },
152
+ },
153
+ title: "Source",
154
+ },
155
+ ],
156
+ title: "Runtime Controls",
157
+ },
158
+ },
159
+ });
160
+
105
161
  function createViewportZoomStressScenario() {
106
162
  return {
107
163
  automated: true,
@@ -115,6 +171,7 @@ function createViewportZoomStressScenario() {
115
171
  id: "viewport-zoom-stress",
116
172
  interaction: "viewport-zoom-stress" as const,
117
173
  stress: true,
174
+ stressFixture: createCombinedRendererStressFixture(),
118
175
  target: "canvas.viewport",
119
176
  workload: false,
120
177
  };
@@ -128,6 +185,24 @@ function createMaxValueStressFixture(value: unknown, reason = "Maximum workload
128
185
  };
129
186
  }
130
187
 
188
+ function createCustomStressFixture(
189
+ value: unknown = { density: 12 },
190
+ reason = "Combined renderer stress state.",
191
+ ) {
192
+ return {
193
+ kind: "custom" as const,
194
+ reason,
195
+ value,
196
+ };
197
+ }
198
+
199
+ function createCombinedRendererStressFixture() {
200
+ return createCustomStressFixture(
201
+ { density: 12, zoom: "toolbar" },
202
+ "Stress checks must run with the densest product output visible.",
203
+ );
204
+ }
205
+
131
206
  function createLargeTextStressValue(): string {
132
207
  return Array.from(
133
208
  { length: 1_000 },
@@ -146,6 +221,271 @@ function createLargeTextStressFixture() {
146
221
  };
147
222
  }
148
223
 
224
+ function createMediaStressFixture(
225
+ value: { height: number; width: number } = { height: 1080, width: 1920 },
226
+ reason = "Large uploaded source media is the heaviest realistic import fixture.",
227
+ ) {
228
+ return {
229
+ kind: "media" as const,
230
+ reason,
231
+ value,
232
+ };
233
+ }
234
+
235
+ function createTextRendererPipeline() {
236
+ return {
237
+ interactionInvalidation: [
238
+ {
239
+ interaction: "control-drag",
240
+ invalidates: ["text-layout"],
241
+ targets: ["render.density"],
242
+ },
243
+ {
244
+ interaction: "control-change",
245
+ invalidates: ["text-layout"],
246
+ targets: ["render.mode"],
247
+ },
248
+ {
249
+ interaction: "viewport-zoom",
250
+ invalidates: [],
251
+ mustNotInvalidate: ["text-layout"],
252
+ targets: ["canvas.viewport"],
253
+ },
254
+ ],
255
+ passes: [
256
+ {
257
+ cacheKey: ["product.content", "render.density", "render.mode"],
258
+ id: "text-layout",
259
+ inputs: ["product.content", "render.density", "render.mode"],
260
+ invalidatedBy: ["product.content", "render.density", "render.mode"],
261
+ kind: "text-layout",
262
+ output: "preview",
263
+ quality: "full",
264
+ runsOn: "main",
265
+ },
266
+ ],
267
+ } as const;
268
+ }
269
+
270
+ function createAnimatedVectorRendererPipeline() {
271
+ return {
272
+ interactionInvalidation: [
273
+ {
274
+ interaction: "control-drag",
275
+ invalidates: ["vector-build"],
276
+ targets: ["render.density"],
277
+ },
278
+ {
279
+ interaction: "control-change",
280
+ invalidates: ["vector-build"],
281
+ targets: ["render.mode"],
282
+ },
283
+ {
284
+ interaction: "animation-frame",
285
+ invalidates: ["animation-composite"],
286
+ mustNotInvalidate: ["vector-build"],
287
+ targets: ["animation.time"],
288
+ },
289
+ {
290
+ interaction: "timeline-playback",
291
+ invalidates: ["animation-composite"],
292
+ mustNotInvalidate: ["vector-build"],
293
+ targets: ["timeline.currentTime"],
294
+ },
295
+ {
296
+ interaction: "timeline-scrub",
297
+ invalidates: ["animation-composite"],
298
+ mustNotInvalidate: ["vector-build"],
299
+ targets: ["timeline.currentTime"],
300
+ },
301
+ {
302
+ interaction: "viewport-drag",
303
+ invalidates: [],
304
+ mustNotInvalidate: ["vector-build", "animation-composite"],
305
+ targets: ["canvas.viewport"],
306
+ },
307
+ {
308
+ interaction: "viewport-zoom",
309
+ invalidates: [],
310
+ mustNotInvalidate: ["vector-build", "animation-composite"],
311
+ targets: ["canvas.viewport"],
312
+ },
313
+ ],
314
+ passes: [
315
+ {
316
+ id: "vector-build",
317
+ inputs: ["render.density", "render.mode"],
318
+ invalidatedBy: ["render.density", "render.mode"],
319
+ kind: "vector-build",
320
+ output: "preview",
321
+ quality: "full",
322
+ runsOn: "main",
323
+ },
324
+ {
325
+ cacheKey: ["vector-build", "animation.time", "timeline.currentTime"],
326
+ id: "animation-composite",
327
+ inputs: ["vector-build", "animation.time", "timeline.currentTime"],
328
+ invalidatedBy: ["animation.time", "timeline.currentTime"],
329
+ kind: "composite",
330
+ output: "preview",
331
+ quality: "preview",
332
+ runsOn: "main",
333
+ },
334
+ ],
335
+ } as const;
336
+ }
337
+
338
+ function createDenseCanvasPipeline() {
339
+ return {
340
+ interactionInvalidation: [
341
+ {
342
+ interaction: "control-drag",
343
+ invalidates: ["dense-background", "export-composite"],
344
+ targets: ["render.density"],
345
+ },
346
+ {
347
+ interaction: "control-change",
348
+ invalidates: ["export-composite"],
349
+ targets: ["render.mode"],
350
+ },
351
+ {
352
+ interaction: "viewport-zoom",
353
+ invalidates: [],
354
+ mustNotInvalidate: ["dense-background", "export-composite"],
355
+ targets: ["canvas.viewport"],
356
+ },
357
+ {
358
+ interaction: "export",
359
+ invalidates: ["export-composite"],
360
+ mustNotInvalidate: ["dense-background"],
361
+ targets: ["export.image.resolution"],
362
+ },
363
+ ],
364
+ passes: [
365
+ {
366
+ cacheKey: ["render.density", "canvas.size", "canvas.renderScale"],
367
+ id: "dense-background",
368
+ inputs: ["render.density", "canvas.size", "canvas.renderScale"],
369
+ invalidatedBy: ["render.density", "canvas.size", "canvas.renderScale"],
370
+ kind: "rasterize",
371
+ output: "preview",
372
+ quality: "retina",
373
+ runsOn: "worker",
374
+ },
375
+ {
376
+ id: "semantic-foreground",
377
+ inputs: ["product.geometry"],
378
+ invalidatedBy: ["product.geometry"],
379
+ kind: "vector-build",
380
+ output: "preview",
381
+ quality: "full",
382
+ runsOn: "main",
383
+ },
384
+ {
385
+ cacheKey: ["dense-background", "semantic-foreground", "export.image.resolution"],
386
+ id: "export-composite",
387
+ inputs: ["dense-background", "semantic-foreground", "export.image.resolution"],
388
+ invalidatedBy: ["dense-background", "semantic-foreground", "export.image.resolution"],
389
+ kind: "composite",
390
+ output: "export",
391
+ quality: "export",
392
+ runsOn: "export-only",
393
+ },
394
+ ],
395
+ } as const;
396
+ }
397
+
398
+ function createPixelRendererPipeline() {
399
+ return {
400
+ interactionInvalidation: [
401
+ {
402
+ interaction: "control-drag",
403
+ invalidates: ["pixel-transform"],
404
+ targets: ["render.density"],
405
+ },
406
+ {
407
+ interaction: "control-change",
408
+ invalidates: ["pixel-transform"],
409
+ targets: ["render.mode"],
410
+ },
411
+ {
412
+ interaction: "viewport-zoom",
413
+ invalidates: [],
414
+ mustNotInvalidate: ["pixel-transform"],
415
+ targets: ["canvas.viewport"],
416
+ },
417
+ ],
418
+ passes: [
419
+ {
420
+ cacheKey: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
421
+ id: "pixel-transform",
422
+ inputs: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
423
+ invalidatedBy: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
424
+ kind: "pixel-transform",
425
+ output: "preview",
426
+ quality: "retina",
427
+ runsOn: "worker",
428
+ },
429
+ ],
430
+ } as const;
431
+ }
432
+
433
+ function createMediaRendererPipeline() {
434
+ return {
435
+ interactionInvalidation: [
436
+ {
437
+ interaction: "media-import",
438
+ invalidates: ["source-decode", "media-preprocess"],
439
+ targets: ["source.image"],
440
+ },
441
+ {
442
+ interaction: "control-drag",
443
+ invalidates: ["effect-composite"],
444
+ mustNotInvalidate: ["source-decode", "media-preprocess"],
445
+ targets: ["render.density"],
446
+ },
447
+ {
448
+ interaction: "viewport-zoom",
449
+ invalidates: [],
450
+ mustNotInvalidate: ["source-decode", "media-preprocess", "effect-composite"],
451
+ targets: ["canvas.viewport"],
452
+ },
453
+ ],
454
+ passes: [
455
+ {
456
+ cacheKey: ["source.image.id", "source.image.width", "source.image.height"],
457
+ id: "source-decode",
458
+ inputs: ["source.image"],
459
+ invalidatedBy: ["source.image"],
460
+ kind: "decode",
461
+ output: "source",
462
+ quality: "full",
463
+ runsOn: "worker",
464
+ },
465
+ {
466
+ cacheKey: ["source-decode", "source.image.id"],
467
+ id: "media-preprocess",
468
+ inputs: ["source-decode"],
469
+ invalidatedBy: ["source-decode"],
470
+ kind: "preprocess",
471
+ output: "intermediate",
472
+ quality: "full",
473
+ runsOn: "worker-or-gpu",
474
+ },
475
+ {
476
+ cacheKey: ["media-preprocess", "render.density"],
477
+ id: "effect-composite",
478
+ inputs: ["media-preprocess", "render.density"],
479
+ invalidatedBy: ["media-preprocess", "render.density"],
480
+ kind: "composite",
481
+ output: "preview",
482
+ quality: "retina",
483
+ runsOn: "gpu",
484
+ },
485
+ ],
486
+ } as const;
487
+ }
488
+
149
489
  function createTextRendererScenarios(
150
490
  options: { includeStressPreview?: boolean; includeViewportZoomStress?: boolean } = {},
151
491
  ) {
@@ -164,6 +504,9 @@ function createTextRendererScenarios(
164
504
  id: "text-preview-render",
165
505
  interaction: "preview-render" as const,
166
506
  stress: includeStressPreview,
507
+ ...(includeStressPreview
508
+ ? { stressFixture: createCombinedRendererStressFixture() }
509
+ : {}),
167
510
  workload: false,
168
511
  },
169
512
  {
@@ -183,6 +526,7 @@ function createTextRendererScenarios(
183
526
  ),
184
527
  target: "render.density",
185
528
  values: { default: 4, max: 12, min: 1 },
529
+ workloadFixture: createLargeTextStressFixture(),
186
530
  workload: true,
187
531
  },
188
532
  {
@@ -228,6 +572,10 @@ function createAnimationFrameScenario() {
228
572
  id: "animation-frame-loop",
229
573
  interaction: "animation-frame" as const,
230
574
  stress: true,
575
+ stressFixture: createCustomStressFixture(
576
+ { animation: "fastest", density: 12 },
577
+ "Animation stress must sample the heaviest animated output.",
578
+ ),
231
579
  workload: false,
232
580
  };
233
581
  }
@@ -244,6 +592,10 @@ function createAnimationViewportDragScenario() {
244
592
  id: "animation-viewport-drag",
245
593
  interaction: "animation-viewport-drag" as const,
246
594
  stress: true,
595
+ stressFixture: createCustomStressFixture(
596
+ { animation: "fastest", density: 12 },
597
+ "Animated viewport drag must run while the heaviest animated output is visible.",
598
+ ),
247
599
  target: "canvas.viewport",
248
600
  workload: false,
249
601
  };
@@ -264,6 +616,7 @@ function createAnimatedVectorRendererConfig(options: { includeViewportDrag: bool
264
616
  whyNotAlternativeStrategies: ["svg preserves semantic vector output for this fixture"],
265
617
  },
266
618
  rendererWorkload: "vector-output",
619
+ rendererPipeline: createAnimatedVectorRendererPipeline(),
267
620
  scenarios: [
268
621
  ...createTextRendererScenarios(),
269
622
  createAnimationFrameScenario(),
@@ -389,6 +742,64 @@ describe("Toolcraft template performance contract", () => {
389
742
  expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toEqual([]);
390
743
  });
391
744
 
745
+ it("rejects slider performance coverage that does not drag the real control", () => {
746
+ const config = defineToolcraftPerformance({
747
+ rendererStrategy: "none",
748
+ rendererWorkload: "none",
749
+ scenarios: [
750
+ {
751
+ automated: true,
752
+ automatedTestName: "perf: opacity change stays responsive",
753
+ browser: true,
754
+ browserTestName: "browser perf: opacity change stays responsive",
755
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
756
+ controlLabel: "Opacity",
757
+ expectedObservable: "Changing Opacity stays responsive.",
758
+ fixture: "opacity responsiveness fixture",
759
+ id: "opacity-change",
760
+ interaction: "control-change",
761
+ target: "render.opacity",
762
+ workload: false,
763
+ },
764
+ ],
765
+ usesCustomRenderer: false,
766
+ workloadTargets: [],
767
+ });
768
+
769
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
770
+ "render.opacity is a slider and must have a control-drag performance scenario proving live canvas/product feedback while dragging.",
771
+ );
772
+ });
773
+
774
+ it("rejects range slider performance coverage that does not drag the real control", () => {
775
+ const config = defineToolcraftPerformance({
776
+ rendererStrategy: "none",
777
+ rendererWorkload: "none",
778
+ scenarios: [
779
+ {
780
+ automated: true,
781
+ automatedTestName: "perf: band change stays responsive",
782
+ browser: true,
783
+ browserTestName: "browser perf: band change stays responsive",
784
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
785
+ controlLabel: "Band",
786
+ expectedObservable: "Changing Band stays responsive.",
787
+ fixture: "band responsiveness fixture",
788
+ id: "band-change",
789
+ interaction: "control-change",
790
+ target: "render.band",
791
+ workload: false,
792
+ },
793
+ ],
794
+ usesCustomRenderer: false,
795
+ workloadTargets: [],
796
+ });
797
+
798
+ expect(validateToolcraftPerformanceCoverage(rangeSliderSchema, config)).toContain(
799
+ "render.band is a rangeSlider and must have a control-drag performance scenario proving live canvas/product feedback while dragging.",
800
+ );
801
+ });
802
+
392
803
  it("requires performance-sensitive controls to be listed in workloadTargets", () => {
393
804
  const config = defineToolcraftPerformance({
394
805
  rendererStrategy: "none",
@@ -433,6 +844,54 @@ describe("Toolcraft template performance contract", () => {
433
844
  );
434
845
  });
435
846
 
847
+ it("requires workload slider coverage to use real control drag", () => {
848
+ const config = defineToolcraftPerformance({
849
+ rendererStrategy: "none",
850
+ rendererWorkload: "none",
851
+ scenarios: [
852
+ {
853
+ automated: true,
854
+ automatedTestName: "perf: density change stays responsive",
855
+ browser: true,
856
+ browserTestName: "browser perf: density change stays responsive",
857
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
858
+ controlLabel: "Density",
859
+ expectedObservable: "Changing Density updates the product without blocking the UI.",
860
+ fixture: "runtime density fixture",
861
+ id: "density-change",
862
+ interaction: "control-change",
863
+ target: "render.density",
864
+ values: { default: 4, max: 12, min: 1 },
865
+ workload: true,
866
+ },
867
+ {
868
+ automated: true,
869
+ automatedTestName: "perf: mode change stays responsive",
870
+ browser: true,
871
+ browserTestName: "browser perf: mode change stays responsive",
872
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
873
+ controlLabel: "Mode",
874
+ expectedObservable: "Changing Mode updates the product without blocking the UI.",
875
+ fixture: "runtime mode fixture",
876
+ id: "mode-change",
877
+ interaction: "control-change",
878
+ target: "render.mode",
879
+ values: { default: "soft", max: "sharp", min: "soft" },
880
+ workload: false,
881
+ },
882
+ ],
883
+ usesCustomRenderer: false,
884
+ workloadTargets: ["render.density"],
885
+ });
886
+
887
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
888
+ expect.arrayContaining([
889
+ "render.density must have min/default/max workload performance coverage through a real control-drag scenario.",
890
+ "render.density is a slider and must have a control-drag performance scenario proving live canvas/product feedback while dragging.",
891
+ ]),
892
+ );
893
+ });
894
+
436
895
  it("rejects responsiveness role on semantically workload controls", () => {
437
896
  const misclassifiedSchema = defineToolcraft({
438
897
  canvas: {
@@ -570,98 +1029,415 @@ describe("Toolcraft template performance contract", () => {
570
1029
  );
571
1030
  });
572
1031
 
573
- it("rejects short stress fixtures for large text workload scenarios", () => {
1032
+ it("requires stress scenarios to declare an exact stress fixture value", () => {
574
1033
  const config = defineToolcraftPerformance({
575
1034
  rendererStrategy: "none",
576
1035
  rendererWorkload: "none",
577
1036
  scenarios: [
578
1037
  {
579
1038
  automated: true,
580
- automatedTestName: "perf: content input stays responsive",
1039
+ automatedTestName: "perf: preview render stays under budget",
581
1040
  browser: true,
582
- browserTestName: "browser perf: content input stays responsive",
583
- budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
584
- controlLabel: "Content",
585
- expectedObservable: "Large text content updates without blocking the UI.",
586
- fixture: "large text content fixture",
587
- id: "content-change",
588
- interaction: "control-change",
589
- stressFixture: {
590
- kind: "large-text",
591
- minChars: 50_000,
592
- minLines: 1_000,
593
- reason: "Long multiline content is the heaviest realistic text workload.",
594
- value: "short text",
595
- },
596
- target: "product.content",
597
- values: { default: "", max: "short text", min: "" },
598
- workload: true,
1041
+ browserTestName: "browser perf: preview render stays under budget",
1042
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
1043
+ expectedObservable: "Preview renders the heaviest output without freezing.",
1044
+ fixture: "combined heavy renderer fixture",
1045
+ id: "preview-render",
1046
+ interaction: "preview-render",
1047
+ stress: true,
1048
+ workload: false,
599
1049
  },
600
1050
  ],
601
1051
  usesCustomRenderer: false,
602
- workloadTargets: ["product.content"],
1052
+ workloadTargets: [],
603
1053
  });
604
1054
 
605
- expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual(
606
- expect.arrayContaining([
607
- "content-change large-text stressFixture.value must contain at least 50000 characters.",
608
- "content-change large-text stressFixture.value must contain at least 1000 lines.",
609
- ]),
1055
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
1056
+ "preview-render stress scenario must declare stressFixture with the real heaviest state used by browser performance tests.",
610
1057
  );
611
1058
  });
612
1059
 
613
- it("accepts large text workload scenarios with machine-checkable stress fixtures", () => {
1060
+ it("rejects stress fixtures without a browser-applicable value", () => {
614
1061
  const config = defineToolcraftPerformance({
615
1062
  rendererStrategy: "none",
616
1063
  rendererWorkload: "none",
617
1064
  scenarios: [
618
1065
  {
619
1066
  automated: true,
620
- automatedTestName: "perf: content input stays responsive",
1067
+ automatedTestName: "perf: preview render stays under budget",
621
1068
  browser: true,
622
- browserTestName: "browser perf: content input stays responsive",
623
- budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
624
- controlLabel: "Content",
625
- expectedObservable: "Large text content updates without blocking the UI.",
626
- fixture: "large text content fixture",
627
- id: "content-change",
628
- interaction: "control-change",
629
- stressFixture: createLargeTextStressFixture(),
630
- target: "product.content",
631
- values: { default: "", max: createLargeTextStressValue(), min: "" },
632
- workload: true,
1069
+ browserTestName: "browser perf: preview render stays under budget",
1070
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
1071
+ expectedObservable: "Preview renders the heaviest output without freezing.",
1072
+ fixture: "combined heavy renderer fixture",
1073
+ id: "preview-render",
1074
+ interaction: "preview-render",
1075
+ stress: true,
1076
+ stressFixture: {
1077
+ kind: "custom",
1078
+ reason: "Combined renderer stress state.",
1079
+ },
1080
+ workload: false,
633
1081
  },
634
1082
  ],
635
1083
  usesCustomRenderer: false,
636
- workloadTargets: ["product.content"],
1084
+ workloadTargets: [],
637
1085
  });
638
1086
 
639
- expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual([]);
1087
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
1088
+ "preview-render stressFixture must include value so browser tests can apply the exact heavy state.",
1089
+ );
640
1090
  });
641
1091
 
642
- it("rejects visible controls without performance scenarios", () => {
1092
+ it("requires custom stress fixtures to be object-shaped heavy states", () => {
643
1093
  const config = defineToolcraftPerformance({
644
1094
  rendererStrategy: "none",
645
1095
  rendererWorkload: "none",
646
1096
  scenarios: [
647
1097
  {
648
1098
  automated: true,
649
- automatedTestName: "perf: density drag stays responsive",
1099
+ automatedTestName: "perf: preview render stays under budget",
650
1100
  browser: true,
651
- browserTestName: "browser perf: density drag stays responsive",
652
- budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
653
- controlLabel: "Density",
654
- expectedObservable: "Dragging Density updates the product without blocking the UI.",
655
- fixture: "runtime density fixture",
656
- id: "density-drag",
657
- interaction: "control-drag",
658
- target: "render.density",
659
- values: { default: 4, max: 12, min: 1 },
660
- workload: true,
1101
+ browserTestName: "browser perf: preview render stays under budget",
1102
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
1103
+ expectedObservable: "Preview renders the heaviest output without freezing.",
1104
+ fixture: "combined heavy renderer fixture",
1105
+ id: "preview-render",
1106
+ interaction: "preview-render",
1107
+ stress: true,
1108
+ stressFixture: {
1109
+ kind: "custom",
1110
+ reason: "Combined renderer stress state.",
1111
+ value: 12,
1112
+ },
1113
+ workload: false,
661
1114
  },
662
1115
  ],
663
1116
  usesCustomRenderer: false,
664
- workloadTargets: ["render.density"],
1117
+ workloadTargets: [],
1118
+ });
1119
+
1120
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
1121
+ "preview-render custom stressFixture.value must be an object with one key per heavy state part so browser tests can apply every key.",
1122
+ );
1123
+ });
1124
+
1125
+ it("rejects empty custom stress fixture objects", () => {
1126
+ const config = defineToolcraftPerformance({
1127
+ rendererStrategy: "none",
1128
+ rendererWorkload: "none",
1129
+ scenarios: [
1130
+ {
1131
+ automated: true,
1132
+ automatedTestName: "perf: preview render stays under budget",
1133
+ browser: true,
1134
+ browserTestName: "browser perf: preview render stays under budget",
1135
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
1136
+ expectedObservable: "Preview renders the heaviest output without freezing.",
1137
+ fixture: "combined heavy renderer fixture",
1138
+ id: "preview-render",
1139
+ interaction: "preview-render",
1140
+ stress: true,
1141
+ stressFixture: {
1142
+ kind: "custom",
1143
+ reason: "Combined renderer stress state.",
1144
+ value: {},
1145
+ },
1146
+ workload: false,
1147
+ },
1148
+ ],
1149
+ usesCustomRenderer: false,
1150
+ workloadTargets: [],
1151
+ });
1152
+
1153
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
1154
+ "preview-render custom stressFixture.value must include at least one heavy state key.",
1155
+ );
1156
+ });
1157
+
1158
+ it("rejects short stress fixtures for large text workload scenarios", () => {
1159
+ const config = defineToolcraftPerformance({
1160
+ rendererStrategy: "none",
1161
+ rendererWorkload: "none",
1162
+ scenarios: [
1163
+ {
1164
+ automated: true,
1165
+ automatedTestName: "perf: content input stays responsive",
1166
+ browser: true,
1167
+ browserTestName: "browser perf: content input stays responsive",
1168
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1169
+ controlLabel: "Content",
1170
+ expectedObservable: "Large text content updates without blocking the UI.",
1171
+ fixture: "large text content fixture",
1172
+ id: "content-change",
1173
+ interaction: "control-change",
1174
+ stressFixture: {
1175
+ kind: "large-text",
1176
+ minChars: 50_000,
1177
+ minLines: 1_000,
1178
+ reason: "Long multiline content is the heaviest realistic text workload.",
1179
+ value: "short text",
1180
+ },
1181
+ target: "product.content",
1182
+ values: { default: "", max: "short text", min: "" },
1183
+ workload: true,
1184
+ },
1185
+ ],
1186
+ usesCustomRenderer: false,
1187
+ workloadTargets: ["product.content"],
1188
+ });
1189
+
1190
+ expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual(
1191
+ expect.arrayContaining([
1192
+ "content-change large-text stressFixture.value must contain at least 50000 characters.",
1193
+ "content-change large-text stressFixture.value must contain at least 1000 lines.",
1194
+ ]),
1195
+ );
1196
+ });
1197
+
1198
+ it("accepts large text workload scenarios with machine-checkable stress fixtures", () => {
1199
+ const config = defineToolcraftPerformance({
1200
+ rendererStrategy: "none",
1201
+ rendererWorkload: "none",
1202
+ scenarios: [
1203
+ {
1204
+ automated: true,
1205
+ automatedTestName: "perf: content input stays responsive",
1206
+ browser: true,
1207
+ browserTestName: "browser perf: content input stays responsive",
1208
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1209
+ controlLabel: "Content",
1210
+ expectedObservable: "Large text content updates without blocking the UI.",
1211
+ fixture: "large text content fixture",
1212
+ id: "content-change",
1213
+ interaction: "control-change",
1214
+ stressFixture: createLargeTextStressFixture(),
1215
+ target: "product.content",
1216
+ values: { default: "", max: createLargeTextStressValue(), min: "" },
1217
+ workload: true,
1218
+ },
1219
+ ],
1220
+ usesCustomRenderer: false,
1221
+ workloadTargets: ["product.content"],
1222
+ });
1223
+
1224
+ expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual([]);
1225
+ });
1226
+
1227
+ it("requires workload controls in custom renderers to declare an independent workload fixture", () => {
1228
+ const config = defineToolcraftPerformance({
1229
+ rendererStrategy: "canvas-2d",
1230
+ rendererWorkload: "text-output",
1231
+ scenarios: [
1232
+ {
1233
+ automated: true,
1234
+ automatedTestName: "perf: density drag stays responsive",
1235
+ browser: true,
1236
+ browserTestName: "browser perf: density drag stays responsive",
1237
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1238
+ controlLabel: "Density",
1239
+ expectedObservable: "Dragging Density updates text output without blocking the UI.",
1240
+ fixture: "runtime density fixture",
1241
+ id: "density-drag",
1242
+ interaction: "control-drag",
1243
+ stressFixture: createMaxValueStressFixture(
1244
+ 12,
1245
+ "Density max is the heaviest control value.",
1246
+ ),
1247
+ target: "render.density",
1248
+ values: { default: 4, max: 12, min: 1 },
1249
+ workload: true,
1250
+ },
1251
+ ],
1252
+ usesCustomRenderer: true,
1253
+ workloadTargets: ["render.density"],
1254
+ });
1255
+
1256
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1257
+ "density-drag workload control scenario must declare workloadFixture for the app's heavy baseline state; stressFixture covers the control value only.",
1258
+ );
1259
+ });
1260
+
1261
+ it("rejects toy media workload fixtures for control-drag baselines", () => {
1262
+ const config = defineToolcraftPerformance({
1263
+ rendererStrategy: "webgl",
1264
+ rendererWorkload: "pixel-output",
1265
+ scenarios: [
1266
+ {
1267
+ automated: true,
1268
+ automatedTestName: "perf: density drag stays responsive",
1269
+ browser: true,
1270
+ browserTestName: "browser perf: density drag stays responsive",
1271
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1272
+ controlLabel: "Density",
1273
+ expectedObservable: "Dragging Density updates source media output without blocking the UI.",
1274
+ fixture: "runtime density fixture",
1275
+ id: "density-drag",
1276
+ interaction: "control-drag",
1277
+ stressFixture: createMaxValueStressFixture(
1278
+ 12,
1279
+ "Density max is the heaviest control value.",
1280
+ ),
1281
+ target: "render.density",
1282
+ values: { default: 4, max: 12, min: 1 },
1283
+ workloadFixture: createMediaStressFixture(
1284
+ { height: 480, width: 640 },
1285
+ "This small source should be rejected as too light.",
1286
+ ),
1287
+ workload: true,
1288
+ },
1289
+ ],
1290
+ usesCustomRenderer: true,
1291
+ workloadTargets: ["render.density"],
1292
+ });
1293
+
1294
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1295
+ "density-drag media workloadFixture.value must be at least 1920x1080-equivalent; received 640x480.",
1296
+ );
1297
+ });
1298
+
1299
+ it("accepts workload controls with separate control stress and app baseline fixtures", () => {
1300
+ const config = defineToolcraftPerformance({
1301
+ rendererStrategy: "canvas-2d",
1302
+ rendererTechnique: {
1303
+ exportRenderer: "canvas-2d",
1304
+ fidelityRisks: ["text output must stay crisp"],
1305
+ performanceRisks: ["density changes can relayout text"],
1306
+ previewRenderer: "canvas-2d",
1307
+ productRepresentation: "text",
1308
+ rendererStrategy: "canvas-2d",
1309
+ rendererWorkload: "text-output",
1310
+ sourceRepresentation: "dom-text",
1311
+ whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
1312
+ },
1313
+ rendererWorkload: "text-output",
1314
+ rendererPipeline: createTextRendererPipeline(),
1315
+ scenarios: createTextRendererScenarios(),
1316
+ usesCustomRenderer: true,
1317
+ workloadTargets: ["render.density"],
1318
+ });
1319
+
1320
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
1321
+ });
1322
+
1323
+ it("rejects workload fixtures that are not paired with scenario stress fixtures", () => {
1324
+ const config = defineToolcraftPerformance({
1325
+ rendererStrategy: "none",
1326
+ rendererWorkload: "none",
1327
+ scenarios: [
1328
+ {
1329
+ automated: true,
1330
+ automatedTestName: "perf: preview baseline stays responsive",
1331
+ browser: true,
1332
+ browserTestName: "browser perf: preview baseline stays responsive",
1333
+ budget: { maxPreviewMs: 1000 },
1334
+ expectedObservable: "Preview renders inside a heavy baseline.",
1335
+ fixture: "baseline fixture",
1336
+ id: "preview-heavy-baseline",
1337
+ interaction: "preview-render",
1338
+ workload: false,
1339
+ workloadFixture: createLargeTextStressFixture(),
1340
+ },
1341
+ ],
1342
+ usesCustomRenderer: false,
1343
+ workloadTargets: [],
1344
+ });
1345
+
1346
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1347
+ "preview-heavy-baseline workloadFixture must be paired with stressFixture so tests apply a heavy baseline and then the measured scenario value.",
1348
+ );
1349
+ });
1350
+
1351
+ it("rejects media-import scenarios that use toy source dimensions", () => {
1352
+ const config = defineToolcraftPerformance({
1353
+ rendererStrategy: "none",
1354
+ rendererWorkload: "none",
1355
+ scenarios: [
1356
+ {
1357
+ automated: true,
1358
+ automatedTestName: "perf: source image import stays responsive",
1359
+ browser: true,
1360
+ browserTestName: "browser perf: source image import stays responsive",
1361
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1362
+ expectedObservable: "Importing source media does not block the app.",
1363
+ fixture: "large source image fixture",
1364
+ id: "source-media-import",
1365
+ interaction: "media-import",
1366
+ stressFixture: createMediaStressFixture(
1367
+ { height: 480, width: 640 },
1368
+ "This small fixture should be rejected as too light.",
1369
+ ),
1370
+ target: "source.image",
1371
+ workload: true,
1372
+ },
1373
+ ],
1374
+ usesCustomRenderer: false,
1375
+ workloadTargets: [],
1376
+ });
1377
+
1378
+ expect(validateToolcraftPerformanceCoverage(mediaUploadSchema, config)).toContain(
1379
+ "source-media-import media stressFixture.value must be at least 1920x1080-equivalent; received 640x480.",
1380
+ );
1381
+ });
1382
+
1383
+ it("requires media-import scenarios to be workload media fixtures", () => {
1384
+ const config = defineToolcraftPerformance({
1385
+ rendererStrategy: "none",
1386
+ rendererWorkload: "none",
1387
+ scenarios: [
1388
+ {
1389
+ automated: true,
1390
+ automatedTestName: "perf: source image import stays responsive",
1391
+ browser: true,
1392
+ browserTestName: "browser perf: source image import stays responsive",
1393
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1394
+ expectedObservable: "Importing source media does not block the app.",
1395
+ fixture: "large source image fixture",
1396
+ id: "source-media-import",
1397
+ interaction: "media-import",
1398
+ stressFixture: createCustomStressFixture(
1399
+ { height: 1080, width: 1920 },
1400
+ "Wrong fixture kind for media import.",
1401
+ ),
1402
+ target: "source.image",
1403
+ workload: false,
1404
+ },
1405
+ ],
1406
+ usesCustomRenderer: false,
1407
+ workloadTargets: [],
1408
+ });
1409
+
1410
+ expect(validateToolcraftPerformanceCoverage(mediaUploadSchema, config)).toEqual(
1411
+ expect.arrayContaining([
1412
+ "source-media-import media-import scenario must set workload true because decoded source size changes renderer workload.",
1413
+ 'source-media-import media-import scenario must use stressFixture.kind "media" with a realistic uploaded source size.',
1414
+ ]),
1415
+ );
1416
+ });
1417
+
1418
+ it("rejects visible controls without performance scenarios", () => {
1419
+ const config = defineToolcraftPerformance({
1420
+ rendererStrategy: "none",
1421
+ rendererWorkload: "none",
1422
+ scenarios: [
1423
+ {
1424
+ automated: true,
1425
+ automatedTestName: "perf: density drag stays responsive",
1426
+ browser: true,
1427
+ browserTestName: "browser perf: density drag stays responsive",
1428
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1429
+ controlLabel: "Density",
1430
+ expectedObservable: "Dragging Density updates the product without blocking the UI.",
1431
+ fixture: "runtime density fixture",
1432
+ id: "density-drag",
1433
+ interaction: "control-drag",
1434
+ target: "render.density",
1435
+ values: { default: 4, max: 12, min: 1 },
1436
+ workload: true,
1437
+ },
1438
+ ],
1439
+ usesCustomRenderer: false,
1440
+ workloadTargets: ["render.density"],
665
1441
  });
666
1442
 
667
1443
  expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
@@ -684,6 +1460,7 @@ describe("Toolcraft template performance contract", () => {
684
1460
  whyNotAlternativeStrategies: ["webgl texture output would rasterize text too early"],
685
1461
  },
686
1462
  rendererWorkload: "text-output",
1463
+ rendererPipeline: createTextRendererPipeline(),
687
1464
  scenarios: createTextRendererScenarios(),
688
1465
  usesCustomRenderer: true,
689
1466
  workloadTargets: ["render.density"],
@@ -812,9 +1589,76 @@ describe("Toolcraft template performance contract", () => {
812
1589
  );
813
1590
  });
814
1591
 
815
- it("accepts animated custom renderers with stress coverage while dragging the viewport", () => {
816
- const config = createAnimatedVectorRendererConfig({ includeViewportDrag: true });
817
-
1592
+ it("requires detail-heavy Canvas 2D renderers to document measured WebGL/WebGPU evaluation", () => {
1593
+ const config = defineToolcraftPerformance({
1594
+ rendererStrategy: "canvas-2d",
1595
+ rendererTechnique: {
1596
+ exportRenderer: "canvas-2d",
1597
+ fidelityRisks: ["dense texture output must match source media colors"],
1598
+ layers: [
1599
+ {
1600
+ content: ["bitmap-media", "noise"],
1601
+ exportMode: "included",
1602
+ id: "media-effect",
1603
+ kind: "product-foreground",
1604
+ primitiveCount: "high",
1605
+ renderer: "canvas-2d",
1606
+ uiSelector: '[data-toolcraft-renderer-layer="media-effect"]',
1607
+ },
1608
+ ],
1609
+ performanceRisks: ["large uploaded media can require millions of pixel operations"],
1610
+ previewRenderer: "canvas-2d",
1611
+ productRepresentation: "pixel",
1612
+ rendererStrategy: "canvas-2d",
1613
+ rendererWorkload: "pixel-output",
1614
+ sourceRepresentation: "image-media",
1615
+ whyNotAlternativeStrategies: ["canvas is convenient"],
1616
+ },
1617
+ rendererWorkload: "pixel-output",
1618
+ scenarios: createTextRendererScenarios(),
1619
+ usesCustomRenderer: true,
1620
+ workloadTargets: ["render.density"],
1621
+ });
1622
+
1623
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1624
+ "Detail-heavy Canvas 2D renderers must evaluate WebGL/WebGPU in rendererTechnique.whyNotAlternativeStrategies or performanceRisks with measured stress evidence before keeping the pixel work on CPU.",
1625
+ );
1626
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1627
+ 'rendererWorkload "pixel-output" should use rendererStrategy "webgl" or "webgpu", received "canvas-2d". Keeping a CPU renderer requires measured WebGL/WebGPU evidence in rendererTechnique.whyNotAlternativeStrategies or performanceRisks.',
1628
+ );
1629
+ });
1630
+
1631
+ it("allows pixel-output Canvas 2D only with measured WebGL/WebGPU evidence", () => {
1632
+ const config = defineToolcraftPerformance({
1633
+ rendererStrategy: "canvas-2d",
1634
+ rendererTechnique: {
1635
+ exportRenderer: "canvas-2d",
1636
+ fidelityRisks: ["pixel output must preserve exact color stops"],
1637
+ performanceRisks: [
1638
+ "Measured WebGL stress evidence showed setup overhead exceeded cached Canvas 2D worker output for this static fixture.",
1639
+ ],
1640
+ previewRenderer: "canvas-2d",
1641
+ productRepresentation: "pixel",
1642
+ rendererStrategy: "canvas-2d",
1643
+ rendererWorkload: "pixel-output",
1644
+ sourceRepresentation: "procedural-data",
1645
+ whyNotAlternativeStrategies: [
1646
+ "WebGL measured stress evidence stayed within budget but did not improve interaction latency for this renderer.",
1647
+ ],
1648
+ },
1649
+ rendererWorkload: "pixel-output",
1650
+ rendererPipeline: createPixelRendererPipeline(),
1651
+ scenarios: createTextRendererScenarios(),
1652
+ usesCustomRenderer: true,
1653
+ workloadTargets: ["render.density"],
1654
+ });
1655
+
1656
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
1657
+ });
1658
+
1659
+ it("accepts animated custom renderers with stress coverage while dragging the viewport", () => {
1660
+ const config = createAnimatedVectorRendererConfig({ includeViewportDrag: true });
1661
+
818
1662
  expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
819
1663
  });
820
1664
 
@@ -834,6 +1678,528 @@ describe("Toolcraft template performance contract", () => {
834
1678
  );
835
1679
  });
836
1680
 
1681
+ it("requires custom renderers to declare a render pipeline inventory", () => {
1682
+ const config = defineToolcraftPerformance({
1683
+ rendererStrategy: "canvas-2d",
1684
+ rendererTechnique: {
1685
+ exportRenderer: "canvas-2d",
1686
+ fidelityRisks: ["canvas output can soften edges"],
1687
+ performanceRisks: ["dense output can jank during drag"],
1688
+ previewRenderer: "canvas-2d",
1689
+ productRepresentation: "mixed",
1690
+ rendererStrategy: "canvas-2d",
1691
+ rendererWorkload: "simple-composition",
1692
+ sourceRepresentation: "procedural-data",
1693
+ whyNotAlternativeStrategies: [
1694
+ "svg cannot preserve dense procedural background output",
1695
+ ],
1696
+ },
1697
+ rendererWorkload: "simple-composition",
1698
+ scenarios: createTextRendererScenarios(),
1699
+ usesCustomRenderer: true,
1700
+ workloadTargets: ["render.density"],
1701
+ });
1702
+
1703
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1704
+ "Custom renderers must declare rendererPipeline so render passes, cache keys, and invalidation are machine-checkable.",
1705
+ );
1706
+ });
1707
+
1708
+ it("rejects vague renderer pipeline pass dependencies", () => {
1709
+ const config = defineToolcraftPerformance({
1710
+ rendererStrategy: "canvas-2d",
1711
+ rendererTechnique: {
1712
+ exportRenderer: "canvas-2d",
1713
+ fidelityRisks: ["dense text output can soften at small sizes"],
1714
+ performanceRisks: ["text layout can be expensive"],
1715
+ previewRenderer: "canvas-2d",
1716
+ productRepresentation: "text",
1717
+ rendererStrategy: "canvas-2d",
1718
+ rendererWorkload: "text-output",
1719
+ sourceRepresentation: "dom-text",
1720
+ whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
1721
+ },
1722
+ rendererWorkload: "text-output",
1723
+ rendererPipeline: {
1724
+ interactionInvalidation: [
1725
+ {
1726
+ interaction: "control-drag",
1727
+ invalidates: ["text-layout"],
1728
+ targets: ["render.density"],
1729
+ },
1730
+ ],
1731
+ passes: [
1732
+ {
1733
+ cacheKey: ["state"],
1734
+ id: "text-layout",
1735
+ inputs: ["state"],
1736
+ invalidatedBy: ["all values"],
1737
+ kind: "text-layout",
1738
+ output: "preview",
1739
+ quality: "full",
1740
+ runsOn: "main",
1741
+ },
1742
+ ],
1743
+ },
1744
+ scenarios: createTextRendererScenarios(),
1745
+ usesCustomRenderer: true,
1746
+ workloadTargets: ["render.density"],
1747
+ });
1748
+
1749
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
1750
+ expect.arrayContaining([
1751
+ 'rendererPipeline pass "text-layout" inputs entry "state" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
1752
+ 'rendererPipeline pass "text-layout" invalidatedBy entry "all values" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
1753
+ 'rendererPipeline pass "text-layout" cacheKey entry "state" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
1754
+ ]),
1755
+ );
1756
+ });
1757
+
1758
+ it("requires cache-sensitive renderer pipeline passes to declare cache keys", () => {
1759
+ const config = defineToolcraftPerformance({
1760
+ rendererStrategy: "canvas-2d",
1761
+ rendererTechnique: {
1762
+ exportRenderer: "canvas-2d",
1763
+ fidelityRisks: ["source pixels must stay crisp"],
1764
+ performanceRisks: ["pixel transforms can block the main thread"],
1765
+ previewRenderer: "canvas-2d",
1766
+ productRepresentation: "pixel",
1767
+ rendererStrategy: "canvas-2d",
1768
+ rendererWorkload: "pixel-output",
1769
+ sourceRepresentation: "image-media",
1770
+ whyNotAlternativeStrategies: [
1771
+ "WebGL/WebGPU measured performance evidence showed no improvement for this fixture",
1772
+ ],
1773
+ },
1774
+ rendererWorkload: "pixel-output",
1775
+ rendererPipeline: {
1776
+ interactionInvalidation: [
1777
+ {
1778
+ interaction: "control-drag",
1779
+ invalidates: ["pixel-pass"],
1780
+ targets: ["render.density"],
1781
+ },
1782
+ ],
1783
+ passes: [
1784
+ {
1785
+ id: "pixel-pass",
1786
+ inputs: ["source.image", "render.density"],
1787
+ invalidatedBy: ["source.image", "render.density"],
1788
+ kind: "pixel-transform",
1789
+ output: "preview",
1790
+ quality: "retina",
1791
+ runsOn: "worker",
1792
+ },
1793
+ ],
1794
+ },
1795
+ scenarios: createTextRendererScenarios(),
1796
+ usesCustomRenderer: true,
1797
+ workloadTargets: ["render.density"],
1798
+ });
1799
+
1800
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1801
+ 'rendererPipeline pass "pixel-pass" is a cache-sensitive pixel-transform pass and must declare cacheKey so tests can reject full recomputation on every control change.',
1802
+ );
1803
+ });
1804
+
1805
+ it("requires composite renderer pipeline passes to declare cache keys", () => {
1806
+ const config = defineToolcraftPerformance({
1807
+ rendererStrategy: "svg",
1808
+ rendererTechnique: {
1809
+ exportRenderer: "svg",
1810
+ fidelityRisks: ["animated composite must preserve vector output"],
1811
+ performanceRisks: ["compositing can be rebuilt too often"],
1812
+ previewRenderer: "svg",
1813
+ productRepresentation: "vector",
1814
+ rendererStrategy: "svg",
1815
+ rendererWorkload: "vector-output",
1816
+ sourceRepresentation: "procedural-data",
1817
+ whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
1818
+ },
1819
+ rendererWorkload: "vector-output",
1820
+ rendererPipeline: {
1821
+ ...createAnimatedVectorRendererPipeline(),
1822
+ passes: createAnimatedVectorRendererPipeline().passes.map((pass) =>
1823
+ pass.id === "animation-composite"
1824
+ ? {
1825
+ id: pass.id,
1826
+ inputs: pass.inputs,
1827
+ invalidatedBy: pass.invalidatedBy,
1828
+ kind: pass.kind,
1829
+ output: pass.output,
1830
+ quality: pass.quality,
1831
+ runsOn: pass.runsOn,
1832
+ }
1833
+ : pass,
1834
+ ),
1835
+ },
1836
+ scenarios: createTextRendererScenarios(),
1837
+ usesCustomRenderer: true,
1838
+ workloadTargets: ["render.density"],
1839
+ });
1840
+
1841
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1842
+ 'rendererPipeline pass "animation-composite" is a cache-sensitive composite pass and must declare cacheKey so tests can reject full recomputation on every control change.',
1843
+ );
1844
+ });
1845
+
1846
+ it("rejects high-frequency viewport interactions that invalidate expensive passes", () => {
1847
+ const config = defineToolcraftPerformance({
1848
+ rendererStrategy: "canvas-2d",
1849
+ rendererTechnique: {
1850
+ exportRenderer: "canvas-2d",
1851
+ fidelityRisks: ["dense background must stay stable while zooming"],
1852
+ layers: [
1853
+ {
1854
+ content: ["dense-pattern"],
1855
+ exportMode: "included",
1856
+ id: "dense-pattern",
1857
+ kind: "background",
1858
+ primitiveCount: "high",
1859
+ renderer: "canvas-2d",
1860
+ },
1861
+ ],
1862
+ performanceRisks: [
1863
+ "WebGL measured stress evidence was reviewed before keeping dense background on Canvas 2D",
1864
+ ],
1865
+ previewRenderer: "canvas-2d",
1866
+ productRepresentation: "pixel",
1867
+ rendererStrategy: "canvas-2d",
1868
+ rendererWorkload: "pixel-output",
1869
+ sourceRepresentation: "procedural-data",
1870
+ whyNotAlternativeStrategies: [
1871
+ "WebGL stress evidence showed shader setup overhead exceeded static cache reuse",
1872
+ ],
1873
+ },
1874
+ rendererWorkload: "pixel-output",
1875
+ rendererPipeline: {
1876
+ interactionInvalidation: [
1877
+ {
1878
+ interaction: "viewport-zoom",
1879
+ invalidates: ["dense-background"],
1880
+ mustNotInvalidate: ["dense-background"],
1881
+ targets: ["canvas.viewport"],
1882
+ },
1883
+ {
1884
+ interaction: "control-drag",
1885
+ invalidates: ["dense-background"],
1886
+ targets: ["render.density"],
1887
+ },
1888
+ ],
1889
+ passes: [
1890
+ {
1891
+ cacheKey: ["render.density", "canvas.size", "canvas.renderScale"],
1892
+ id: "dense-background",
1893
+ inputs: ["render.density", "canvas.size", "canvas.renderScale"],
1894
+ invalidatedBy: ["render.density", "canvas.size", "canvas.renderScale"],
1895
+ kind: "rasterize",
1896
+ output: "preview",
1897
+ quality: "retina",
1898
+ runsOn: "worker",
1899
+ },
1900
+ ],
1901
+ },
1902
+ scenarios: createTextRendererScenarios(),
1903
+ usesCustomRenderer: true,
1904
+ workloadTargets: ["render.density"],
1905
+ });
1906
+
1907
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
1908
+ expect.arrayContaining([
1909
+ 'rendererPipeline viewport-zoom cannot both invalidate and mustNotInvalidate pass "dense-background".',
1910
+ 'rendererPipeline viewport-zoom must not invalidate expensive pass "dense-background" (rasterize). Move viewport work to transforms/uniforms or explain it through a cheaper pass.',
1911
+ ]),
1912
+ );
1913
+ });
1914
+
1915
+ it("requires media decoding pipelines to include media-import coverage", () => {
1916
+ const config = defineToolcraftPerformance({
1917
+ rendererStrategy: "webgl",
1918
+ rendererTechnique: {
1919
+ exportRenderer: "webgl",
1920
+ fidelityRisks: ["large source media must preserve color"],
1921
+ performanceRisks: ["source decode can be expensive"],
1922
+ previewRenderer: "webgl",
1923
+ productRepresentation: "pixel",
1924
+ rendererStrategy: "webgl",
1925
+ rendererWorkload: "pixel-output",
1926
+ sourceRepresentation: "image-media",
1927
+ whyNotAlternativeStrategies: ["canvas-2d would run per-pixel work on the main thread"],
1928
+ },
1929
+ rendererWorkload: "pixel-output",
1930
+ rendererPipeline: createMediaRendererPipeline(),
1931
+ scenarios: createTextRendererScenarios(),
1932
+ usesCustomRenderer: true,
1933
+ workloadTargets: ["render.density"],
1934
+ });
1935
+
1936
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1937
+ 'rendererPipeline pass "source-decode" decodes media, so performance scenarios must include media-import coverage.',
1938
+ );
1939
+ });
1940
+
1941
+ it("requires workload targets to appear in renderer pipeline invalidation targets", () => {
1942
+ const config = defineToolcraftPerformance({
1943
+ rendererStrategy: "canvas-2d",
1944
+ rendererTechnique: {
1945
+ exportRenderer: "canvas-2d",
1946
+ fidelityRisks: ["text output must stay crisp"],
1947
+ performanceRisks: ["density changes can relayout text"],
1948
+ previewRenderer: "canvas-2d",
1949
+ productRepresentation: "text",
1950
+ rendererStrategy: "canvas-2d",
1951
+ rendererWorkload: "text-output",
1952
+ sourceRepresentation: "dom-text",
1953
+ whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
1954
+ },
1955
+ rendererWorkload: "text-output",
1956
+ rendererPipeline: {
1957
+ ...createTextRendererPipeline(),
1958
+ interactionInvalidation: [
1959
+ {
1960
+ interaction: "control-change",
1961
+ invalidates: ["text-layout"],
1962
+ targets: ["render.mode"],
1963
+ },
1964
+ ],
1965
+ },
1966
+ scenarios: createTextRendererScenarios(),
1967
+ usesCustomRenderer: true,
1968
+ workloadTargets: ["render.density"],
1969
+ });
1970
+
1971
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1972
+ "Performance workload target render.density must appear in rendererPipeline interactionInvalidation targets.",
1973
+ );
1974
+ });
1975
+
1976
+ it("requires performance scenario interactions to appear in renderer pipeline invalidation", () => {
1977
+ const config = defineToolcraftPerformance({
1978
+ rendererStrategy: "canvas-2d",
1979
+ rendererTechnique: {
1980
+ exportRenderer: "canvas-2d",
1981
+ fidelityRisks: ["text output must stay crisp"],
1982
+ performanceRisks: ["density changes can relayout text"],
1983
+ previewRenderer: "canvas-2d",
1984
+ productRepresentation: "text",
1985
+ rendererStrategy: "canvas-2d",
1986
+ rendererWorkload: "text-output",
1987
+ sourceRepresentation: "dom-text",
1988
+ whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
1989
+ },
1990
+ rendererWorkload: "text-output",
1991
+ rendererPipeline: {
1992
+ ...createTextRendererPipeline(),
1993
+ interactionInvalidation: [
1994
+ {
1995
+ interaction: "control-drag",
1996
+ invalidates: ["text-layout"],
1997
+ targets: ["render.density"],
1998
+ },
1999
+ {
2000
+ interaction: "control-change",
2001
+ invalidates: ["text-layout"],
2002
+ targets: ["render.mode"],
2003
+ },
2004
+ ],
2005
+ },
2006
+ scenarios: createTextRendererScenarios(),
2007
+ usesCustomRenderer: true,
2008
+ workloadTargets: ["render.density"],
2009
+ });
2010
+
2011
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
2012
+ "Performance scenario viewport-zoom-stress exercises viewport-zoom, so rendererPipeline.interactionInvalidation must declare that interaction.",
2013
+ );
2014
+ });
2015
+
2016
+ it("requires timeline playback scenarios to appear in renderer pipeline invalidation", () => {
2017
+ const config = defineToolcraftPerformance({
2018
+ rendererStrategy: "svg",
2019
+ rendererTechnique: {
2020
+ exportRenderer: "svg",
2021
+ fidelityRisks: ["timeline playback must preserve vector motion"],
2022
+ performanceRisks: ["timeline playback can jank at high density"],
2023
+ previewRenderer: "svg",
2024
+ productRepresentation: "vector",
2025
+ rendererStrategy: "svg",
2026
+ rendererWorkload: "vector-output",
2027
+ sourceRepresentation: "procedural-data",
2028
+ whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
2029
+ },
2030
+ rendererWorkload: "vector-output",
2031
+ rendererPipeline: {
2032
+ ...createAnimatedVectorRendererPipeline(),
2033
+ interactionInvalidation: createAnimatedVectorRendererPipeline().interactionInvalidation.filter(
2034
+ (entry) => entry.interaction !== "timeline-playback",
2035
+ ),
2036
+ },
2037
+ scenarios: [
2038
+ ...createTextRendererScenarios(),
2039
+ {
2040
+ automated: true,
2041
+ automatedTestName: "perf: timeline playback stays smooth",
2042
+ browser: true,
2043
+ browserTestName: "browser perf: timeline playback stays smooth",
2044
+ budget: { maxFrameGapMs: 80, maxLongTaskMs: 80 },
2045
+ expectedObservable: "Timeline playback advances without blocking frames.",
2046
+ fixture: "timeline playback fixture",
2047
+ id: "timeline-playback",
2048
+ interaction: "timeline-playback",
2049
+ stress: true,
2050
+ stressFixture: createCombinedRendererStressFixture(),
2051
+ target: "timeline.currentTime",
2052
+ uiSelector: '[data-slot="timeline-playback-handle"]',
2053
+ workload: false,
2054
+ },
2055
+ ],
2056
+ usesCustomRenderer: true,
2057
+ workloadTargets: ["render.density"],
2058
+ });
2059
+
2060
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
2061
+ "Performance scenario timeline-playback exercises timeline-playback, so rendererPipeline.interactionInvalidation must declare that interaction.",
2062
+ );
2063
+ });
2064
+
2065
+ it("validates timeline scrub scenarios through renderer pipeline invalidation", () => {
2066
+ const config = defineToolcraftPerformance({
2067
+ rendererStrategy: "svg",
2068
+ rendererTechnique: {
2069
+ exportRenderer: "svg",
2070
+ fidelityRisks: ["timeline scrub must preserve vector motion"],
2071
+ performanceRisks: ["timeline scrub can jank at high density"],
2072
+ previewRenderer: "svg",
2073
+ productRepresentation: "vector",
2074
+ rendererStrategy: "svg",
2075
+ rendererWorkload: "vector-output",
2076
+ sourceRepresentation: "procedural-data",
2077
+ whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
2078
+ },
2079
+ rendererWorkload: "vector-output",
2080
+ rendererPipeline: createAnimatedVectorRendererPipeline(),
2081
+ scenarios: [
2082
+ ...createTextRendererScenarios(),
2083
+ {
2084
+ automated: true,
2085
+ automatedTestName: "perf: timeline scrub stays responsive",
2086
+ browser: true,
2087
+ browserTestName: "browser perf: timeline scrub stays responsive",
2088
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
2089
+ expectedObservable: "Timeline scrub updates output without blocking frames.",
2090
+ fixture: "timeline scrub fixture",
2091
+ id: "timeline-scrub",
2092
+ interaction: "timeline-scrub",
2093
+ stress: true,
2094
+ stressFixture: createCombinedRendererStressFixture(),
2095
+ target: "timeline.currentTime",
2096
+ uiSelector: '[data-slot="timeline-playback-handle"]',
2097
+ workload: false,
2098
+ },
2099
+ ],
2100
+ usesCustomRenderer: true,
2101
+ workloadTargets: ["render.density"],
2102
+ });
2103
+
2104
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
2105
+ });
2106
+
2107
+ it("requires timeline performance scenarios to name the real timeline UI", () => {
2108
+ const config = defineToolcraftPerformance({
2109
+ rendererStrategy: "svg",
2110
+ rendererTechnique: {
2111
+ exportRenderer: "svg",
2112
+ fidelityRisks: ["timeline scrub must preserve vector motion"],
2113
+ performanceRisks: ["timeline scrub can jank at high density"],
2114
+ previewRenderer: "svg",
2115
+ productRepresentation: "vector",
2116
+ rendererStrategy: "svg",
2117
+ rendererWorkload: "vector-output",
2118
+ sourceRepresentation: "procedural-data",
2119
+ whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
2120
+ },
2121
+ rendererWorkload: "vector-output",
2122
+ rendererPipeline: createAnimatedVectorRendererPipeline(),
2123
+ scenarios: [
2124
+ ...createTextRendererScenarios(),
2125
+ {
2126
+ automated: true,
2127
+ automatedTestName: "perf: timeline scrub stays responsive",
2128
+ browser: true,
2129
+ browserTestName: "browser perf: timeline scrub stays responsive",
2130
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
2131
+ expectedObservable: "Timeline scrub updates output without blocking frames.",
2132
+ fixture: "timeline scrub fixture",
2133
+ id: "timeline-scrub",
2134
+ interaction: "timeline-scrub",
2135
+ stress: true,
2136
+ stressFixture: createCombinedRendererStressFixture(),
2137
+ target: "timeline.currentTime",
2138
+ workload: false,
2139
+ },
2140
+ ],
2141
+ usesCustomRenderer: true,
2142
+ workloadTargets: ["render.density"],
2143
+ });
2144
+
2145
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
2146
+ "timeline-scrub timeline-scrub scenario must declare controlLabel or uiSelector for its real browser interaction.",
2147
+ );
2148
+ });
2149
+
2150
+ it("validates mask drag scenarios through renderer pipeline invalidation", () => {
2151
+ const config = defineToolcraftPerformance({
2152
+ rendererStrategy: "svg",
2153
+ rendererTechnique: {
2154
+ exportRenderer: "svg",
2155
+ fidelityRisks: ["mask handles must stay aligned with vector output"],
2156
+ performanceRisks: ["dragging a mask handle can rebuild output too often"],
2157
+ previewRenderer: "svg",
2158
+ productRepresentation: "vector",
2159
+ rendererStrategy: "svg",
2160
+ rendererWorkload: "vector-output",
2161
+ sourceRepresentation: "procedural-data",
2162
+ whyNotAlternativeStrategies: ["svg preserves semantic handle geometry"],
2163
+ },
2164
+ rendererWorkload: "vector-output",
2165
+ rendererPipeline: {
2166
+ ...createAnimatedVectorRendererPipeline(),
2167
+ interactionInvalidation: [
2168
+ ...createAnimatedVectorRendererPipeline().interactionInvalidation,
2169
+ {
2170
+ interaction: "mask-drag",
2171
+ invalidates: ["animation-composite"],
2172
+ mustNotInvalidate: ["vector-build"],
2173
+ targets: ["mask.anchor"],
2174
+ },
2175
+ ],
2176
+ },
2177
+ scenarios: [
2178
+ ...createTextRendererScenarios(),
2179
+ {
2180
+ automated: true,
2181
+ automatedTestName: "perf: mask drag stays responsive",
2182
+ browser: true,
2183
+ browserTestName: "browser perf: mask drag stays responsive",
2184
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
2185
+ expectedObservable: "Dragging the mask handle updates output without blocking frames.",
2186
+ fixture: "mask drag fixture",
2187
+ id: "mask-drag",
2188
+ interaction: "mask-drag",
2189
+ stress: true,
2190
+ stressFixture: createCombinedRendererStressFixture(),
2191
+ target: "mask.anchor",
2192
+ uiSelector: '[data-toolcraft-canvas-handle][data-testid="mask-anchor"]',
2193
+ workload: false,
2194
+ },
2195
+ ],
2196
+ usesCustomRenderer: true,
2197
+ workloadTargets: ["render.density"],
2198
+ });
2199
+
2200
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
2201
+ });
2202
+
837
2203
  it("rejects renderer technique mismatches and missing explanations", () => {
838
2204
  const config = defineToolcraftPerformance({
839
2205
  rendererStrategy: "canvas-2d",
@@ -909,9 +2275,11 @@ describe("Toolcraft template performance contract", () => {
909
2275
  sourceRepresentation: "procedural-data",
910
2276
  whyNotAlternativeStrategies: [
911
2277
  "rendering thousands of background dots as DOM nodes would be expensive",
2278
+ "WebGL/WebGPU stress evidence is not needed yet because measured canvas-2d preview budgets pass for this dense static background.",
912
2279
  ],
913
2280
  },
914
2281
  rendererWorkload: "simple-composition",
2282
+ rendererPipeline: createDenseCanvasPipeline(),
915
2283
  scenarios: createTextRendererScenarios(),
916
2284
  usesCustomRenderer: true,
917
2285
  workloadTargets: ["render.density"],