@pixel-point/toolcraft 0.0.9 → 0.0.12

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 (77) hide show
  1. package/README.md +42 -9
  2. package/package.json +2 -1
  3. package/src/cli.mjs +4 -2
  4. package/src/cli.test.mjs +5 -2
  5. package/src/generate.mjs +14 -6
  6. package/src/generate.test.mjs +33 -2
  7. package/templates/runtime/contracts/component-contracts.test.ts +175 -36
  8. package/templates/runtime/contracts/component-contracts.ts +90 -45
  9. package/templates/runtime/contracts/decision-contracts.test.ts +33 -1
  10. package/templates/runtime/contracts/decision-contracts.ts +19 -6
  11. package/templates/runtime/export/export.test.ts +63 -0
  12. package/templates/runtime/export/export.ts +55 -0
  13. package/templates/runtime/index.ts +1 -0
  14. package/templates/runtime/react/canvas-shell.test.tsx +58 -4
  15. package/templates/runtime/react/canvas-shell.tsx +31 -9
  16. package/templates/runtime/react/controls-panel.test.tsx +478 -27
  17. package/templates/runtime/react/controls-panel.tsx +71 -26
  18. package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
  19. package/templates/runtime/react/settings-transfer.test.ts +4 -0
  20. package/templates/runtime/react/settings-transfer.ts +6 -1
  21. package/templates/runtime/react/timeline-panel.test.tsx +14 -0
  22. package/templates/runtime/react/timeline-panel.tsx +44 -7
  23. package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
  24. package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
  25. package/templates/runtime/react/toolcraft-app.tsx +56 -37
  26. package/templates/runtime/schema/define-toolcraft.test.ts +225 -173
  27. package/templates/runtime/schema/define-toolcraft.ts +117 -247
  28. package/templates/runtime/schema/runtime-targets.ts +21 -0
  29. package/templates/runtime/schema/types.ts +41 -0
  30. package/templates/runtime/state/create-template-state.test.ts +156 -0
  31. package/templates/runtime/state/create-template-state.ts +38 -8
  32. package/templates/runtime/state/media-defaults.ts +105 -0
  33. package/templates/runtime/state/persistence.test.ts +58 -0
  34. package/templates/runtime/state/persistence.ts +105 -1
  35. package/templates/runtime/state/reducer.test.ts +280 -4
  36. package/templates/runtime/state/reducer.ts +195 -9
  37. package/templates/runtime/state/timeline-loop.test.ts +71 -0
  38. package/templates/runtime/state/timeline-loop.ts +35 -0
  39. package/templates/runtime/state/types.ts +27 -0
  40. package/templates/runtime/testing/performance.test.ts +657 -2
  41. package/templates/runtime/testing/performance.ts +789 -49
  42. package/templates/starter/AGENTS.md +22 -16
  43. package/templates/starter/docs/toolcraft/README.md +8 -4
  44. package/templates/starter/docs/toolcraft/acceptance-testing.md +41 -8
  45. package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
  46. package/templates/starter/docs/toolcraft/assembly-workflow.md +51 -20
  47. package/templates/starter/docs/toolcraft/component-rules.md +58 -37
  48. package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
  49. package/templates/starter/docs/toolcraft/performance.md +30 -10
  50. package/templates/starter/docs/toolcraft/schema-reference.md +134 -33
  51. package/templates/starter/docs/toolcraft/workflow.md +5 -2
  52. package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
  53. package/templates/starter/e2e/app-performance.spec.ts +57 -4
  54. package/templates/starter/e2e/performance-helpers.ts +45 -0
  55. package/templates/starter/index.html +1 -0
  56. package/templates/starter/package.json +3 -2
  57. package/templates/starter/playwright.config.ts +1 -1
  58. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  59. package/templates/starter/scripts/run-vite-on-free-port.mjs +82 -16
  60. package/templates/starter/scripts/toolcraft-port.mjs +178 -0
  61. package/templates/starter/scripts/toolcraft-port.test.mjs +147 -0
  62. package/templates/starter/src/app/starter-acceptance.test.ts +2573 -313
  63. package/templates/starter/src/app/starter-acceptance.ts +978 -81
  64. package/templates/starter/src/app/starter-performance.test.ts +130 -8
  65. package/templates/starter/src/app/starter-performance.ts +5 -0
  66. package/templates/starter/src/app/starter-schema.test.ts +32 -7
  67. package/templates/starter/src/app/starter-schema.ts +6 -2
  68. package/templates/starter/vite.config.ts +58 -2
  69. package/templates/ui/components/controls/actions/actions-control.tsx +46 -3
  70. package/templates/ui/components/controls/color/palette-control.tsx +34 -4
  71. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
  72. package/templates/ui/components/controls/file-drop/index.ts +6 -1
  73. package/templates/ui/components/controls/index.ts +2 -0
  74. package/templates/ui/components/controls/range-slider/range-slider-value.ts +19 -5
  75. package/templates/ui/components/controls/select/select-control.tsx +4 -26
  76. package/templates/ui/components/controls/vector/vector-control.tsx +25 -4
  77. package/templates/ui/components/panel/panel-actions.tsx +1 -1
@@ -5,6 +5,8 @@ import {
5
5
  collectToolcraftPerformanceSensitiveControls,
6
6
  collectToolcraftUnclassifiedPerformanceControls,
7
7
  defineToolcraftPerformance,
8
+ defaultToolcraftBrowserCheckPolicy,
9
+ type ToolcraftBrowserCheckPolicy,
8
10
  validateToolcraftPerformanceCoverage,
9
11
  } from "./performance";
10
12
 
@@ -180,6 +182,14 @@ function createViewportZoomStressScenario() {
180
182
  function createMaxValueStressFixture(value: unknown, reason = "Maximum workload value.") {
181
183
  return {
182
184
  kind: "max-value" as const,
185
+ loadProfile: {
186
+ hardLimit: value,
187
+ metric: "numeric-max" as const,
188
+ smoothTarget: value,
189
+ smoothTargetRatio: 1,
190
+ target: "render.density",
191
+ userFacingRange: "fully-guaranteed" as const,
192
+ },
183
193
  reason,
184
194
  value,
185
195
  };
@@ -191,6 +201,14 @@ function createCustomStressFixture(
191
201
  ) {
192
202
  return {
193
203
  kind: "custom" as const,
204
+ loadProfile: {
205
+ hardLimit: value,
206
+ metric: "custom" as const,
207
+ smoothTarget: value,
208
+ smoothTargetRatio: 1,
209
+ target: "renderer.output",
210
+ userFacingRange: "fully-guaranteed" as const,
211
+ },
194
212
  reason,
195
213
  value,
196
214
  };
@@ -227,11 +245,60 @@ function createMediaStressFixture(
227
245
  ) {
228
246
  return {
229
247
  kind: "media" as const,
248
+ loadProfile: {
249
+ hardLimit: value,
250
+ metric: "media-area" as const,
251
+ smoothTarget: value,
252
+ smoothTargetRatio: 1,
253
+ target: "source.image",
254
+ userFacingRange: "fully-guaranteed" as const,
255
+ },
230
256
  reason,
231
257
  value,
232
258
  };
233
259
  }
234
260
 
261
+ function createDegradedMaxValueStressFixture() {
262
+ return {
263
+ kind: "max-value" as const,
264
+ loadProfile: {
265
+ degradationStepPercent: 10 as const,
266
+ evidence: [
267
+ {
268
+ attemptedTarget: 12,
269
+ decision: "Keep 12 available as high density while guaranteeing smooth drag through 11.",
270
+ measuredResult: "At density 12, maxFrameGapMs 148 exceeded the 80ms budget.",
271
+ optimizationAttempted: "Cached glyph atlas and coalesced preview work to requestAnimationFrame.",
272
+ result: "failed" as const,
273
+ scenarioId: "density-drag",
274
+ },
275
+ ],
276
+ hardLimit: 12,
277
+ metric: "numeric-max" as const,
278
+ smoothTarget: 11,
279
+ smoothTargetRatio: 0.9,
280
+ target: "render.density",
281
+ userFacingRange: "experimental-above-smooth" as const,
282
+ },
283
+ reason: "Density 11 is the measured smooth target after hard-limit testing.",
284
+ value: 11,
285
+ };
286
+ }
287
+
288
+ function createMeasuredWebglEvidence(scenarioId = "text-preview-render") {
289
+ return [
290
+ {
291
+ alternativeStrategy: "webgl" as const,
292
+ decision:
293
+ "Canvas 2D remains selected because cached CPU rendering stayed within budget for this product fixture while preserving export parity.",
294
+ fixture: "large product stress fixture at render scale 2",
295
+ measuredResult:
296
+ "WebGL comparison: maxFrameGapMs 72, maxLongTaskMs 64; Canvas 2D comparison: maxFrameGapMs 68, maxLongTaskMs 58.",
297
+ scenarioId,
298
+ },
299
+ ];
300
+ }
301
+
235
302
  function createTextRendererPipeline() {
236
303
  return {
237
304
  interactionInvalidation: [
@@ -395,6 +462,61 @@ function createDenseCanvasPipeline() {
395
462
  } as const;
396
463
  }
397
464
 
465
+ function createMainThreadCanvasCompositePipeline() {
466
+ return {
467
+ interactionInvalidation: [
468
+ {
469
+ interaction: "control-drag",
470
+ invalidates: ["text-texture", "warp-composite", "optical-composite"],
471
+ targets: ["render.density"],
472
+ },
473
+ {
474
+ interaction: "control-change",
475
+ invalidates: ["text-texture", "warp-composite", "optical-composite"],
476
+ targets: ["render.mode"],
477
+ },
478
+ {
479
+ interaction: "viewport-zoom",
480
+ invalidates: [],
481
+ mustNotInvalidate: ["text-texture", "warp-composite", "optical-composite"],
482
+ targets: ["canvas.viewport"],
483
+ },
484
+ ],
485
+ passes: [
486
+ {
487
+ cacheKey: ["product.content", "render.density", "canvas.size", "canvas.renderScale"],
488
+ id: "text-texture",
489
+ inputs: ["product.content", "render.density", "canvas.size", "canvas.renderScale"],
490
+ invalidatedBy: ["product.content", "render.density", "canvas.size", "canvas.renderScale"],
491
+ kind: "rasterize",
492
+ output: "intermediate",
493
+ quality: "retina",
494
+ runsOn: "main",
495
+ },
496
+ {
497
+ cacheKey: ["text-texture", "render.density", "canvas.size"],
498
+ id: "warp-composite",
499
+ inputs: ["text-texture", "render.density", "canvas.size"],
500
+ invalidatedBy: ["text-texture", "render.density", "canvas.size"],
501
+ kind: "composite",
502
+ output: "preview",
503
+ quality: "retina",
504
+ runsOn: "main",
505
+ },
506
+ {
507
+ cacheKey: ["warp-composite", "render.mode", "canvas.renderScale"],
508
+ id: "optical-composite",
509
+ inputs: ["warp-composite", "render.mode", "canvas.renderScale"],
510
+ invalidatedBy: ["warp-composite", "render.mode", "canvas.renderScale"],
511
+ kind: "composite",
512
+ output: "preview",
513
+ quality: "retina",
514
+ runsOn: "main",
515
+ },
516
+ ],
517
+ } as const;
518
+ }
519
+
398
520
  function createPixelRendererPipeline() {
399
521
  return {
400
522
  interactionInvalidation: [
@@ -628,6 +750,71 @@ function createAnimatedVectorRendererConfig(options: { includeViewportDrag: bool
628
750
  }
629
751
 
630
752
  describe("Toolcraft template performance contract", () => {
753
+ it("defaults browser checks to agent browser with Playwright fallback", () => {
754
+ const config = defineToolcraftPerformance({
755
+ rendererStrategy: "none",
756
+ rendererWorkload: "none",
757
+ scenarios: [
758
+ {
759
+ automated: true,
760
+ automatedTestName: "perf: opacity drag stays responsive",
761
+ browser: true,
762
+ browserTestName: "browser perf: opacity drag stays responsive",
763
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
764
+ controlLabel: "Opacity",
765
+ expectedObservable: "Dragging Opacity stays responsive.",
766
+ fixture: "opacity responsiveness fixture",
767
+ id: "opacity-drag",
768
+ interaction: "control-drag",
769
+ target: "render.opacity",
770
+ workload: false,
771
+ },
772
+ ],
773
+ usesCustomRenderer: false,
774
+ workloadTargets: [],
775
+ });
776
+
777
+ expect(config.browserCheckPolicy).toEqual(defaultToolcraftBrowserCheckPolicy);
778
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toEqual([]);
779
+ });
780
+
781
+ it("rejects explicit Playwright-first browser check policies", () => {
782
+ const config = defineToolcraftPerformance({
783
+ browserCheckPolicy: {
784
+ fallbackRunner: "playwright",
785
+ fallbackWhen: ["agent-browser-unavailable"],
786
+ preferredRunner: "playwright",
787
+ } as unknown as ToolcraftBrowserCheckPolicy,
788
+ rendererStrategy: "none",
789
+ rendererWorkload: "none",
790
+ scenarios: [
791
+ {
792
+ automated: true,
793
+ automatedTestName: "perf: opacity drag stays responsive",
794
+ browser: true,
795
+ browserTestName: "browser perf: opacity drag stays responsive",
796
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
797
+ controlLabel: "Opacity",
798
+ expectedObservable: "Dragging Opacity stays responsive.",
799
+ fixture: "opacity responsiveness fixture",
800
+ id: "opacity-drag",
801
+ interaction: "control-drag",
802
+ target: "render.opacity",
803
+ workload: false,
804
+ },
805
+ ],
806
+ usesCustomRenderer: false,
807
+ workloadTargets: [],
808
+ });
809
+
810
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toEqual(
811
+ expect.arrayContaining([
812
+ 'browserCheckPolicy.preferredRunner must be "agent-browser" so AI agents use their controlled browser before fallback automation.',
813
+ 'browserCheckPolicy.fallbackWhen must include "ci" so CI/non-agent automation has a portable performance runner.',
814
+ ]),
815
+ );
816
+ });
817
+
631
818
  it("collects performance-sensitive controls from schema", () => {
632
819
  expect(collectToolcraftPerformanceSensitiveControls(testSchema)).toEqual([
633
820
  expect.objectContaining({
@@ -682,6 +869,7 @@ describe("Toolcraft template performance contract", () => {
682
869
  const unclassifiedSchema = defineToolcraft({
683
870
  canvas: {
684
871
  enabled: true,
872
+ renderScale: true,
685
873
  sizing: { mode: "intrinsic-media" },
686
874
  },
687
875
  panels: {
@@ -704,6 +892,7 @@ describe("Toolcraft template performance contract", () => {
704
892
  ],
705
893
  title: "Runtime Controls",
706
894
  },
895
+ timeline: { mode: "playback" },
707
896
  },
708
897
  });
709
898
 
@@ -1195,6 +1384,81 @@ describe("Toolcraft template performance contract", () => {
1195
1384
  );
1196
1385
  });
1197
1386
 
1387
+ it("rejects short large-text values hidden inside custom fixtures", () => {
1388
+ const config = defineToolcraftPerformance({
1389
+ rendererStrategy: "none",
1390
+ rendererWorkload: "none",
1391
+ scenarios: [
1392
+ {
1393
+ automated: true,
1394
+ automatedTestName: "perf: preview render stays under budget",
1395
+ browser: true,
1396
+ browserTestName: "browser perf: preview render stays under budget",
1397
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
1398
+ expectedObservable: "Preview renders the heaviest output without freezing.",
1399
+ fixture: "combined large text fixture",
1400
+ id: "preview-render",
1401
+ interaction: "preview-render",
1402
+ stress: true,
1403
+ stressFixture: {
1404
+ kind: "custom",
1405
+ reason: "Combined renderer stress includes pasted product content.",
1406
+ value: {
1407
+ content: "short text",
1408
+ renderScale: 2,
1409
+ },
1410
+ },
1411
+ workload: false,
1412
+ },
1413
+ ],
1414
+ usesCustomRenderer: false,
1415
+ workloadTargets: [],
1416
+ });
1417
+
1418
+ expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual(
1419
+ expect.arrayContaining([
1420
+ "preview-render custom stressFixture.content must contain at least 50000 characters.",
1421
+ "preview-render custom stressFixture.content must contain at least 1000 lines.",
1422
+ ]),
1423
+ );
1424
+ });
1425
+
1426
+ it("rejects custom render scale fixtures that are not concrete high-resolution values", () => {
1427
+ const config = defineToolcraftPerformance({
1428
+ rendererStrategy: "none",
1429
+ rendererWorkload: "none",
1430
+ scenarios: [
1431
+ {
1432
+ automated: true,
1433
+ automatedTestName: "perf: preview render stays under budget",
1434
+ browser: true,
1435
+ browserTestName: "browser perf: preview render stays under budget",
1436
+ budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
1437
+ expectedObservable: "Preview renders the heaviest output without freezing.",
1438
+ fixture: "combined render scale fixture",
1439
+ id: "preview-render",
1440
+ interaction: "preview-render",
1441
+ stress: true,
1442
+ stressFixture: {
1443
+ kind: "custom",
1444
+ reason: "Combined renderer stress includes selected resolution scale.",
1445
+ value: {
1446
+ density: 12,
1447
+ renderScale: "2",
1448
+ },
1449
+ },
1450
+ workload: false,
1451
+ },
1452
+ ],
1453
+ usesCustomRenderer: false,
1454
+ workloadTargets: [],
1455
+ });
1456
+
1457
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
1458
+ "preview-render custom stressFixture.renderScale must be a numeric Resolution scale greater than 1 so browser tests prove high-resolution backing pixels.",
1459
+ );
1460
+ });
1461
+
1198
1462
  it("accepts large text workload scenarios with machine-checkable stress fixtures", () => {
1199
1463
  const config = defineToolcraftPerformance({
1200
1464
  rendererStrategy: "none",
@@ -1224,6 +1488,352 @@ describe("Toolcraft template performance contract", () => {
1224
1488
  expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual([]);
1225
1489
  });
1226
1490
 
1491
+ it("requires max-value fixtures to declare a measured load profile", () => {
1492
+ const config = defineToolcraftPerformance({
1493
+ rendererStrategy: "none",
1494
+ rendererWorkload: "none",
1495
+ scenarios: [
1496
+ {
1497
+ automated: true,
1498
+ automatedTestName: "perf: density drag stays responsive",
1499
+ browser: true,
1500
+ browserTestName: "browser perf: density drag stays responsive",
1501
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1502
+ controlLabel: "Density",
1503
+ expectedObservable: "Dragging Density updates text output without blocking the UI.",
1504
+ fixture: "runtime density fixture",
1505
+ id: "density-drag",
1506
+ interaction: "control-drag",
1507
+ stressFixture: {
1508
+ kind: "max-value",
1509
+ reason: "Density 11 is claimed as the smooth target without hard-limit evidence.",
1510
+ value: 11,
1511
+ },
1512
+ target: "render.density",
1513
+ values: { default: 4, max: 12, min: 1 },
1514
+ workload: true,
1515
+ },
1516
+ ],
1517
+ usesCustomRenderer: false,
1518
+ workloadTargets: ["render.density"],
1519
+ });
1520
+
1521
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1522
+ "density-drag stressFixture.loadProfile must declare hardLimit, smoothTarget, and smoothTargetRatio so performance workload is a measured smooth target rather than a hidden toy fixture.",
1523
+ );
1524
+ });
1525
+
1526
+ it("rejects load profiles whose smooth target does not match the applied fixture", () => {
1527
+ const config = defineToolcraftPerformance({
1528
+ rendererStrategy: "none",
1529
+ rendererWorkload: "none",
1530
+ scenarios: [
1531
+ {
1532
+ automated: true,
1533
+ automatedTestName: "perf: density drag stays responsive",
1534
+ browser: true,
1535
+ browserTestName: "browser perf: density drag stays responsive",
1536
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1537
+ controlLabel: "Density",
1538
+ expectedObservable: "Dragging Density updates text output without blocking the UI.",
1539
+ fixture: "runtime density fixture",
1540
+ id: "density-drag",
1541
+ interaction: "control-drag",
1542
+ stressFixture: {
1543
+ kind: "max-value",
1544
+ loadProfile: {
1545
+ hardLimit: 12,
1546
+ metric: "numeric-max",
1547
+ smoothTarget: 11,
1548
+ smoothTargetRatio: 0.9,
1549
+ target: "render.density",
1550
+ userFacingRange: "experimental-above-smooth",
1551
+ },
1552
+ reason: "Density 10 does not match the documented smooth target.",
1553
+ value: 10,
1554
+ },
1555
+ target: "render.density",
1556
+ values: { default: 4, max: 12, min: 1 },
1557
+ workload: true,
1558
+ },
1559
+ ],
1560
+ usesCustomRenderer: false,
1561
+ workloadTargets: ["render.density"],
1562
+ });
1563
+
1564
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
1565
+ expect.arrayContaining([
1566
+ "density-drag stressFixture.loadProfile.smoothTarget must match stressFixture.value so browser tests apply the documented smooth workload target.",
1567
+ "density-drag stressFixture.loadProfile.degradationStepPercent must be 10 when smoothTargetRatio is below 1.",
1568
+ "density-drag stressFixture.loadProfile.evidence must include failed measurements for each 10 percent step above smoothTargetRatio 0.9.",
1569
+ ]),
1570
+ );
1571
+ });
1572
+
1573
+ it("rejects load profiles whose ratio does not describe the measured smooth target", () => {
1574
+ const config = defineToolcraftPerformance({
1575
+ rendererStrategy: "none",
1576
+ rendererWorkload: "none",
1577
+ scenarios: [
1578
+ {
1579
+ automated: true,
1580
+ automatedTestName: "perf: density drag stays responsive",
1581
+ browser: true,
1582
+ browserTestName: "browser perf: density drag stays responsive",
1583
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1584
+ controlLabel: "Density",
1585
+ expectedObservable: "Dragging Density updates text output without blocking the UI.",
1586
+ fixture: "runtime density fixture",
1587
+ id: "density-drag",
1588
+ interaction: "control-drag",
1589
+ stressFixture: {
1590
+ kind: "max-value",
1591
+ loadProfile: {
1592
+ degradationStepPercent: 10,
1593
+ evidence: [
1594
+ {
1595
+ attemptedTarget: 12,
1596
+ decision:
1597
+ "Density 12 remains experimental because it misses frame budget after optimization.",
1598
+ measuredResult: "At density 12, maxFrameGapMs 148 exceeded the 80ms budget.",
1599
+ optimizationAttempted:
1600
+ "Cached glyph atlas and coalesced preview work to requestAnimationFrame.",
1601
+ result: "failed",
1602
+ scenarioId: "density-drag",
1603
+ },
1604
+ ],
1605
+ hardLimit: 12,
1606
+ metric: "numeric-max",
1607
+ smoothTarget: 2,
1608
+ smoothTargetRatio: 0.9,
1609
+ target: "render.density",
1610
+ userFacingRange: "experimental-above-smooth",
1611
+ },
1612
+ reason: "Density 2 is far below the claimed 90 percent smooth target.",
1613
+ value: 2,
1614
+ },
1615
+ target: "render.density",
1616
+ values: { default: 4, max: 12, min: 1 },
1617
+ workload: true,
1618
+ },
1619
+ ],
1620
+ usesCustomRenderer: false,
1621
+ workloadTargets: ["render.density"],
1622
+ });
1623
+
1624
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1625
+ "density-drag stressFixture.loadProfile.smoothTargetRatio 0.9 must describe the actual smooth target ratio 0.1667.",
1626
+ );
1627
+ });
1628
+
1629
+ it("rejects fully guaranteed custom load profiles whose smooth target is not the hard limit", () => {
1630
+ const config = defineToolcraftPerformance({
1631
+ rendererStrategy: "canvas-2d",
1632
+ rendererWorkload: "pixel-output",
1633
+ scenarios: [
1634
+ {
1635
+ automated: true,
1636
+ automatedTestName: "perf: combined preview stays responsive",
1637
+ browser: true,
1638
+ browserTestName: "browser perf: combined preview stays responsive",
1639
+ budget: { maxPreviewMs: 200, maxRenderMs: 200 },
1640
+ expectedObservable: "Preview renders the combined source and effect workload.",
1641
+ fixture: "combined custom fixture",
1642
+ id: "combined-preview",
1643
+ interaction: "preview-render",
1644
+ stress: true,
1645
+ stressFixture: {
1646
+ kind: "custom",
1647
+ loadProfile: {
1648
+ hardLimit: { density: 12, sourceMedia: { height: 2160, width: 3840 } },
1649
+ metric: "custom",
1650
+ smoothTarget: { density: 4, sourceMedia: { height: 1080, width: 1920 } },
1651
+ smoothTargetRatio: 1,
1652
+ target: "renderer.output",
1653
+ userFacingRange: "fully-guaranteed",
1654
+ },
1655
+ reason: "This claims full coverage while applying a smaller custom state.",
1656
+ value: { density: 4, sourceMedia: { height: 1080, width: 1920 } },
1657
+ },
1658
+ target: "renderer.output",
1659
+ workload: false,
1660
+ },
1661
+ ],
1662
+ usesCustomRenderer: true,
1663
+ workloadTargets: [],
1664
+ });
1665
+
1666
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1667
+ "combined-preview stressFixture.loadProfile.smoothTargetRatio 1 requires smoothTarget to equal hardLimit.",
1668
+ );
1669
+ });
1670
+
1671
+ it("accepts a 10 percent degraded smooth target with hard-limit failure evidence", () => {
1672
+ const config = defineToolcraftPerformance({
1673
+ rendererStrategy: "none",
1674
+ rendererWorkload: "none",
1675
+ scenarios: [
1676
+ {
1677
+ automated: true,
1678
+ automatedTestName: "perf: density drag stays responsive",
1679
+ browser: true,
1680
+ browserTestName: "browser perf: density drag stays responsive",
1681
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1682
+ controlLabel: "Density",
1683
+ expectedObservable: "Dragging Density updates text output without blocking the UI.",
1684
+ fixture: "runtime density fixture",
1685
+ id: "density-drag",
1686
+ interaction: "control-drag",
1687
+ stressFixture: createDegradedMaxValueStressFixture(),
1688
+ target: "render.density",
1689
+ values: { default: 4, max: 12, min: 1 },
1690
+ workload: true,
1691
+ },
1692
+ {
1693
+ automated: true,
1694
+ automatedTestName: "perf: mode change stays responsive",
1695
+ browser: true,
1696
+ browserTestName: "browser perf: mode change stays responsive",
1697
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1698
+ controlLabel: "Mode",
1699
+ expectedObservable: "Changing Mode updates text output without blocking the UI.",
1700
+ fixture: "runtime mode fixture",
1701
+ id: "mode-change",
1702
+ interaction: "control-change",
1703
+ target: "render.mode",
1704
+ values: { default: "soft", max: "sharp", min: "soft" },
1705
+ workload: false,
1706
+ },
1707
+ ],
1708
+ usesCustomRenderer: false,
1709
+ workloadTargets: ["render.density"],
1710
+ });
1711
+
1712
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
1713
+ });
1714
+
1715
+ it("requires evidence for every skipped 10 percent degradation step", () => {
1716
+ const degradedFixture = createDegradedMaxValueStressFixture();
1717
+ const config = defineToolcraftPerformance({
1718
+ rendererStrategy: "none",
1719
+ rendererWorkload: "none",
1720
+ scenarios: [
1721
+ {
1722
+ automated: true,
1723
+ automatedTestName: "perf: density drag stays responsive",
1724
+ browser: true,
1725
+ browserTestName: "browser perf: density drag stays responsive",
1726
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1727
+ controlLabel: "Density",
1728
+ expectedObservable: "Dragging Density updates text output without blocking the UI.",
1729
+ fixture: "runtime density fixture",
1730
+ id: "density-drag",
1731
+ interaction: "control-drag",
1732
+ stressFixture: {
1733
+ ...degradedFixture,
1734
+ loadProfile: {
1735
+ ...degradedFixture.loadProfile,
1736
+ smoothTarget: 10,
1737
+ smoothTargetRatio: 0.8,
1738
+ },
1739
+ value: 10,
1740
+ },
1741
+ target: "render.density",
1742
+ values: { default: 4, max: 12, min: 1 },
1743
+ workload: true,
1744
+ },
1745
+ ],
1746
+ usesCustomRenderer: false,
1747
+ workloadTargets: ["render.density"],
1748
+ });
1749
+
1750
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1751
+ "density-drag stressFixture.loadProfile.evidence must include failed measurements for each 10 percent step above smoothTargetRatio 0.8.",
1752
+ );
1753
+ });
1754
+
1755
+ it("rejects numeric hard limits that do not match schema max", () => {
1756
+ const fixture = createDegradedMaxValueStressFixture();
1757
+ const config = defineToolcraftPerformance({
1758
+ rendererStrategy: "none",
1759
+ rendererWorkload: "none",
1760
+ scenarios: [
1761
+ {
1762
+ automated: true,
1763
+ automatedTestName: "perf: density drag stays responsive",
1764
+ browser: true,
1765
+ browserTestName: "browser perf: density drag stays responsive",
1766
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1767
+ controlLabel: "Density",
1768
+ expectedObservable: "Dragging Density updates text output without blocking the UI.",
1769
+ fixture: "runtime density fixture",
1770
+ id: "density-drag",
1771
+ interaction: "control-drag",
1772
+ stressFixture: {
1773
+ ...fixture,
1774
+ loadProfile: {
1775
+ ...fixture.loadProfile,
1776
+ hardLimit: 10,
1777
+ },
1778
+ },
1779
+ target: "render.density",
1780
+ values: { default: 4, max: 12, min: 1 },
1781
+ workload: true,
1782
+ },
1783
+ ],
1784
+ usesCustomRenderer: false,
1785
+ workloadTargets: ["render.density"],
1786
+ });
1787
+
1788
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1789
+ "density-drag stressFixture.loadProfile.hardLimit must match schema max 12.",
1790
+ );
1791
+ });
1792
+
1793
+ it("rejects toy many-items fixtures below their declared useful count", () => {
1794
+ const config = defineToolcraftPerformance({
1795
+ rendererStrategy: "none",
1796
+ rendererWorkload: "none",
1797
+ scenarios: [
1798
+ {
1799
+ automated: true,
1800
+ automatedTestName: "perf: items drag stays responsive",
1801
+ browser: true,
1802
+ browserTestName: "browser perf: items drag stays responsive",
1803
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
1804
+ controlLabel: "Density",
1805
+ expectedObservable: "Dragging Density updates many items without blocking the UI.",
1806
+ fixture: "many item fixture",
1807
+ id: "items-density-drag",
1808
+ interaction: "control-drag",
1809
+ stressFixture: {
1810
+ kind: "many-items",
1811
+ loadProfile: {
1812
+ hardLimit: 100,
1813
+ metric: "count",
1814
+ smoothTarget: 2,
1815
+ smoothTargetRatio: 1,
1816
+ target: "items.count",
1817
+ userFacingRange: "fully-guaranteed",
1818
+ },
1819
+ minCount: 10,
1820
+ reason: "Two items is too small to prove collection performance.",
1821
+ value: 2,
1822
+ },
1823
+ target: "render.density",
1824
+ values: { default: 4, max: 12, min: 1 },
1825
+ workload: true,
1826
+ },
1827
+ ],
1828
+ usesCustomRenderer: false,
1829
+ workloadTargets: ["render.density"],
1830
+ });
1831
+
1832
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
1833
+ "items-density-drag stressFixture.value must include at least 10 items for many-items fixtures.",
1834
+ );
1835
+ });
1836
+
1227
1837
  it("requires workload controls in custom renderers to declare an independent workload fixture", () => {
1228
1838
  const config = defineToolcraftPerformance({
1229
1839
  rendererStrategy: "canvas-2d",
@@ -1469,6 +2079,49 @@ describe("Toolcraft template performance contract", () => {
1469
2079
  expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
1470
2080
  });
1471
2081
 
2082
+ it("rejects main-thread Canvas 2D raster/composite pipelines misclassified as text output", () => {
2083
+ const config = defineToolcraftPerformance({
2084
+ rendererStrategy: "canvas-2d",
2085
+ rendererTechnique: {
2086
+ exportRenderer: "canvas-2d",
2087
+ fidelityRisks: ["rasterized text must preserve the shader-style composite"],
2088
+ layers: [
2089
+ {
2090
+ content: ["text", "noise"],
2091
+ exportMode: "composited",
2092
+ id: "product-text-composite",
2093
+ intentionalRasterizationReason:
2094
+ "The product text is warped, tinted, and exported as final pixels.",
2095
+ kind: "product-foreground",
2096
+ primitiveCount: "medium",
2097
+ renderer: "canvas-2d",
2098
+ uiSelector: '[data-toolcraft-renderer-layer="product-text-composite"]',
2099
+ },
2100
+ ],
2101
+ performanceRisks: [
2102
+ "WebGL measured stress evidence was reviewed before keeping the text composite on Canvas 2D.",
2103
+ ],
2104
+ previewRenderer: "canvas-2d",
2105
+ productRepresentation: "mixed",
2106
+ rendererStrategy: "canvas-2d",
2107
+ rendererWorkload: "text-output",
2108
+ sourceRepresentation: "dom-text",
2109
+ whyNotAlternativeStrategies: [
2110
+ "WebGL measured stress evidence showed text atlas uploads dominated this fixture.",
2111
+ ],
2112
+ },
2113
+ rendererWorkload: "text-output",
2114
+ rendererPipeline: createMainThreadCanvasCompositePipeline(),
2115
+ scenarios: createTextRendererScenarios(),
2116
+ usesCustomRenderer: true,
2117
+ workloadTargets: ["render.density"],
2118
+ });
2119
+
2120
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
2121
+ 'Canvas 2D pipelines with main-thread rasterize/composite preview pressure must use rendererWorkload "pixel-output" or move expensive passes off the main thread; received "text-output".',
2122
+ );
2123
+ });
2124
+
1472
2125
  it("requires animated custom renderers to test viewport dragging during animation", () => {
1473
2126
  const config = createAnimatedVectorRendererConfig({ includeViewportDrag: false });
1474
2127
 
@@ -1621,10 +2274,10 @@ describe("Toolcraft template performance contract", () => {
1621
2274
  });
1622
2275
 
1623
2276
  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.",
2277
+ "Detail-heavy Canvas 2D renderers must include rendererTechnique.measuredAlternativeEvidence for WebGL/WebGPU stress comparison before keeping the pixel work on CPU.",
1625
2278
  );
1626
2279
  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.',
2280
+ 'rendererWorkload "pixel-output" should use rendererStrategy "webgl" or "webgpu", received "canvas-2d". Keeping a CPU renderer requires rendererTechnique.measuredAlternativeEvidence for WebGL/WebGPU stress comparison.',
1628
2281
  );
1629
2282
  });
1630
2283
 
@@ -1637,6 +2290,7 @@ describe("Toolcraft template performance contract", () => {
1637
2290
  performanceRisks: [
1638
2291
  "Measured WebGL stress evidence showed setup overhead exceeded cached Canvas 2D worker output for this static fixture.",
1639
2292
  ],
2293
+ measuredAlternativeEvidence: createMeasuredWebglEvidence(),
1640
2294
  previewRenderer: "canvas-2d",
1641
2295
  productRepresentation: "pixel",
1642
2296
  rendererStrategy: "canvas-2d",
@@ -2267,6 +2921,7 @@ describe("Toolcraft template performance contract", () => {
2267
2921
  renderer: "canvas-2d",
2268
2922
  },
2269
2923
  ],
2924
+ measuredAlternativeEvidence: createMeasuredWebglEvidence(),
2270
2925
  performanceRisks: ["dense background redraw can be expensive at large output size"],
2271
2926
  previewRenderer: "canvas-2d",
2272
2927
  productRepresentation: "mixed",