@pixel-point/toolcraft 0.0.7 → 0.0.8

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 (41) hide show
  1. package/package.json +1 -1
  2. package/templates/runtime/contracts/component-contracts.test.ts +84 -12
  3. package/templates/runtime/contracts/component-contracts.ts +40 -17
  4. package/templates/runtime/contracts/decision-contracts.ts +1 -1
  5. package/templates/runtime/export/export.test.ts +31 -0
  6. package/templates/runtime/export/export.ts +41 -0
  7. package/templates/runtime/react/canvas-shell.test.tsx +77 -1
  8. package/templates/runtime/react/canvas-shell.tsx +178 -23
  9. package/templates/runtime/react/control-conditions.ts +166 -0
  10. package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
  11. package/templates/runtime/react/controls-panel.test.tsx +404 -8
  12. package/templates/runtime/react/controls-panel.tsx +90 -4
  13. package/templates/runtime/react/media-file.ts +19 -0
  14. package/templates/runtime/schema/define-toolcraft.test.ts +1 -0
  15. package/templates/runtime/schema/define-toolcraft.ts +4 -2
  16. package/templates/runtime/schema/types.ts +4 -0
  17. package/templates/runtime/state/reducer.test.ts +304 -0
  18. package/templates/runtime/state/reducer.ts +148 -9
  19. package/templates/runtime/state/types.ts +10 -2
  20. package/templates/runtime/testing/performance.test.ts +1282 -48
  21. package/templates/runtime/testing/performance.ts +676 -39
  22. package/templates/starter/AGENTS.md +6 -5
  23. package/templates/starter/docs/toolcraft/acceptance-testing.md +11 -5
  24. package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -2
  25. package/templates/starter/docs/toolcraft/component-rules.md +16 -7
  26. package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
  27. package/templates/starter/docs/toolcraft/performance.md +47 -5
  28. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  29. package/templates/starter/docs/toolcraft/schema-reference.md +6 -6
  30. package/templates/starter/docs/toolcraft/workflow.md +2 -2
  31. package/templates/starter/e2e/app-performance.spec.ts +136 -3
  32. package/templates/starter/e2e/performance-helpers.ts +197 -0
  33. package/templates/starter/package.json +3 -0
  34. package/templates/starter/src/app/starter-acceptance.test.ts +529 -19
  35. package/templates/starter/src/app/starter-acceptance.ts +269 -12
  36. package/templates/starter/src/app/starter-performance.test.ts +66 -5
  37. package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
  38. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
  39. package/templates/ui/components/controls/file-drop/index.ts +1 -1
  40. package/templates/ui/components/controls/index.ts +1 -0
  41. package/templates/ui/components/panel/panel-section.tsx +53 -1
@@ -102,6 +102,34 @@ const largeTextSchema = defineToolcraft({
102
102
  },
103
103
  });
104
104
 
105
+ const mediaUploadSchema = defineToolcraft({
106
+ canvas: {
107
+ enabled: true,
108
+ sizing: { mode: "intrinsic-media" },
109
+ upload: true,
110
+ },
111
+ panels: {
112
+ controls: {
113
+ sections: [
114
+ {
115
+ controls: {
116
+ source: {
117
+ defaultValue: null,
118
+ label: "Source image",
119
+ performanceReason: "Uploaded media dimensions change renderer workload.",
120
+ performanceRole: "responsiveness",
121
+ target: "source.image",
122
+ type: "fileDrop",
123
+ },
124
+ },
125
+ title: "Source",
126
+ },
127
+ ],
128
+ title: "Runtime Controls",
129
+ },
130
+ },
131
+ });
132
+
105
133
  function createViewportZoomStressScenario() {
106
134
  return {
107
135
  automated: true,
@@ -115,6 +143,7 @@ function createViewportZoomStressScenario() {
115
143
  id: "viewport-zoom-stress",
116
144
  interaction: "viewport-zoom-stress" as const,
117
145
  stress: true,
146
+ stressFixture: createCombinedRendererStressFixture(),
118
147
  target: "canvas.viewport",
119
148
  workload: false,
120
149
  };
@@ -128,6 +157,24 @@ function createMaxValueStressFixture(value: unknown, reason = "Maximum workload
128
157
  };
129
158
  }
130
159
 
160
+ function createCustomStressFixture(
161
+ value: unknown = { density: 12 },
162
+ reason = "Combined renderer stress state.",
163
+ ) {
164
+ return {
165
+ kind: "custom" as const,
166
+ reason,
167
+ value,
168
+ };
169
+ }
170
+
171
+ function createCombinedRendererStressFixture() {
172
+ return createCustomStressFixture(
173
+ { density: 12, zoom: "toolbar" },
174
+ "Stress checks must run with the densest product output visible.",
175
+ );
176
+ }
177
+
131
178
  function createLargeTextStressValue(): string {
132
179
  return Array.from(
133
180
  { length: 1_000 },
@@ -146,6 +193,271 @@ function createLargeTextStressFixture() {
146
193
  };
147
194
  }
148
195
 
196
+ function createMediaStressFixture(
197
+ value: { height: number; width: number } = { height: 1080, width: 1920 },
198
+ reason = "Large uploaded source media is the heaviest realistic import fixture.",
199
+ ) {
200
+ return {
201
+ kind: "media" as const,
202
+ reason,
203
+ value,
204
+ };
205
+ }
206
+
207
+ function createTextRendererPipeline() {
208
+ return {
209
+ interactionInvalidation: [
210
+ {
211
+ interaction: "control-drag",
212
+ invalidates: ["text-layout"],
213
+ targets: ["render.density"],
214
+ },
215
+ {
216
+ interaction: "control-change",
217
+ invalidates: ["text-layout"],
218
+ targets: ["render.mode"],
219
+ },
220
+ {
221
+ interaction: "viewport-zoom",
222
+ invalidates: [],
223
+ mustNotInvalidate: ["text-layout"],
224
+ targets: ["canvas.viewport"],
225
+ },
226
+ ],
227
+ passes: [
228
+ {
229
+ cacheKey: ["product.content", "render.density", "render.mode"],
230
+ id: "text-layout",
231
+ inputs: ["product.content", "render.density", "render.mode"],
232
+ invalidatedBy: ["product.content", "render.density", "render.mode"],
233
+ kind: "text-layout",
234
+ output: "preview",
235
+ quality: "full",
236
+ runsOn: "main",
237
+ },
238
+ ],
239
+ } as const;
240
+ }
241
+
242
+ function createAnimatedVectorRendererPipeline() {
243
+ return {
244
+ interactionInvalidation: [
245
+ {
246
+ interaction: "control-drag",
247
+ invalidates: ["vector-build"],
248
+ targets: ["render.density"],
249
+ },
250
+ {
251
+ interaction: "control-change",
252
+ invalidates: ["vector-build"],
253
+ targets: ["render.mode"],
254
+ },
255
+ {
256
+ interaction: "animation-frame",
257
+ invalidates: ["animation-composite"],
258
+ mustNotInvalidate: ["vector-build"],
259
+ targets: ["animation.time"],
260
+ },
261
+ {
262
+ interaction: "timeline-playback",
263
+ invalidates: ["animation-composite"],
264
+ mustNotInvalidate: ["vector-build"],
265
+ targets: ["timeline.currentTime"],
266
+ },
267
+ {
268
+ interaction: "timeline-scrub",
269
+ invalidates: ["animation-composite"],
270
+ mustNotInvalidate: ["vector-build"],
271
+ targets: ["timeline.currentTime"],
272
+ },
273
+ {
274
+ interaction: "viewport-drag",
275
+ invalidates: [],
276
+ mustNotInvalidate: ["vector-build", "animation-composite"],
277
+ targets: ["canvas.viewport"],
278
+ },
279
+ {
280
+ interaction: "viewport-zoom",
281
+ invalidates: [],
282
+ mustNotInvalidate: ["vector-build", "animation-composite"],
283
+ targets: ["canvas.viewport"],
284
+ },
285
+ ],
286
+ passes: [
287
+ {
288
+ id: "vector-build",
289
+ inputs: ["render.density", "render.mode"],
290
+ invalidatedBy: ["render.density", "render.mode"],
291
+ kind: "vector-build",
292
+ output: "preview",
293
+ quality: "full",
294
+ runsOn: "main",
295
+ },
296
+ {
297
+ cacheKey: ["vector-build", "animation.time", "timeline.currentTime"],
298
+ id: "animation-composite",
299
+ inputs: ["vector-build", "animation.time", "timeline.currentTime"],
300
+ invalidatedBy: ["animation.time", "timeline.currentTime"],
301
+ kind: "composite",
302
+ output: "preview",
303
+ quality: "preview",
304
+ runsOn: "main",
305
+ },
306
+ ],
307
+ } as const;
308
+ }
309
+
310
+ function createDenseCanvasPipeline() {
311
+ return {
312
+ interactionInvalidation: [
313
+ {
314
+ interaction: "control-drag",
315
+ invalidates: ["dense-background", "export-composite"],
316
+ targets: ["render.density"],
317
+ },
318
+ {
319
+ interaction: "control-change",
320
+ invalidates: ["export-composite"],
321
+ targets: ["render.mode"],
322
+ },
323
+ {
324
+ interaction: "viewport-zoom",
325
+ invalidates: [],
326
+ mustNotInvalidate: ["dense-background", "export-composite"],
327
+ targets: ["canvas.viewport"],
328
+ },
329
+ {
330
+ interaction: "export",
331
+ invalidates: ["export-composite"],
332
+ mustNotInvalidate: ["dense-background"],
333
+ targets: ["export.image.resolution"],
334
+ },
335
+ ],
336
+ passes: [
337
+ {
338
+ cacheKey: ["render.density", "canvas.size", "canvas.renderScale"],
339
+ id: "dense-background",
340
+ inputs: ["render.density", "canvas.size", "canvas.renderScale"],
341
+ invalidatedBy: ["render.density", "canvas.size", "canvas.renderScale"],
342
+ kind: "rasterize",
343
+ output: "preview",
344
+ quality: "retina",
345
+ runsOn: "worker",
346
+ },
347
+ {
348
+ id: "semantic-foreground",
349
+ inputs: ["product.geometry"],
350
+ invalidatedBy: ["product.geometry"],
351
+ kind: "vector-build",
352
+ output: "preview",
353
+ quality: "full",
354
+ runsOn: "main",
355
+ },
356
+ {
357
+ cacheKey: ["dense-background", "semantic-foreground", "export.image.resolution"],
358
+ id: "export-composite",
359
+ inputs: ["dense-background", "semantic-foreground", "export.image.resolution"],
360
+ invalidatedBy: ["dense-background", "semantic-foreground", "export.image.resolution"],
361
+ kind: "composite",
362
+ output: "export",
363
+ quality: "export",
364
+ runsOn: "export-only",
365
+ },
366
+ ],
367
+ } as const;
368
+ }
369
+
370
+ function createPixelRendererPipeline() {
371
+ return {
372
+ interactionInvalidation: [
373
+ {
374
+ interaction: "control-drag",
375
+ invalidates: ["pixel-transform"],
376
+ targets: ["render.density"],
377
+ },
378
+ {
379
+ interaction: "control-change",
380
+ invalidates: ["pixel-transform"],
381
+ targets: ["render.mode"],
382
+ },
383
+ {
384
+ interaction: "viewport-zoom",
385
+ invalidates: [],
386
+ mustNotInvalidate: ["pixel-transform"],
387
+ targets: ["canvas.viewport"],
388
+ },
389
+ ],
390
+ passes: [
391
+ {
392
+ cacheKey: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
393
+ id: "pixel-transform",
394
+ inputs: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
395
+ invalidatedBy: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
396
+ kind: "pixel-transform",
397
+ output: "preview",
398
+ quality: "retina",
399
+ runsOn: "worker",
400
+ },
401
+ ],
402
+ } as const;
403
+ }
404
+
405
+ function createMediaRendererPipeline() {
406
+ return {
407
+ interactionInvalidation: [
408
+ {
409
+ interaction: "media-import",
410
+ invalidates: ["source-decode", "media-preprocess"],
411
+ targets: ["source.image"],
412
+ },
413
+ {
414
+ interaction: "control-drag",
415
+ invalidates: ["effect-composite"],
416
+ mustNotInvalidate: ["source-decode", "media-preprocess"],
417
+ targets: ["render.density"],
418
+ },
419
+ {
420
+ interaction: "viewport-zoom",
421
+ invalidates: [],
422
+ mustNotInvalidate: ["source-decode", "media-preprocess", "effect-composite"],
423
+ targets: ["canvas.viewport"],
424
+ },
425
+ ],
426
+ passes: [
427
+ {
428
+ cacheKey: ["source.image.id", "source.image.width", "source.image.height"],
429
+ id: "source-decode",
430
+ inputs: ["source.image"],
431
+ invalidatedBy: ["source.image"],
432
+ kind: "decode",
433
+ output: "source",
434
+ quality: "full",
435
+ runsOn: "worker",
436
+ },
437
+ {
438
+ cacheKey: ["source-decode", "source.image.id"],
439
+ id: "media-preprocess",
440
+ inputs: ["source-decode"],
441
+ invalidatedBy: ["source-decode"],
442
+ kind: "preprocess",
443
+ output: "intermediate",
444
+ quality: "full",
445
+ runsOn: "worker-or-gpu",
446
+ },
447
+ {
448
+ cacheKey: ["media-preprocess", "render.density"],
449
+ id: "effect-composite",
450
+ inputs: ["media-preprocess", "render.density"],
451
+ invalidatedBy: ["media-preprocess", "render.density"],
452
+ kind: "composite",
453
+ output: "preview",
454
+ quality: "retina",
455
+ runsOn: "gpu",
456
+ },
457
+ ],
458
+ } as const;
459
+ }
460
+
149
461
  function createTextRendererScenarios(
150
462
  options: { includeStressPreview?: boolean; includeViewportZoomStress?: boolean } = {},
151
463
  ) {
@@ -164,6 +476,9 @@ function createTextRendererScenarios(
164
476
  id: "text-preview-render",
165
477
  interaction: "preview-render" as const,
166
478
  stress: includeStressPreview,
479
+ ...(includeStressPreview
480
+ ? { stressFixture: createCombinedRendererStressFixture() }
481
+ : {}),
167
482
  workload: false,
168
483
  },
169
484
  {
@@ -183,6 +498,7 @@ function createTextRendererScenarios(
183
498
  ),
184
499
  target: "render.density",
185
500
  values: { default: 4, max: 12, min: 1 },
501
+ workloadFixture: createLargeTextStressFixture(),
186
502
  workload: true,
187
503
  },
188
504
  {
@@ -228,6 +544,10 @@ function createAnimationFrameScenario() {
228
544
  id: "animation-frame-loop",
229
545
  interaction: "animation-frame" as const,
230
546
  stress: true,
547
+ stressFixture: createCustomStressFixture(
548
+ { animation: "fastest", density: 12 },
549
+ "Animation stress must sample the heaviest animated output.",
550
+ ),
231
551
  workload: false,
232
552
  };
233
553
  }
@@ -244,6 +564,10 @@ function createAnimationViewportDragScenario() {
244
564
  id: "animation-viewport-drag",
245
565
  interaction: "animation-viewport-drag" as const,
246
566
  stress: true,
567
+ stressFixture: createCustomStressFixture(
568
+ { animation: "fastest", density: 12 },
569
+ "Animated viewport drag must run while the heaviest animated output is visible.",
570
+ ),
247
571
  target: "canvas.viewport",
248
572
  workload: false,
249
573
  };
@@ -264,6 +588,7 @@ function createAnimatedVectorRendererConfig(options: { includeViewportDrag: bool
264
588
  whyNotAlternativeStrategies: ["svg preserves semantic vector output for this fixture"],
265
589
  },
266
590
  rendererWorkload: "vector-output",
591
+ rendererPipeline: createAnimatedVectorRendererPipeline(),
267
592
  scenarios: [
268
593
  ...createTextRendererScenarios(),
269
594
  createAnimationFrameScenario(),
@@ -570,6 +895,132 @@ describe("Toolcraft template performance contract", () => {
570
895
  );
571
896
  });
572
897
 
898
+ it("requires stress scenarios to declare an exact stress fixture value", () => {
899
+ const config = defineToolcraftPerformance({
900
+ rendererStrategy: "none",
901
+ rendererWorkload: "none",
902
+ scenarios: [
903
+ {
904
+ automated: true,
905
+ automatedTestName: "perf: preview render stays under budget",
906
+ browser: true,
907
+ browserTestName: "browser perf: preview render stays under budget",
908
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
909
+ expectedObservable: "Preview renders the heaviest output without freezing.",
910
+ fixture: "combined heavy renderer fixture",
911
+ id: "preview-render",
912
+ interaction: "preview-render",
913
+ stress: true,
914
+ workload: false,
915
+ },
916
+ ],
917
+ usesCustomRenderer: false,
918
+ workloadTargets: [],
919
+ });
920
+
921
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
922
+ "preview-render stress scenario must declare stressFixture with the real heaviest state used by browser performance tests.",
923
+ );
924
+ });
925
+
926
+ it("rejects stress fixtures without a browser-applicable value", () => {
927
+ const config = defineToolcraftPerformance({
928
+ rendererStrategy: "none",
929
+ rendererWorkload: "none",
930
+ scenarios: [
931
+ {
932
+ automated: true,
933
+ automatedTestName: "perf: preview render stays under budget",
934
+ browser: true,
935
+ browserTestName: "browser perf: preview render stays under budget",
936
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
937
+ expectedObservable: "Preview renders the heaviest output without freezing.",
938
+ fixture: "combined heavy renderer fixture",
939
+ id: "preview-render",
940
+ interaction: "preview-render",
941
+ stress: true,
942
+ stressFixture: {
943
+ kind: "custom",
944
+ reason: "Combined renderer stress state.",
945
+ },
946
+ workload: false,
947
+ },
948
+ ],
949
+ usesCustomRenderer: false,
950
+ workloadTargets: [],
951
+ });
952
+
953
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
954
+ "preview-render stressFixture must include value so browser tests can apply the exact heavy state.",
955
+ );
956
+ });
957
+
958
+ it("requires custom stress fixtures to be object-shaped heavy states", () => {
959
+ const config = defineToolcraftPerformance({
960
+ rendererStrategy: "none",
961
+ rendererWorkload: "none",
962
+ scenarios: [
963
+ {
964
+ automated: true,
965
+ automatedTestName: "perf: preview render stays under budget",
966
+ browser: true,
967
+ browserTestName: "browser perf: preview render stays under budget",
968
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
969
+ expectedObservable: "Preview renders the heaviest output without freezing.",
970
+ fixture: "combined heavy renderer fixture",
971
+ id: "preview-render",
972
+ interaction: "preview-render",
973
+ stress: true,
974
+ stressFixture: {
975
+ kind: "custom",
976
+ reason: "Combined renderer stress state.",
977
+ value: 12,
978
+ },
979
+ workload: false,
980
+ },
981
+ ],
982
+ usesCustomRenderer: false,
983
+ workloadTargets: [],
984
+ });
985
+
986
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
987
+ "preview-render custom stressFixture.value must be an object with one key per heavy state part so browser tests can apply every key.",
988
+ );
989
+ });
990
+
991
+ it("rejects empty custom stress fixture objects", () => {
992
+ const config = defineToolcraftPerformance({
993
+ rendererStrategy: "none",
994
+ rendererWorkload: "none",
995
+ scenarios: [
996
+ {
997
+ automated: true,
998
+ automatedTestName: "perf: preview render stays under budget",
999
+ browser: true,
1000
+ browserTestName: "browser perf: preview render stays under budget",
1001
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
1002
+ expectedObservable: "Preview renders the heaviest output without freezing.",
1003
+ fixture: "combined heavy renderer fixture",
1004
+ id: "preview-render",
1005
+ interaction: "preview-render",
1006
+ stress: true,
1007
+ stressFixture: {
1008
+ kind: "custom",
1009
+ reason: "Combined renderer stress state.",
1010
+ value: {},
1011
+ },
1012
+ workload: false,
1013
+ },
1014
+ ],
1015
+ usesCustomRenderer: false,
1016
+ workloadTargets: [],
1017
+ });
1018
+
1019
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
1020
+ "preview-render custom stressFixture.value must include at least one heavy state key.",
1021
+ );
1022
+ });
1023
+
573
1024
  it("rejects short stress fixtures for large text workload scenarios", () => {
574
1025
  const config = defineToolcraftPerformance({
575
1026
  rendererStrategy: "none",
@@ -639,10 +1090,10 @@ describe("Toolcraft template performance contract", () => {
639
1090
  expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual([]);
640
1091
  });
641
1092
 
642
- it("rejects visible controls without performance scenarios", () => {
1093
+ it("requires workload controls in custom renderers to declare an independent workload fixture", () => {
643
1094
  const config = defineToolcraftPerformance({
644
- rendererStrategy: "none",
645
- rendererWorkload: "none",
1095
+ rendererStrategy: "canvas-2d",
1096
+ rendererWorkload: "text-output",
646
1097
  scenarios: [
647
1098
  {
648
1099
  automated: true,
@@ -651,95 +1102,287 @@ describe("Toolcraft template performance contract", () => {
651
1102
  browserTestName: "browser perf: density drag stays responsive",
652
1103
  budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
653
1104
  controlLabel: "Density",
654
- expectedObservable: "Dragging Density updates the product without blocking the UI.",
1105
+ expectedObservable: "Dragging Density updates text output without blocking the UI.",
655
1106
  fixture: "runtime density fixture",
656
1107
  id: "density-drag",
657
1108
  interaction: "control-drag",
1109
+ stressFixture: createMaxValueStressFixture(
1110
+ 12,
1111
+ "Density max is the heaviest control value.",
1112
+ ),
658
1113
  target: "render.density",
659
1114
  values: { default: 4, max: 12, min: 1 },
660
1115
  workload: true,
661
1116
  },
662
1117
  ],
663
- usesCustomRenderer: false,
1118
+ usesCustomRenderer: true,
664
1119
  workloadTargets: ["render.density"],
665
1120
  });
666
1121
 
667
1122
  expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
668
- "render.mode must have a performance scenario because every visible control can affect app responsiveness.",
1123
+ "density-drag workload control scenario must declare workloadFixture for the app's heavy baseline state; stressFixture covers the control value only.",
669
1124
  );
670
1125
  });
671
1126
 
672
- it("allows text-output renderers without forcing a GPU strategy", () => {
1127
+ it("rejects toy media workload fixtures for control-drag baselines", () => {
673
1128
  const config = defineToolcraftPerformance({
674
- rendererStrategy: "canvas-2d",
675
- rendererTechnique: {
676
- exportRenderer: "canvas-2d",
677
- fidelityRisks: ["glyph metrics must stay crisp at product output size"],
678
- performanceRisks: ["large glyph grids can be expensive during drag"],
679
- previewRenderer: "canvas-2d",
680
- productRepresentation: "text",
681
- rendererStrategy: "canvas-2d",
682
- rendererWorkload: "text-output",
683
- sourceRepresentation: "dom-text",
684
- whyNotAlternativeStrategies: ["webgl texture output would rasterize text too early"],
685
- },
686
- rendererWorkload: "text-output",
687
- scenarios: createTextRendererScenarios(),
1129
+ rendererStrategy: "webgl",
1130
+ rendererWorkload: "pixel-output",
1131
+ scenarios: [
1132
+ {
1133
+ automated: true,
1134
+ automatedTestName: "perf: density drag stays responsive",
1135
+ browser: true,
1136
+ browserTestName: "browser perf: density drag stays responsive",
1137
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1138
+ controlLabel: "Density",
1139
+ expectedObservable: "Dragging Density updates source media output without blocking the UI.",
1140
+ fixture: "runtime density fixture",
1141
+ id: "density-drag",
1142
+ interaction: "control-drag",
1143
+ stressFixture: createMaxValueStressFixture(
1144
+ 12,
1145
+ "Density max is the heaviest control value.",
1146
+ ),
1147
+ target: "render.density",
1148
+ values: { default: 4, max: 12, min: 1 },
1149
+ workloadFixture: createMediaStressFixture(
1150
+ { height: 480, width: 640 },
1151
+ "This small source should be rejected as too light.",
1152
+ ),
1153
+ workload: true,
1154
+ },
1155
+ ],
688
1156
  usesCustomRenderer: true,
689
1157
  workloadTargets: ["render.density"],
690
1158
  });
691
1159
 
692
- expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
693
- });
694
-
695
- it("requires animated custom renderers to test viewport dragging during animation", () => {
696
- const config = createAnimatedVectorRendererConfig({ includeViewportDrag: false });
697
-
698
1160
  expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
699
- "Animated custom renderers must include an animation-viewport-drag performance scenario that samples frames while physically moving the canvas viewport.",
1161
+ "density-drag media workloadFixture.value must be at least 1920x1080-equivalent; received 640x480.",
700
1162
  );
701
1163
  });
702
1164
 
703
- it("requires detail-heavy custom renderers to test real viewport zoom stress", () => {
1165
+ it("accepts workload controls with separate control stress and app baseline fixtures", () => {
704
1166
  const config = defineToolcraftPerformance({
705
1167
  rendererStrategy: "canvas-2d",
706
1168
  rendererTechnique: {
707
1169
  exportRenderer: "canvas-2d",
708
- fidelityRisks: ["glyph metrics must stay crisp at product output size"],
709
- performanceRisks: ["large glyph grids can shake or jank during canvas zoom"],
1170
+ fidelityRisks: ["text output must stay crisp"],
1171
+ performanceRisks: ["density changes can relayout text"],
710
1172
  previewRenderer: "canvas-2d",
711
1173
  productRepresentation: "text",
712
1174
  rendererStrategy: "canvas-2d",
713
1175
  rendererWorkload: "text-output",
714
1176
  sourceRepresentation: "dom-text",
715
- whyNotAlternativeStrategies: ["webgl texture output would rasterize text too early"],
1177
+ whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
716
1178
  },
717
1179
  rendererWorkload: "text-output",
718
- scenarios: createTextRendererScenarios({ includeViewportZoomStress: false }),
1180
+ rendererPipeline: createTextRendererPipeline(),
1181
+ scenarios: createTextRendererScenarios(),
719
1182
  usesCustomRenderer: true,
720
1183
  workloadTargets: ["render.density"],
721
1184
  });
722
1185
 
1186
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
1187
+ });
1188
+
1189
+ it("rejects workload fixtures that are not paired with scenario stress fixtures", () => {
1190
+ const config = defineToolcraftPerformance({
1191
+ rendererStrategy: "none",
1192
+ rendererWorkload: "none",
1193
+ scenarios: [
1194
+ {
1195
+ automated: true,
1196
+ automatedTestName: "perf: preview baseline stays responsive",
1197
+ browser: true,
1198
+ browserTestName: "browser perf: preview baseline stays responsive",
1199
+ budget: { maxPreviewMs: 1000 },
1200
+ expectedObservable: "Preview renders inside a heavy baseline.",
1201
+ fixture: "baseline fixture",
1202
+ id: "preview-heavy-baseline",
1203
+ interaction: "preview-render",
1204
+ workload: false,
1205
+ workloadFixture: createLargeTextStressFixture(),
1206
+ },
1207
+ ],
1208
+ usesCustomRenderer: false,
1209
+ workloadTargets: [],
1210
+ });
1211
+
723
1212
  expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
724
- "Detail-heavy or animated custom renderers must include a viewport-zoom-stress performance scenario that uses real zoom controls while sampling frame gaps and long tasks.",
1213
+ "preview-heavy-baseline workloadFixture must be paired with stressFixture so tests apply a heavy baseline and then the measured scenario value.",
725
1214
  );
726
1215
  });
727
1216
 
728
- it("requires detail-heavy custom renderers to prove worst-case preview or animation stress", () => {
1217
+ it("rejects media-import scenarios that use toy source dimensions", () => {
729
1218
  const config = defineToolcraftPerformance({
730
- rendererStrategy: "canvas-2d",
731
- rendererTechnique: {
732
- exportRenderer: "canvas-2d",
733
- fidelityRisks: ["dense texture output must stay visually stable"],
734
- layers: [
735
- {
736
- content: ["dense-pattern"],
737
- exportMode: "included",
738
- id: "dense-pattern",
739
- kind: "product-foreground",
740
- primitiveCount: "high",
741
- renderer: "canvas-2d",
742
- uiSelector: '[data-toolcraft-renderer-layer="dense-pattern"]',
1219
+ rendererStrategy: "none",
1220
+ rendererWorkload: "none",
1221
+ scenarios: [
1222
+ {
1223
+ automated: true,
1224
+ automatedTestName: "perf: source image import stays responsive",
1225
+ browser: true,
1226
+ browserTestName: "browser perf: source image import stays responsive",
1227
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1228
+ expectedObservable: "Importing source media does not block the app.",
1229
+ fixture: "large source image fixture",
1230
+ id: "source-media-import",
1231
+ interaction: "media-import",
1232
+ stressFixture: createMediaStressFixture(
1233
+ { height: 480, width: 640 },
1234
+ "This small fixture should be rejected as too light.",
1235
+ ),
1236
+ target: "source.image",
1237
+ workload: true,
1238
+ },
1239
+ ],
1240
+ usesCustomRenderer: false,
1241
+ workloadTargets: [],
1242
+ });
1243
+
1244
+ expect(validateToolcraftPerformanceCoverage(mediaUploadSchema, config)).toContain(
1245
+ "source-media-import media stressFixture.value must be at least 1920x1080-equivalent; received 640x480.",
1246
+ );
1247
+ });
1248
+
1249
+ it("requires media-import scenarios to be workload media fixtures", () => {
1250
+ const config = defineToolcraftPerformance({
1251
+ rendererStrategy: "none",
1252
+ rendererWorkload: "none",
1253
+ scenarios: [
1254
+ {
1255
+ automated: true,
1256
+ automatedTestName: "perf: source image import stays responsive",
1257
+ browser: true,
1258
+ browserTestName: "browser perf: source image import stays responsive",
1259
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1260
+ expectedObservable: "Importing source media does not block the app.",
1261
+ fixture: "large source image fixture",
1262
+ id: "source-media-import",
1263
+ interaction: "media-import",
1264
+ stressFixture: createCustomStressFixture(
1265
+ { height: 1080, width: 1920 },
1266
+ "Wrong fixture kind for media import.",
1267
+ ),
1268
+ target: "source.image",
1269
+ workload: false,
1270
+ },
1271
+ ],
1272
+ usesCustomRenderer: false,
1273
+ workloadTargets: [],
1274
+ });
1275
+
1276
+ expect(validateToolcraftPerformanceCoverage(mediaUploadSchema, config)).toEqual(
1277
+ expect.arrayContaining([
1278
+ "source-media-import media-import scenario must set workload true because decoded source size changes renderer workload.",
1279
+ 'source-media-import media-import scenario must use stressFixture.kind "media" with a realistic uploaded source size.',
1280
+ ]),
1281
+ );
1282
+ });
1283
+
1284
+ it("rejects visible controls without performance scenarios", () => {
1285
+ const config = defineToolcraftPerformance({
1286
+ rendererStrategy: "none",
1287
+ rendererWorkload: "none",
1288
+ scenarios: [
1289
+ {
1290
+ automated: true,
1291
+ automatedTestName: "perf: density drag stays responsive",
1292
+ browser: true,
1293
+ browserTestName: "browser perf: density drag stays responsive",
1294
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1295
+ controlLabel: "Density",
1296
+ expectedObservable: "Dragging Density updates the product without blocking the UI.",
1297
+ fixture: "runtime density fixture",
1298
+ id: "density-drag",
1299
+ interaction: "control-drag",
1300
+ target: "render.density",
1301
+ values: { default: 4, max: 12, min: 1 },
1302
+ workload: true,
1303
+ },
1304
+ ],
1305
+ usesCustomRenderer: false,
1306
+ workloadTargets: ["render.density"],
1307
+ });
1308
+
1309
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1310
+ "render.mode must have a performance scenario because every visible control can affect app responsiveness.",
1311
+ );
1312
+ });
1313
+
1314
+ it("allows text-output renderers without forcing a GPU strategy", () => {
1315
+ const config = defineToolcraftPerformance({
1316
+ rendererStrategy: "canvas-2d",
1317
+ rendererTechnique: {
1318
+ exportRenderer: "canvas-2d",
1319
+ fidelityRisks: ["glyph metrics must stay crisp at product output size"],
1320
+ performanceRisks: ["large glyph grids can be expensive during drag"],
1321
+ previewRenderer: "canvas-2d",
1322
+ productRepresentation: "text",
1323
+ rendererStrategy: "canvas-2d",
1324
+ rendererWorkload: "text-output",
1325
+ sourceRepresentation: "dom-text",
1326
+ whyNotAlternativeStrategies: ["webgl texture output would rasterize text too early"],
1327
+ },
1328
+ rendererWorkload: "text-output",
1329
+ rendererPipeline: createTextRendererPipeline(),
1330
+ scenarios: createTextRendererScenarios(),
1331
+ usesCustomRenderer: true,
1332
+ workloadTargets: ["render.density"],
1333
+ });
1334
+
1335
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
1336
+ });
1337
+
1338
+ it("requires animated custom renderers to test viewport dragging during animation", () => {
1339
+ const config = createAnimatedVectorRendererConfig({ includeViewportDrag: false });
1340
+
1341
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1342
+ "Animated custom renderers must include an animation-viewport-drag performance scenario that samples frames while physically moving the canvas viewport.",
1343
+ );
1344
+ });
1345
+
1346
+ it("requires detail-heavy custom renderers to test real viewport zoom stress", () => {
1347
+ const config = defineToolcraftPerformance({
1348
+ rendererStrategy: "canvas-2d",
1349
+ rendererTechnique: {
1350
+ exportRenderer: "canvas-2d",
1351
+ fidelityRisks: ["glyph metrics must stay crisp at product output size"],
1352
+ performanceRisks: ["large glyph grids can shake or jank during canvas zoom"],
1353
+ previewRenderer: "canvas-2d",
1354
+ productRepresentation: "text",
1355
+ rendererStrategy: "canvas-2d",
1356
+ rendererWorkload: "text-output",
1357
+ sourceRepresentation: "dom-text",
1358
+ whyNotAlternativeStrategies: ["webgl texture output would rasterize text too early"],
1359
+ },
1360
+ rendererWorkload: "text-output",
1361
+ scenarios: createTextRendererScenarios({ includeViewportZoomStress: false }),
1362
+ usesCustomRenderer: true,
1363
+ workloadTargets: ["render.density"],
1364
+ });
1365
+
1366
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1367
+ "Detail-heavy or animated custom renderers must include a viewport-zoom-stress performance scenario that uses real zoom controls while sampling frame gaps and long tasks.",
1368
+ );
1369
+ });
1370
+
1371
+ it("requires detail-heavy custom renderers to prove worst-case preview or animation stress", () => {
1372
+ const config = defineToolcraftPerformance({
1373
+ rendererStrategy: "canvas-2d",
1374
+ rendererTechnique: {
1375
+ exportRenderer: "canvas-2d",
1376
+ fidelityRisks: ["dense texture output must stay visually stable"],
1377
+ layers: [
1378
+ {
1379
+ content: ["dense-pattern"],
1380
+ exportMode: "included",
1381
+ id: "dense-pattern",
1382
+ kind: "product-foreground",
1383
+ primitiveCount: "high",
1384
+ renderer: "canvas-2d",
1385
+ uiSelector: '[data-toolcraft-renderer-layer="dense-pattern"]',
743
1386
  },
744
1387
  {
745
1388
  content: ["geometry"],
@@ -812,6 +1455,73 @@ describe("Toolcraft template performance contract", () => {
812
1455
  );
813
1456
  });
814
1457
 
1458
+ it("requires detail-heavy Canvas 2D renderers to document measured WebGL/WebGPU evaluation", () => {
1459
+ const config = defineToolcraftPerformance({
1460
+ rendererStrategy: "canvas-2d",
1461
+ rendererTechnique: {
1462
+ exportRenderer: "canvas-2d",
1463
+ fidelityRisks: ["dense texture output must match source media colors"],
1464
+ layers: [
1465
+ {
1466
+ content: ["bitmap-media", "noise"],
1467
+ exportMode: "included",
1468
+ id: "media-effect",
1469
+ kind: "product-foreground",
1470
+ primitiveCount: "high",
1471
+ renderer: "canvas-2d",
1472
+ uiSelector: '[data-toolcraft-renderer-layer="media-effect"]',
1473
+ },
1474
+ ],
1475
+ performanceRisks: ["large uploaded media can require millions of pixel operations"],
1476
+ previewRenderer: "canvas-2d",
1477
+ productRepresentation: "pixel",
1478
+ rendererStrategy: "canvas-2d",
1479
+ rendererWorkload: "pixel-output",
1480
+ sourceRepresentation: "image-media",
1481
+ whyNotAlternativeStrategies: ["canvas is convenient"],
1482
+ },
1483
+ rendererWorkload: "pixel-output",
1484
+ scenarios: createTextRendererScenarios(),
1485
+ usesCustomRenderer: true,
1486
+ workloadTargets: ["render.density"],
1487
+ });
1488
+
1489
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1490
+ "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.",
1491
+ );
1492
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1493
+ '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.',
1494
+ );
1495
+ });
1496
+
1497
+ it("allows pixel-output Canvas 2D only with measured WebGL/WebGPU evidence", () => {
1498
+ const config = defineToolcraftPerformance({
1499
+ rendererStrategy: "canvas-2d",
1500
+ rendererTechnique: {
1501
+ exportRenderer: "canvas-2d",
1502
+ fidelityRisks: ["pixel output must preserve exact color stops"],
1503
+ performanceRisks: [
1504
+ "Measured WebGL stress evidence showed setup overhead exceeded cached Canvas 2D worker output for this static fixture.",
1505
+ ],
1506
+ previewRenderer: "canvas-2d",
1507
+ productRepresentation: "pixel",
1508
+ rendererStrategy: "canvas-2d",
1509
+ rendererWorkload: "pixel-output",
1510
+ sourceRepresentation: "procedural-data",
1511
+ whyNotAlternativeStrategies: [
1512
+ "WebGL measured stress evidence stayed within budget but did not improve interaction latency for this renderer.",
1513
+ ],
1514
+ },
1515
+ rendererWorkload: "pixel-output",
1516
+ rendererPipeline: createPixelRendererPipeline(),
1517
+ scenarios: createTextRendererScenarios(),
1518
+ usesCustomRenderer: true,
1519
+ workloadTargets: ["render.density"],
1520
+ });
1521
+
1522
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
1523
+ });
1524
+
815
1525
  it("accepts animated custom renderers with stress coverage while dragging the viewport", () => {
816
1526
  const config = createAnimatedVectorRendererConfig({ includeViewportDrag: true });
817
1527
 
@@ -834,6 +1544,528 @@ describe("Toolcraft template performance contract", () => {
834
1544
  );
835
1545
  });
836
1546
 
1547
+ it("requires custom renderers to declare a render pipeline inventory", () => {
1548
+ const config = defineToolcraftPerformance({
1549
+ rendererStrategy: "canvas-2d",
1550
+ rendererTechnique: {
1551
+ exportRenderer: "canvas-2d",
1552
+ fidelityRisks: ["canvas output can soften edges"],
1553
+ performanceRisks: ["dense output can jank during drag"],
1554
+ previewRenderer: "canvas-2d",
1555
+ productRepresentation: "mixed",
1556
+ rendererStrategy: "canvas-2d",
1557
+ rendererWorkload: "simple-composition",
1558
+ sourceRepresentation: "procedural-data",
1559
+ whyNotAlternativeStrategies: [
1560
+ "svg cannot preserve dense procedural background output",
1561
+ ],
1562
+ },
1563
+ rendererWorkload: "simple-composition",
1564
+ scenarios: createTextRendererScenarios(),
1565
+ usesCustomRenderer: true,
1566
+ workloadTargets: ["render.density"],
1567
+ });
1568
+
1569
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1570
+ "Custom renderers must declare rendererPipeline so render passes, cache keys, and invalidation are machine-checkable.",
1571
+ );
1572
+ });
1573
+
1574
+ it("rejects vague renderer pipeline pass dependencies", () => {
1575
+ const config = defineToolcraftPerformance({
1576
+ rendererStrategy: "canvas-2d",
1577
+ rendererTechnique: {
1578
+ exportRenderer: "canvas-2d",
1579
+ fidelityRisks: ["dense text output can soften at small sizes"],
1580
+ performanceRisks: ["text layout can be expensive"],
1581
+ previewRenderer: "canvas-2d",
1582
+ productRepresentation: "text",
1583
+ rendererStrategy: "canvas-2d",
1584
+ rendererWorkload: "text-output",
1585
+ sourceRepresentation: "dom-text",
1586
+ whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
1587
+ },
1588
+ rendererWorkload: "text-output",
1589
+ rendererPipeline: {
1590
+ interactionInvalidation: [
1591
+ {
1592
+ interaction: "control-drag",
1593
+ invalidates: ["text-layout"],
1594
+ targets: ["render.density"],
1595
+ },
1596
+ ],
1597
+ passes: [
1598
+ {
1599
+ cacheKey: ["state"],
1600
+ id: "text-layout",
1601
+ inputs: ["state"],
1602
+ invalidatedBy: ["all values"],
1603
+ kind: "text-layout",
1604
+ output: "preview",
1605
+ quality: "full",
1606
+ runsOn: "main",
1607
+ },
1608
+ ],
1609
+ },
1610
+ scenarios: createTextRendererScenarios(),
1611
+ usesCustomRenderer: true,
1612
+ workloadTargets: ["render.density"],
1613
+ });
1614
+
1615
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
1616
+ expect.arrayContaining([
1617
+ 'rendererPipeline pass "text-layout" inputs entry "state" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
1618
+ 'rendererPipeline pass "text-layout" invalidatedBy entry "all values" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
1619
+ 'rendererPipeline pass "text-layout" cacheKey entry "state" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
1620
+ ]),
1621
+ );
1622
+ });
1623
+
1624
+ it("requires cache-sensitive renderer pipeline passes to declare cache keys", () => {
1625
+ const config = defineToolcraftPerformance({
1626
+ rendererStrategy: "canvas-2d",
1627
+ rendererTechnique: {
1628
+ exportRenderer: "canvas-2d",
1629
+ fidelityRisks: ["source pixels must stay crisp"],
1630
+ performanceRisks: ["pixel transforms can block the main thread"],
1631
+ previewRenderer: "canvas-2d",
1632
+ productRepresentation: "pixel",
1633
+ rendererStrategy: "canvas-2d",
1634
+ rendererWorkload: "pixel-output",
1635
+ sourceRepresentation: "image-media",
1636
+ whyNotAlternativeStrategies: [
1637
+ "WebGL/WebGPU measured performance evidence showed no improvement for this fixture",
1638
+ ],
1639
+ },
1640
+ rendererWorkload: "pixel-output",
1641
+ rendererPipeline: {
1642
+ interactionInvalidation: [
1643
+ {
1644
+ interaction: "control-drag",
1645
+ invalidates: ["pixel-pass"],
1646
+ targets: ["render.density"],
1647
+ },
1648
+ ],
1649
+ passes: [
1650
+ {
1651
+ id: "pixel-pass",
1652
+ inputs: ["source.image", "render.density"],
1653
+ invalidatedBy: ["source.image", "render.density"],
1654
+ kind: "pixel-transform",
1655
+ output: "preview",
1656
+ quality: "retina",
1657
+ runsOn: "worker",
1658
+ },
1659
+ ],
1660
+ },
1661
+ scenarios: createTextRendererScenarios(),
1662
+ usesCustomRenderer: true,
1663
+ workloadTargets: ["render.density"],
1664
+ });
1665
+
1666
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1667
+ '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.',
1668
+ );
1669
+ });
1670
+
1671
+ it("requires composite renderer pipeline passes to declare cache keys", () => {
1672
+ const config = defineToolcraftPerformance({
1673
+ rendererStrategy: "svg",
1674
+ rendererTechnique: {
1675
+ exportRenderer: "svg",
1676
+ fidelityRisks: ["animated composite must preserve vector output"],
1677
+ performanceRisks: ["compositing can be rebuilt too often"],
1678
+ previewRenderer: "svg",
1679
+ productRepresentation: "vector",
1680
+ rendererStrategy: "svg",
1681
+ rendererWorkload: "vector-output",
1682
+ sourceRepresentation: "procedural-data",
1683
+ whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
1684
+ },
1685
+ rendererWorkload: "vector-output",
1686
+ rendererPipeline: {
1687
+ ...createAnimatedVectorRendererPipeline(),
1688
+ passes: createAnimatedVectorRendererPipeline().passes.map((pass) =>
1689
+ pass.id === "animation-composite"
1690
+ ? {
1691
+ id: pass.id,
1692
+ inputs: pass.inputs,
1693
+ invalidatedBy: pass.invalidatedBy,
1694
+ kind: pass.kind,
1695
+ output: pass.output,
1696
+ quality: pass.quality,
1697
+ runsOn: pass.runsOn,
1698
+ }
1699
+ : pass,
1700
+ ),
1701
+ },
1702
+ scenarios: createTextRendererScenarios(),
1703
+ usesCustomRenderer: true,
1704
+ workloadTargets: ["render.density"],
1705
+ });
1706
+
1707
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1708
+ 'rendererPipeline pass "animation-composite" is a cache-sensitive composite pass and must declare cacheKey so tests can reject full recomputation on every control change.',
1709
+ );
1710
+ });
1711
+
1712
+ it("rejects high-frequency viewport interactions that invalidate expensive passes", () => {
1713
+ const config = defineToolcraftPerformance({
1714
+ rendererStrategy: "canvas-2d",
1715
+ rendererTechnique: {
1716
+ exportRenderer: "canvas-2d",
1717
+ fidelityRisks: ["dense background must stay stable while zooming"],
1718
+ layers: [
1719
+ {
1720
+ content: ["dense-pattern"],
1721
+ exportMode: "included",
1722
+ id: "dense-pattern",
1723
+ kind: "background",
1724
+ primitiveCount: "high",
1725
+ renderer: "canvas-2d",
1726
+ },
1727
+ ],
1728
+ performanceRisks: [
1729
+ "WebGL measured stress evidence was reviewed before keeping dense background on Canvas 2D",
1730
+ ],
1731
+ previewRenderer: "canvas-2d",
1732
+ productRepresentation: "pixel",
1733
+ rendererStrategy: "canvas-2d",
1734
+ rendererWorkload: "pixel-output",
1735
+ sourceRepresentation: "procedural-data",
1736
+ whyNotAlternativeStrategies: [
1737
+ "WebGL stress evidence showed shader setup overhead exceeded static cache reuse",
1738
+ ],
1739
+ },
1740
+ rendererWorkload: "pixel-output",
1741
+ rendererPipeline: {
1742
+ interactionInvalidation: [
1743
+ {
1744
+ interaction: "viewport-zoom",
1745
+ invalidates: ["dense-background"],
1746
+ mustNotInvalidate: ["dense-background"],
1747
+ targets: ["canvas.viewport"],
1748
+ },
1749
+ {
1750
+ interaction: "control-drag",
1751
+ invalidates: ["dense-background"],
1752
+ targets: ["render.density"],
1753
+ },
1754
+ ],
1755
+ passes: [
1756
+ {
1757
+ cacheKey: ["render.density", "canvas.size", "canvas.renderScale"],
1758
+ id: "dense-background",
1759
+ inputs: ["render.density", "canvas.size", "canvas.renderScale"],
1760
+ invalidatedBy: ["render.density", "canvas.size", "canvas.renderScale"],
1761
+ kind: "rasterize",
1762
+ output: "preview",
1763
+ quality: "retina",
1764
+ runsOn: "worker",
1765
+ },
1766
+ ],
1767
+ },
1768
+ scenarios: createTextRendererScenarios(),
1769
+ usesCustomRenderer: true,
1770
+ workloadTargets: ["render.density"],
1771
+ });
1772
+
1773
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
1774
+ expect.arrayContaining([
1775
+ 'rendererPipeline viewport-zoom cannot both invalidate and mustNotInvalidate pass "dense-background".',
1776
+ 'rendererPipeline viewport-zoom must not invalidate expensive pass "dense-background" (rasterize). Move viewport work to transforms/uniforms or explain it through a cheaper pass.',
1777
+ ]),
1778
+ );
1779
+ });
1780
+
1781
+ it("requires media decoding pipelines to include media-import coverage", () => {
1782
+ const config = defineToolcraftPerformance({
1783
+ rendererStrategy: "webgl",
1784
+ rendererTechnique: {
1785
+ exportRenderer: "webgl",
1786
+ fidelityRisks: ["large source media must preserve color"],
1787
+ performanceRisks: ["source decode can be expensive"],
1788
+ previewRenderer: "webgl",
1789
+ productRepresentation: "pixel",
1790
+ rendererStrategy: "webgl",
1791
+ rendererWorkload: "pixel-output",
1792
+ sourceRepresentation: "image-media",
1793
+ whyNotAlternativeStrategies: ["canvas-2d would run per-pixel work on the main thread"],
1794
+ },
1795
+ rendererWorkload: "pixel-output",
1796
+ rendererPipeline: createMediaRendererPipeline(),
1797
+ scenarios: createTextRendererScenarios(),
1798
+ usesCustomRenderer: true,
1799
+ workloadTargets: ["render.density"],
1800
+ });
1801
+
1802
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1803
+ 'rendererPipeline pass "source-decode" decodes media, so performance scenarios must include media-import coverage.',
1804
+ );
1805
+ });
1806
+
1807
+ it("requires workload targets to appear in renderer pipeline invalidation targets", () => {
1808
+ const config = defineToolcraftPerformance({
1809
+ rendererStrategy: "canvas-2d",
1810
+ rendererTechnique: {
1811
+ exportRenderer: "canvas-2d",
1812
+ fidelityRisks: ["text output must stay crisp"],
1813
+ performanceRisks: ["density changes can relayout text"],
1814
+ previewRenderer: "canvas-2d",
1815
+ productRepresentation: "text",
1816
+ rendererStrategy: "canvas-2d",
1817
+ rendererWorkload: "text-output",
1818
+ sourceRepresentation: "dom-text",
1819
+ whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
1820
+ },
1821
+ rendererWorkload: "text-output",
1822
+ rendererPipeline: {
1823
+ ...createTextRendererPipeline(),
1824
+ interactionInvalidation: [
1825
+ {
1826
+ interaction: "control-change",
1827
+ invalidates: ["text-layout"],
1828
+ targets: ["render.mode"],
1829
+ },
1830
+ ],
1831
+ },
1832
+ scenarios: createTextRendererScenarios(),
1833
+ usesCustomRenderer: true,
1834
+ workloadTargets: ["render.density"],
1835
+ });
1836
+
1837
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1838
+ "Performance workload target render.density must appear in rendererPipeline interactionInvalidation targets.",
1839
+ );
1840
+ });
1841
+
1842
+ it("requires performance scenario interactions to appear in renderer pipeline invalidation", () => {
1843
+ const config = defineToolcraftPerformance({
1844
+ rendererStrategy: "canvas-2d",
1845
+ rendererTechnique: {
1846
+ exportRenderer: "canvas-2d",
1847
+ fidelityRisks: ["text output must stay crisp"],
1848
+ performanceRisks: ["density changes can relayout text"],
1849
+ previewRenderer: "canvas-2d",
1850
+ productRepresentation: "text",
1851
+ rendererStrategy: "canvas-2d",
1852
+ rendererWorkload: "text-output",
1853
+ sourceRepresentation: "dom-text",
1854
+ whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
1855
+ },
1856
+ rendererWorkload: "text-output",
1857
+ rendererPipeline: {
1858
+ ...createTextRendererPipeline(),
1859
+ interactionInvalidation: [
1860
+ {
1861
+ interaction: "control-drag",
1862
+ invalidates: ["text-layout"],
1863
+ targets: ["render.density"],
1864
+ },
1865
+ {
1866
+ interaction: "control-change",
1867
+ invalidates: ["text-layout"],
1868
+ targets: ["render.mode"],
1869
+ },
1870
+ ],
1871
+ },
1872
+ scenarios: createTextRendererScenarios(),
1873
+ usesCustomRenderer: true,
1874
+ workloadTargets: ["render.density"],
1875
+ });
1876
+
1877
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1878
+ "Performance scenario viewport-zoom-stress exercises viewport-zoom, so rendererPipeline.interactionInvalidation must declare that interaction.",
1879
+ );
1880
+ });
1881
+
1882
+ it("requires timeline playback scenarios to appear in renderer pipeline invalidation", () => {
1883
+ const config = defineToolcraftPerformance({
1884
+ rendererStrategy: "svg",
1885
+ rendererTechnique: {
1886
+ exportRenderer: "svg",
1887
+ fidelityRisks: ["timeline playback must preserve vector motion"],
1888
+ performanceRisks: ["timeline playback can jank at high density"],
1889
+ previewRenderer: "svg",
1890
+ productRepresentation: "vector",
1891
+ rendererStrategy: "svg",
1892
+ rendererWorkload: "vector-output",
1893
+ sourceRepresentation: "procedural-data",
1894
+ whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
1895
+ },
1896
+ rendererWorkload: "vector-output",
1897
+ rendererPipeline: {
1898
+ ...createAnimatedVectorRendererPipeline(),
1899
+ interactionInvalidation: createAnimatedVectorRendererPipeline().interactionInvalidation.filter(
1900
+ (entry) => entry.interaction !== "timeline-playback",
1901
+ ),
1902
+ },
1903
+ scenarios: [
1904
+ ...createTextRendererScenarios(),
1905
+ {
1906
+ automated: true,
1907
+ automatedTestName: "perf: timeline playback stays smooth",
1908
+ browser: true,
1909
+ browserTestName: "browser perf: timeline playback stays smooth",
1910
+ budget: { maxFrameGapMs: 80, maxLongTaskMs: 80 },
1911
+ expectedObservable: "Timeline playback advances without blocking frames.",
1912
+ fixture: "timeline playback fixture",
1913
+ id: "timeline-playback",
1914
+ interaction: "timeline-playback",
1915
+ stress: true,
1916
+ stressFixture: createCombinedRendererStressFixture(),
1917
+ target: "timeline.currentTime",
1918
+ uiSelector: '[data-slot="timeline-playback-handle"]',
1919
+ workload: false,
1920
+ },
1921
+ ],
1922
+ usesCustomRenderer: true,
1923
+ workloadTargets: ["render.density"],
1924
+ });
1925
+
1926
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1927
+ "Performance scenario timeline-playback exercises timeline-playback, so rendererPipeline.interactionInvalidation must declare that interaction.",
1928
+ );
1929
+ });
1930
+
1931
+ it("validates timeline scrub scenarios through renderer pipeline invalidation", () => {
1932
+ const config = defineToolcraftPerformance({
1933
+ rendererStrategy: "svg",
1934
+ rendererTechnique: {
1935
+ exportRenderer: "svg",
1936
+ fidelityRisks: ["timeline scrub must preserve vector motion"],
1937
+ performanceRisks: ["timeline scrub can jank at high density"],
1938
+ previewRenderer: "svg",
1939
+ productRepresentation: "vector",
1940
+ rendererStrategy: "svg",
1941
+ rendererWorkload: "vector-output",
1942
+ sourceRepresentation: "procedural-data",
1943
+ whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
1944
+ },
1945
+ rendererWorkload: "vector-output",
1946
+ rendererPipeline: createAnimatedVectorRendererPipeline(),
1947
+ scenarios: [
1948
+ ...createTextRendererScenarios(),
1949
+ {
1950
+ automated: true,
1951
+ automatedTestName: "perf: timeline scrub stays responsive",
1952
+ browser: true,
1953
+ browserTestName: "browser perf: timeline scrub stays responsive",
1954
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
1955
+ expectedObservable: "Timeline scrub updates output without blocking frames.",
1956
+ fixture: "timeline scrub fixture",
1957
+ id: "timeline-scrub",
1958
+ interaction: "timeline-scrub",
1959
+ stress: true,
1960
+ stressFixture: createCombinedRendererStressFixture(),
1961
+ target: "timeline.currentTime",
1962
+ uiSelector: '[data-slot="timeline-playback-handle"]',
1963
+ workload: false,
1964
+ },
1965
+ ],
1966
+ usesCustomRenderer: true,
1967
+ workloadTargets: ["render.density"],
1968
+ });
1969
+
1970
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
1971
+ });
1972
+
1973
+ it("requires timeline performance scenarios to name the real timeline UI", () => {
1974
+ const config = defineToolcraftPerformance({
1975
+ rendererStrategy: "svg",
1976
+ rendererTechnique: {
1977
+ exportRenderer: "svg",
1978
+ fidelityRisks: ["timeline scrub must preserve vector motion"],
1979
+ performanceRisks: ["timeline scrub can jank at high density"],
1980
+ previewRenderer: "svg",
1981
+ productRepresentation: "vector",
1982
+ rendererStrategy: "svg",
1983
+ rendererWorkload: "vector-output",
1984
+ sourceRepresentation: "procedural-data",
1985
+ whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
1986
+ },
1987
+ rendererWorkload: "vector-output",
1988
+ rendererPipeline: createAnimatedVectorRendererPipeline(),
1989
+ scenarios: [
1990
+ ...createTextRendererScenarios(),
1991
+ {
1992
+ automated: true,
1993
+ automatedTestName: "perf: timeline scrub stays responsive",
1994
+ browser: true,
1995
+ browserTestName: "browser perf: timeline scrub stays responsive",
1996
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
1997
+ expectedObservable: "Timeline scrub updates output without blocking frames.",
1998
+ fixture: "timeline scrub fixture",
1999
+ id: "timeline-scrub",
2000
+ interaction: "timeline-scrub",
2001
+ stress: true,
2002
+ stressFixture: createCombinedRendererStressFixture(),
2003
+ target: "timeline.currentTime",
2004
+ workload: false,
2005
+ },
2006
+ ],
2007
+ usesCustomRenderer: true,
2008
+ workloadTargets: ["render.density"],
2009
+ });
2010
+
2011
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
2012
+ "timeline-scrub timeline-scrub scenario must declare controlLabel or uiSelector for its real browser interaction.",
2013
+ );
2014
+ });
2015
+
2016
+ it("validates mask drag scenarios through renderer pipeline invalidation", () => {
2017
+ const config = defineToolcraftPerformance({
2018
+ rendererStrategy: "svg",
2019
+ rendererTechnique: {
2020
+ exportRenderer: "svg",
2021
+ fidelityRisks: ["mask handles must stay aligned with vector output"],
2022
+ performanceRisks: ["dragging a mask handle can rebuild output too often"],
2023
+ previewRenderer: "svg",
2024
+ productRepresentation: "vector",
2025
+ rendererStrategy: "svg",
2026
+ rendererWorkload: "vector-output",
2027
+ sourceRepresentation: "procedural-data",
2028
+ whyNotAlternativeStrategies: ["svg preserves semantic handle geometry"],
2029
+ },
2030
+ rendererWorkload: "vector-output",
2031
+ rendererPipeline: {
2032
+ ...createAnimatedVectorRendererPipeline(),
2033
+ interactionInvalidation: [
2034
+ ...createAnimatedVectorRendererPipeline().interactionInvalidation,
2035
+ {
2036
+ interaction: "mask-drag",
2037
+ invalidates: ["animation-composite"],
2038
+ mustNotInvalidate: ["vector-build"],
2039
+ targets: ["mask.anchor"],
2040
+ },
2041
+ ],
2042
+ },
2043
+ scenarios: [
2044
+ ...createTextRendererScenarios(),
2045
+ {
2046
+ automated: true,
2047
+ automatedTestName: "perf: mask drag stays responsive",
2048
+ browser: true,
2049
+ browserTestName: "browser perf: mask drag stays responsive",
2050
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
2051
+ expectedObservable: "Dragging the mask handle updates output without blocking frames.",
2052
+ fixture: "mask drag fixture",
2053
+ id: "mask-drag",
2054
+ interaction: "mask-drag",
2055
+ stress: true,
2056
+ stressFixture: createCombinedRendererStressFixture(),
2057
+ target: "mask.anchor",
2058
+ uiSelector: '[data-toolcraft-canvas-handle][data-testid="mask-anchor"]',
2059
+ workload: false,
2060
+ },
2061
+ ],
2062
+ usesCustomRenderer: true,
2063
+ workloadTargets: ["render.density"],
2064
+ });
2065
+
2066
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
2067
+ });
2068
+
837
2069
  it("rejects renderer technique mismatches and missing explanations", () => {
838
2070
  const config = defineToolcraftPerformance({
839
2071
  rendererStrategy: "canvas-2d",
@@ -909,9 +2141,11 @@ describe("Toolcraft template performance contract", () => {
909
2141
  sourceRepresentation: "procedural-data",
910
2142
  whyNotAlternativeStrategies: [
911
2143
  "rendering thousands of background dots as DOM nodes would be expensive",
2144
+ "WebGL/WebGPU stress evidence is not needed yet because measured canvas-2d preview budgets pass for this dense static background.",
912
2145
  ],
913
2146
  },
914
2147
  rendererWorkload: "simple-composition",
2148
+ rendererPipeline: createDenseCanvasPipeline(),
915
2149
  scenarios: createTextRendererScenarios(),
916
2150
  usesCustomRenderer: true,
917
2151
  workloadTargets: ["render.density"],