@pixel-point/toolcraft 0.0.16 → 0.0.17

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 (170) hide show
  1. package/package.json +1 -1
  2. package/src/generate-performance-iteration-execution.test.mjs +2 -2
  3. package/src/generate-test-demand-only-lifecycle.mjs +14 -3
  4. package/src/generate-test-dependency-sandbox.mjs +42 -5
  5. package/src/generate-test-doc-assertions.mjs +68 -18
  6. package/src/generate-test-doc-assertions.test.mjs +22 -0
  7. package/src/generate-test-file-assertions.mjs +43 -76
  8. package/src/generate-test-output-assertions.mjs +95 -0
  9. package/src/generate-test-package-assertions.mjs +6 -0
  10. package/src/generate-test-performance-boundary-assertions.mjs +45 -0
  11. package/src/generate-test-product-fixture.mjs +1 -0
  12. package/src/generate-test-product-fixture.test.mjs +18 -0
  13. package/src/generate-test-product-iteration.mjs +1 -0
  14. package/src/generate-test-product-iteration.test.mjs +1 -0
  15. package/src/generate-test-standalone-delivery-stages.mjs +14 -6
  16. package/src/generate.test.mjs +14 -0
  17. package/templates/runtime/contracts/component-contracts.choices.test.ts +12 -9
  18. package/templates/runtime/contracts/component-contracts.choices.ts +10 -9
  19. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +17 -2
  20. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +1 -1
  21. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +1 -1
  22. package/templates/runtime/contracts/component-contracts.runtime.ts +3 -2
  23. package/templates/runtime/contracts/decision-contracts.test.ts +25 -4
  24. package/templates/runtime/contracts/decision-contracts.ts +3 -3
  25. package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
  26. package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
  27. package/templates/runtime/export/artifact-download.test.ts +71 -0
  28. package/templates/runtime/export/artifact-download.ts +80 -0
  29. package/templates/runtime/export/artifact-export-request.ts +23 -0
  30. package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
  31. package/templates/runtime/export/artifact-export-settings.ts +96 -0
  32. package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
  33. package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
  34. package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
  35. package/templates/runtime/export/artifact-frame-state.ts +44 -0
  36. package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
  37. package/templates/runtime/export/artifact-scene-frame.ts +118 -0
  38. package/templates/runtime/export/export-background.ts +65 -0
  39. package/templates/runtime/export/export-error.ts +43 -0
  40. package/templates/runtime/export/export.test.ts +6 -186
  41. package/templates/runtime/export/image-artifact-export.test.ts +111 -0
  42. package/templates/runtime/export/image-artifact-export.ts +145 -0
  43. package/templates/runtime/export/index.ts +14 -1
  44. package/templates/runtime/export/product-export-renderer.test.ts +77 -0
  45. package/templates/runtime/export/product-export-renderer.ts +63 -0
  46. package/templates/runtime/export/video-artifact-export.test.ts +166 -0
  47. package/templates/runtime/export/video-artifact-export.ts +164 -0
  48. package/templates/runtime/export/video-encoding-backend.ts +124 -0
  49. package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
  50. package/templates/runtime/export/video-encoding-policy.ts +86 -0
  51. package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
  52. package/templates/runtime/export/video-frame-schedule.ts +42 -0
  53. package/templates/runtime/index.ts +1 -0
  54. package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
  55. package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
  56. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +4 -303
  57. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +12 -27
  58. package/templates/runtime/react/app-shell/toolcraft-app.tsx +22 -4
  59. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
  60. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
  61. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
  62. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
  63. package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
  64. package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
  65. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
  66. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
  67. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
  68. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +17 -83
  69. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +42 -84
  70. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +62 -0
  71. package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
  72. package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
  73. package/templates/runtime/scene/scene-bounds.ts +1 -2
  74. package/templates/runtime/schema/canvas-render-scale.ts +62 -0
  75. package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
  76. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +3 -3
  77. package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
  78. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
  79. package/templates/runtime/schema/define-toolcraft.ts +1 -1
  80. package/templates/runtime/schema/schema-resolvers.ts +0 -56
  81. package/templates/runtime/schema/types.ts +4 -4
  82. package/templates/runtime/state/canvas-background-state.ts +6 -0
  83. package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
  84. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
  85. package/templates/starter/AGENTS.md +8 -7
  86. package/templates/starter/NOTICE.md +6 -0
  87. package/templates/starter/docs/toolcraft/README.md +2 -1
  88. package/templates/starter/docs/toolcraft/acceptance-testing.md +8 -6
  89. package/templates/starter/docs/toolcraft/agent-worklog.md +3 -3
  90. package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -3
  91. package/templates/starter/docs/toolcraft/core/media-upload.md +2 -2
  92. package/templates/starter/docs/toolcraft/core/performance.md +14 -3
  93. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +3 -1
  94. package/templates/starter/docs/toolcraft/core/setup-export.md +16 -12
  95. package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
  96. package/templates/starter/docs/toolcraft/performance.md +20 -6
  97. package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
  98. package/templates/starter/docs/toolcraft/schema-reference.md +2 -6
  99. package/templates/starter/docs/toolcraft/workflow.md +7 -2
  100. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
  101. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +43 -1
  102. package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
  103. package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
  104. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
  105. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
  106. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +4 -1
  107. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +0 -1
  108. package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
  109. package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
  110. package/templates/starter/e2e/app-performance.spec.ts +16 -9
  111. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +9 -330
  112. package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
  113. package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
  114. package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
  115. package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
  116. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +4 -1
  117. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +43 -9
  118. package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
  119. package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
  120. package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
  121. package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
  122. package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
  123. package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
  124. package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
  125. package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
  126. package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
  127. package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
  128. package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
  129. package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
  130. package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
  131. package/templates/starter/e2e/performance-path-helpers.ts +210 -91
  132. package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
  133. package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
  134. package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
  135. package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
  136. package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
  137. package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
  138. package/templates/starter/gitignore +1 -0
  139. package/templates/starter/package.json +1 -0
  140. package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
  141. package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
  142. package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
  143. package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
  144. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +24 -2
  145. package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
  146. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +46 -0
  147. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
  148. package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
  149. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +168 -0
  150. package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
  151. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
  152. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +155 -206
  153. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
  154. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
  155. package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
  156. package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
  157. package/templates/starter/src/app/acceptance/output-background-rules.ts +1 -1
  158. package/templates/starter/src/app/acceptance/output-export.ts +13 -2
  159. package/templates/starter/src/app/acceptance/types.ts +7 -0
  160. package/templates/starter/src/app/acceptance/validate-coverage.ts +2 -1
  161. package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
  162. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
  163. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +42 -1
  164. package/templates/starter/src/app/starter-acceptance.ts +2 -0
  165. package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
  166. package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
  167. package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
  168. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -0
  169. package/templates/starter/src/routes/index.tsx +1 -0
  170. package/templates/runtime/export/export.ts +0 -166
@@ -0,0 +1,351 @@
1
+ import type { ToolcraftCanvasBackingMetrics } from "./browser-render-scale-evidence";
2
+
3
+ export type ToolcraftPerformanceCanvasQualityObservation = Readonly<{
4
+ kind: "attribute" | "installed" | "resize" | "visible-size";
5
+ metrics: ToolcraftCanvasBackingMetrics;
6
+ }>;
7
+
8
+ type ToolcraftPerformanceCanvasQualityHandshake = Readonly<{
9
+ kind: "handshake";
10
+ }>;
11
+
12
+ export type ToolcraftPerformanceCanvasQualityInstallerOptions = Readonly<{
13
+ baselineCssSize: Readonly<{ height: number; width: number }>;
14
+ bindingName: string;
15
+ canvasSelector: string;
16
+ stateKey: string;
17
+ }>;
18
+
19
+ export function isToolcraftPerformanceCanvasQualityHandshake(
20
+ value: unknown,
21
+ ): value is ToolcraftPerformanceCanvasQualityHandshake {
22
+ return (
23
+ typeof value === "object" &&
24
+ value !== null &&
25
+ !Array.isArray(value) &&
26
+ (value as { kind?: unknown }).kind === "handshake"
27
+ );
28
+ }
29
+
30
+ export function isToolcraftPerformanceCanvasQualityObservation(
31
+ value: unknown,
32
+ ): value is ToolcraftPerformanceCanvasQualityObservation {
33
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
34
+ return false;
35
+ }
36
+ const observation = value as Record<string, unknown>;
37
+ const metrics = observation.metrics;
38
+ return (
39
+ (observation.kind === "attribute" ||
40
+ observation.kind === "installed" ||
41
+ observation.kind === "resize" ||
42
+ observation.kind === "visible-size") &&
43
+ typeof metrics === "object" &&
44
+ metrics !== null &&
45
+ !Array.isArray(metrics) &&
46
+ [
47
+ "backingHeight",
48
+ "backingWidth",
49
+ "cssHeight",
50
+ "cssWidth",
51
+ "devicePixelRatio",
52
+ ].every((key) => Number.isFinite((metrics as Record<string, unknown>)[key]))
53
+ );
54
+ }
55
+
56
+ export async function installToolcraftPerformanceCanvasQualityObserver({
57
+ baselineCssSize,
58
+ bindingName,
59
+ canvasSelector,
60
+ stateKey,
61
+ }: ToolcraftPerformanceCanvasQualityInstallerOptions): Promise<void> {
62
+ type BrowserBinding = (
63
+ message:
64
+ | ToolcraftPerformanceCanvasQualityHandshake
65
+ | ToolcraftPerformanceCanvasQualityObservation,
66
+ ) => Promise<boolean>;
67
+ type BrowserState = Readonly<{
68
+ begin: () => void;
69
+ dispose: () => void;
70
+ flush: () => Promise<void>;
71
+ }>;
72
+
73
+ if (window !== window.top || Reflect.get(globalThis, stateKey)) return;
74
+ const binding = Reflect.get(globalThis, bindingName) as
75
+ | BrowserBinding
76
+ | undefined;
77
+ if (typeof binding !== "function") {
78
+ throw new Error("Toolcraft canvas quality guard binding is unavailable.");
79
+ }
80
+
81
+ const earlyObservations: ToolcraftPerformanceCanvasQualityObservation[] = [];
82
+ let earlyLastMetrics: ToolcraftCanvasBackingMetrics | undefined;
83
+ const readVisibleMetrics = (
84
+ canvas: HTMLCanvasElement,
85
+ ): ToolcraftCanvasBackingMetrics | undefined => {
86
+ const rect = canvas.getBoundingClientRect();
87
+ if (
88
+ !canvas.isConnected ||
89
+ rect.width <= 0 ||
90
+ rect.height <= 0 ||
91
+ getComputedStyle(canvas).visibility === "hidden"
92
+ ) {
93
+ return undefined;
94
+ }
95
+ return {
96
+ backingHeight: canvas.height,
97
+ backingWidth: canvas.width,
98
+ cssHeight: rect.height,
99
+ cssWidth: rect.width,
100
+ devicePixelRatio: window.devicePixelRatio || 1,
101
+ };
102
+ };
103
+ const restorePreMutationBackingMetrics = (
104
+ record: MutationRecord,
105
+ basis: ToolcraftCanvasBackingMetrics,
106
+ ): ToolcraftCanvasBackingMetrics => ({
107
+ ...basis,
108
+ ...(record.attributeName === "width"
109
+ ? {
110
+ backingWidth:
111
+ record.oldValue === null ? 300 : Number(record.oldValue),
112
+ }
113
+ : {
114
+ backingHeight:
115
+ record.oldValue === null ? 150 : Number(record.oldValue),
116
+ }),
117
+ });
118
+ const captureEarlyVisibleCanvas = () => {
119
+ const candidate = document.querySelector(canvasSelector);
120
+ if (!(candidate instanceof HTMLCanvasElement)) return;
121
+ const metrics = readVisibleMetrics(candidate);
122
+ if (!metrics) return;
123
+ earlyLastMetrics = metrics;
124
+ earlyObservations.push({ kind: "installed", metrics });
125
+ };
126
+ const captureEarlyRecords = (records: readonly MutationRecord[]) => {
127
+ const previousMetrics = earlyLastMetrics;
128
+ captureEarlyVisibleCanvas();
129
+ for (const record of records) {
130
+ const target = record.target;
131
+ if (
132
+ !(target instanceof HTMLCanvasElement) ||
133
+ !target.matches(canvasSelector) ||
134
+ (record.attributeName !== "width" &&
135
+ record.attributeName !== "height")
136
+ ) {
137
+ continue;
138
+ }
139
+ const basis = readVisibleMetrics(target) ?? {
140
+ backingHeight: target.height,
141
+ backingWidth: target.width,
142
+ cssHeight: baselineCssSize.height,
143
+ cssWidth: baselineCssSize.width,
144
+ devicePixelRatio: window.devicePixelRatio || 1,
145
+ };
146
+ earlyObservations.push({ kind: "attribute", metrics: basis });
147
+ earlyObservations.push({
148
+ kind: "attribute",
149
+ metrics: restorePreMutationBackingMetrics(
150
+ record,
151
+ previousMetrics ?? basis,
152
+ ),
153
+ });
154
+ }
155
+ };
156
+ const authorityObserver = new MutationObserver(captureEarlyRecords);
157
+ authorityObserver.observe(document, {
158
+ attributeFilter: ["class", "height", "style", "width"],
159
+ attributeOldValue: true,
160
+ attributes: true,
161
+ childList: true,
162
+ subtree: true,
163
+ });
164
+ captureEarlyVisibleCanvas();
165
+ let sessionActive = false;
166
+ try {
167
+ sessionActive = await binding({ kind: "handshake" });
168
+ } finally {
169
+ captureEarlyRecords(authorityObserver.takeRecords());
170
+ authorityObserver.disconnect();
171
+ }
172
+ if (!sessionActive || Reflect.get(globalThis, stateKey)) return;
173
+
174
+ const bufferedObservations = [...earlyObservations];
175
+ let attributeObserver: MutationObserver | undefined;
176
+ let discoveryObserver: MutationObserver | undefined;
177
+ let replacementObserver: MutationObserver | undefined;
178
+ let resizeObserver: ResizeObserver | undefined;
179
+ let visibleSizeObserver: MutationObserver | undefined;
180
+ let lastConnectedMetrics: ToolcraftCanvasBackingMetrics | undefined;
181
+ let observedCanvas: HTMLCanvasElement | undefined;
182
+
183
+ const deliver = (
184
+ kind: ToolcraftPerformanceCanvasQualityObservation["kind"],
185
+ metrics: ToolcraftCanvasBackingMetrics,
186
+ ) => {
187
+ bufferedObservations.push({ kind, metrics });
188
+ };
189
+
190
+ const sendCanvas = (
191
+ kind: ToolcraftPerformanceCanvasQualityObservation["kind"],
192
+ canvas: HTMLCanvasElement,
193
+ ) => {
194
+ const metrics = readVisibleMetrics(canvas);
195
+ if (!metrics) return;
196
+ lastConnectedMetrics = metrics;
197
+ deliver(kind, metrics);
198
+ };
199
+
200
+ const sendUsingLastVisibleSize = (
201
+ kind: ToolcraftPerformanceCanvasQualityObservation["kind"],
202
+ canvas: HTMLCanvasElement,
203
+ ) => {
204
+ const connected = readVisibleMetrics(canvas);
205
+ if (connected) lastConnectedMetrics = connected;
206
+ const basis = connected ?? lastConnectedMetrics;
207
+ if (!basis) return;
208
+ deliver(kind, {
209
+ ...basis,
210
+ backingHeight: canvas.height,
211
+ backingWidth: canvas.width,
212
+ });
213
+ };
214
+
215
+ const processAttributeRecords = (
216
+ canvas: HTMLCanvasElement,
217
+ records: readonly MutationRecord[],
218
+ ) => {
219
+ if (records.length === 0) return;
220
+ const previousMetrics = lastConnectedMetrics;
221
+ sendUsingLastVisibleSize("attribute", canvas);
222
+ for (const record of records) {
223
+ if (record.attributeName !== "width" && record.attributeName !== "height")
224
+ continue;
225
+ const currentMetrics = lastConnectedMetrics;
226
+ if (!currentMetrics) continue;
227
+ deliver(
228
+ "attribute",
229
+ restorePreMutationBackingMetrics(
230
+ record,
231
+ previousMetrics ?? currentMetrics,
232
+ ),
233
+ );
234
+ }
235
+ };
236
+
237
+ const disconnectCanvasObservers = () => {
238
+ if (attributeObserver && observedCanvas) {
239
+ processAttributeRecords(observedCanvas, attributeObserver.takeRecords());
240
+ }
241
+ attributeObserver?.disconnect();
242
+ replacementObserver?.disconnect();
243
+ resizeObserver?.disconnect();
244
+ visibleSizeObserver?.disconnect();
245
+ attributeObserver = undefined;
246
+ replacementObserver = undefined;
247
+ resizeObserver = undefined;
248
+ visibleSizeObserver = undefined;
249
+ lastConnectedMetrics = undefined;
250
+ observedCanvas = undefined;
251
+ };
252
+
253
+ const findVisibleCanvas = (): HTMLCanvasElement | undefined => {
254
+ const candidate = document.querySelector(canvasSelector);
255
+ if (!(candidate instanceof HTMLCanvasElement)) return undefined;
256
+ const rect = candidate.getBoundingClientRect();
257
+ return candidate.isConnected && rect.width > 0 && rect.height > 0
258
+ ? candidate
259
+ : undefined;
260
+ };
261
+
262
+ const observeCanvas = (canvas: HTMLCanvasElement) => {
263
+ if (observedCanvas === canvas) return;
264
+ disconnectCanvasObservers();
265
+ observedCanvas = canvas;
266
+ discoveryObserver?.disconnect();
267
+ discoveryObserver = undefined;
268
+
269
+ attributeObserver = new MutationObserver((records) =>
270
+ processAttributeRecords(canvas, records),
271
+ );
272
+ attributeObserver.observe(canvas, {
273
+ attributeFilter: ["height", "width"],
274
+ attributeOldValue: true,
275
+ attributes: true,
276
+ });
277
+
278
+ resizeObserver = new ResizeObserver(() => sendCanvas("resize", canvas));
279
+ resizeObserver.observe(canvas);
280
+
281
+ visibleSizeObserver = new MutationObserver(() =>
282
+ sendCanvas("visible-size", canvas),
283
+ );
284
+ const visibleSizeTargets = new Set<Element>([canvas]);
285
+ let ancestor = canvas.parentElement;
286
+ while (ancestor) {
287
+ visibleSizeTargets.add(ancestor);
288
+ if (ancestor === document.documentElement) break;
289
+ ancestor = ancestor.parentElement;
290
+ }
291
+ for (const target of visibleSizeTargets) {
292
+ visibleSizeObserver.observe(target, {
293
+ attributeFilter: ["class", "style"],
294
+ attributes: true,
295
+ });
296
+ }
297
+
298
+ const parent = canvas.parentNode;
299
+ if (parent) {
300
+ replacementObserver = new MutationObserver(() => {
301
+ const replacement = findVisibleCanvas();
302
+ if (replacement && replacement !== observedCanvas) {
303
+ observeCanvas(replacement);
304
+ }
305
+ });
306
+ replacementObserver.observe(parent, { childList: true });
307
+ }
308
+ sendCanvas("installed", canvas);
309
+ };
310
+
311
+ const discoverCanvas = () => {
312
+ const canvas = findVisibleCanvas();
313
+ if (canvas) {
314
+ observeCanvas(canvas);
315
+ return;
316
+ }
317
+ if (discoveryObserver) return;
318
+ discoveryObserver = new MutationObserver(() => {
319
+ const discovered = findVisibleCanvas();
320
+ if (discovered) observeCanvas(discovered);
321
+ });
322
+ discoveryObserver.observe(document, {
323
+ attributes: true,
324
+ childList: true,
325
+ subtree: true,
326
+ });
327
+ };
328
+
329
+ const state: BrowserState = {
330
+ begin: () => {
331
+ bufferedObservations.length = 0;
332
+ },
333
+ dispose: () => {
334
+ bufferedObservations.length = 0;
335
+ discoveryObserver?.disconnect();
336
+ discoveryObserver = undefined;
337
+ disconnectCanvasObservers();
338
+ },
339
+ flush: async () => {
340
+ while (bufferedObservations.length > 0) {
341
+ const batch = bufferedObservations.splice(
342
+ 0,
343
+ bufferedObservations.length,
344
+ );
345
+ await Promise.all(batch.map((observation) => binding(observation)));
346
+ }
347
+ },
348
+ };
349
+ Reflect.set(globalThis, stateKey, state);
350
+ discoverCanvas();
351
+ }
@@ -29,8 +29,8 @@ export async function expectToolcraftDiscreteSliderMarkers(
29
29
  ).toBeVisible();
30
30
  expect(
31
31
  await slider.locator('[data-slot="slider-marker"]').count(),
32
- `Toolcraft discrete slider "${target}" should render multiple markers.`,
33
- ).toBeGreaterThan(1);
32
+ `Toolcraft discrete slider "${target}" should render at least one interior marker.`,
33
+ ).toBeGreaterThan(0);
34
34
 
35
35
  await attachToolcraftBrowserRuntimeEvidence({
36
36
  evidenceType: "discrete-slider-layout",
@@ -5,6 +5,10 @@ import type { ToolcraftPerformancePath } from "@repo/toolcraft-runtime";
5
5
  import type { ToolcraftPerformancePipelinePhase } from "../src/app/test-evidence/browser-performance-contract";
6
6
  import type { ToolcraftCompiledFixtureApplications } from "./performance-compiled-fixture-runtime";
7
7
 
8
+ export type ToolcraftPerformanceCanvasBacking = Readonly<{
9
+ canvasSelector: string;
10
+ }>;
11
+
8
12
  export type ToolcraftPerformancePathActionContext = Readonly<{
9
13
  page: Page;
10
14
  path: ToolcraftPerformancePath;
@@ -20,10 +24,6 @@ type ToolcraftPerformancePathAdapterBase = Readonly<{
20
24
  ) => Promise<unknown> | unknown;
21
25
  pathId: string;
22
26
  prepare: (page: Page) => Promise<void>;
23
- renderScaleBacking?: Readonly<{
24
- canvasSelector: string;
25
- selectedScale: number;
26
- }>;
27
27
  verifyOutcome?: (
28
28
  context: ToolcraftPerformancePathActionContext,
29
29
  ) => Promise<void> | void;