@pixel-point/toolcraft 0.0.8 → 0.0.11
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.
- package/README.md +42 -9
- package/package.json +1 -1
- package/src/generate.mjs +42 -5
- package/src/generate.test.mjs +40 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +251 -47
- package/templates/runtime/contracts/component-contracts.ts +130 -57
- package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
- package/templates/runtime/contracts/decision-contracts.ts +22 -9
- package/templates/runtime/export/export.test.ts +63 -0
- package/templates/runtime/export/export.ts +55 -0
- package/templates/runtime/index.ts +1 -0
- package/templates/runtime/react/canvas-shell.test.tsx +58 -4
- package/templates/runtime/react/canvas-shell.tsx +31 -9
- package/templates/runtime/react/controls-panel.test.tsx +916 -108
- package/templates/runtime/react/controls-panel.tsx +136 -30
- package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
- package/templates/runtime/react/settings-transfer.test.ts +4 -0
- package/templates/runtime/react/settings-transfer.ts +6 -1
- package/templates/runtime/react/timeline-panel.test.tsx +14 -0
- package/templates/runtime/react/timeline-panel.tsx +44 -7
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
- package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
- package/templates/runtime/react/toolcraft-app.tsx +56 -37
- package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
- package/templates/runtime/schema/define-toolcraft.ts +140 -246
- package/templates/runtime/schema/runtime-targets.ts +21 -0
- package/templates/runtime/schema/types.ts +44 -0
- package/templates/runtime/state/create-template-state.test.ts +156 -0
- package/templates/runtime/state/create-template-state.ts +38 -8
- package/templates/runtime/state/media-defaults.ts +105 -0
- package/templates/runtime/state/persistence.test.ts +58 -0
- package/templates/runtime/state/persistence.ts +105 -1
- package/templates/runtime/state/reducer.test.ts +280 -4
- package/templates/runtime/state/reducer.ts +195 -9
- package/templates/runtime/state/timeline-loop.test.ts +71 -0
- package/templates/runtime/state/timeline-loop.ts +35 -0
- package/templates/runtime/state/types.ts +27 -0
- package/templates/runtime/testing/performance.test.ts +810 -21
- package/templates/runtime/testing/performance.ts +823 -53
- package/templates/starter/AGENTS.md +24 -18
- package/templates/starter/docs/toolcraft/README.md +8 -4
- package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
- package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
- package/templates/starter/docs/toolcraft/component-rules.md +75 -42
- package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
- package/templates/starter/docs/toolcraft/performance.md +37 -11
- package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
- package/templates/starter/docs/toolcraft/workflow.md +5 -2
- package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
- package/templates/starter/e2e/app-performance.spec.ts +55 -2
- package/templates/starter/e2e/performance-helpers.ts +45 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/index.html +1 -0
- package/templates/starter/package.json +3 -0
- package/templates/starter/playwright.config.ts +1 -1
- package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
- package/templates/starter/scripts/toolcraft-port.mjs +280 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
- package/templates/starter/src/app/starter-acceptance.ts +1453 -97
- package/templates/starter/src/app/starter-performance.test.ts +111 -7
- package/templates/starter/src/app/starter-performance.ts +5 -0
- package/templates/starter/src/app/starter-schema.test.ts +32 -7
- package/templates/starter/src/app/starter-schema.ts +6 -2
- package/templates/starter/vite.config.ts +58 -2
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/palette-control.tsx +34 -4
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
- package/templates/ui/components/controls/file-drop/index.ts +6 -1
- package/templates/ui/components/controls/index.ts +4 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
- package/templates/ui/components/controls/select/select-control.tsx +8 -25
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
- package/templates/ui/components/panel/panel-actions.tsx +1 -1
- package/templates/ui/components/panel/panel-section.tsx +29 -5
|
@@ -12,7 +12,14 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
getToolcraftControlOrderTargets,
|
|
14
14
|
type ToolcraftComponentAcceptance,
|
|
15
|
+
type ToolcraftReferenceCoverage,
|
|
16
|
+
type ToolcraftReferenceFeatureInventoryItem,
|
|
17
|
+
type ToolcraftReferenceStudyEvidence,
|
|
18
|
+
type ToolcraftReferenceTimelineCoverage,
|
|
19
|
+
type ToolcraftTransferMode,
|
|
20
|
+
type ToolcraftVideoReferenceStudyEvidence,
|
|
15
21
|
starterAcceptance,
|
|
22
|
+
starterControlSectionInventory,
|
|
16
23
|
starterProductReadiness,
|
|
17
24
|
starterTransferMode,
|
|
18
25
|
validateToolcraftAcceptanceCoverage,
|
|
@@ -100,6 +107,7 @@ const requiredAgentWorklogDecisionTrailFields = [
|
|
|
100
107
|
"Task type",
|
|
101
108
|
"User-visible result",
|
|
102
109
|
"Source/reference checked",
|
|
110
|
+
"Reference inputs",
|
|
103
111
|
"Docs/contracts read",
|
|
104
112
|
"Contract rules applied",
|
|
105
113
|
"Decision",
|
|
@@ -110,6 +118,10 @@ const requiredAgentWorklogDecisionTrailFields = [
|
|
|
110
118
|
"Skipped checks",
|
|
111
119
|
"Risks",
|
|
112
120
|
] as const;
|
|
121
|
+
const worklogVideoReferencePattern =
|
|
122
|
+
/\b(?:source\/reference checked|source reviewed|reference inputs):[^\n]*(?:reference\s+(?:video|gif)|screen\s*recording|contact[-\s]*sheet|storyboard|frame[-\s]*by[-\s]*frame|extracted[-\s]*frames?|cleanshot|ffprobe|[^\s]+\.(?:mp4|mov|webm|gif))\b/i;
|
|
123
|
+
const worklogVideoReferenceStudyPattern =
|
|
124
|
+
/\b(video reference study|storyboard|frame[-\s]*by[-\s]*frame|transition analysis|frame[-\s]*to[-\s]*frame|contact[-\s]*sheet|extracted frames?)\b/i;
|
|
113
125
|
|
|
114
126
|
const playbackTimelineAcceptance: ToolcraftComponentAcceptance = {
|
|
115
127
|
automated: true,
|
|
@@ -133,9 +145,133 @@ const playbackTimelineAcceptance: ToolcraftComponentAcceptance = {
|
|
|
133
145
|
"rendered-frame",
|
|
134
146
|
],
|
|
135
147
|
userAction:
|
|
136
|
-
"Edit timeline duration,
|
|
148
|
+
"Edit timeline duration, verify the seamless forward-only loop still stitches first and last frames with no mirror, yoyo, ping-pong, or reverse motion, then scrub, pause, and resume playback from the timeline panel.",
|
|
137
149
|
};
|
|
138
150
|
|
|
151
|
+
const keyframesTimelineAcceptance: ToolcraftComponentAcceptance = {
|
|
152
|
+
automated: true,
|
|
153
|
+
automatedTestName: "timeline keyframes evaluate rendered output",
|
|
154
|
+
browser: true,
|
|
155
|
+
browserTestName: "browser: timeline keyframes evaluate rendered output",
|
|
156
|
+
componentType: "timeline",
|
|
157
|
+
evidence: "timeline-output",
|
|
158
|
+
expectedObservable:
|
|
159
|
+
"Keyframed values create editable rows and change rendered output at different timeline times.",
|
|
160
|
+
fixture: "keyframes timeline fixture",
|
|
161
|
+
id: "timeline.keyframes",
|
|
162
|
+
kind: "runtime",
|
|
163
|
+
target: "timeline.keyframes",
|
|
164
|
+
timelineCoverage: "keyframes",
|
|
165
|
+
userAction: "Create a keyframe, edit its value, and scrub the timeline.",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const productDerivedEightSecondLoop = {
|
|
169
|
+
evidence:
|
|
170
|
+
"The product timing fixture derives one complete forward animation cycle over 8s from the authored playback baseline.",
|
|
171
|
+
seconds: 8,
|
|
172
|
+
source: "product-derived" as const,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const referenceStudyEvidence = {
|
|
176
|
+
behaviorEvidence:
|
|
177
|
+
"Ran the original app in a local browser and checked canvas sizing, controls, and renderer state changes.",
|
|
178
|
+
referenceLocation: "/fixtures/legacy-badge-wall",
|
|
179
|
+
reproductionSteps:
|
|
180
|
+
"Installed dependencies, started the reference app, opened it in the browser, and compared behavior against the port.",
|
|
181
|
+
sourceEvidence:
|
|
182
|
+
"Inspected reference routes, renderer, control state, export handlers, and media lifecycle source files.",
|
|
183
|
+
status: "ran-original",
|
|
184
|
+
} satisfies ToolcraftReferenceStudyEvidence;
|
|
185
|
+
|
|
186
|
+
const videoReferenceMotionAcceptance: ToolcraftComponentAcceptance = {
|
|
187
|
+
automated: true,
|
|
188
|
+
automatedTestName: "video reference motion matches frame delta study",
|
|
189
|
+
browser: true,
|
|
190
|
+
browserTestName: "browser: video reference motion matches frame delta study",
|
|
191
|
+
componentType: "custom-renderer",
|
|
192
|
+
evidence: "product-output",
|
|
193
|
+
expectedObservable:
|
|
194
|
+
"The renderer preserves the video reference behavior observed across the storyboard transitions.",
|
|
195
|
+
fixture: "video reference storyboard fixture",
|
|
196
|
+
id: "reference.video.motion",
|
|
197
|
+
kind: "runtime",
|
|
198
|
+
target: "reference.video.motion",
|
|
199
|
+
userAction:
|
|
200
|
+
"Compare the implemented renderer against the storyboard frames and frame-to-frame behavior deltas.",
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const videoReferenceStudyEvidence = {
|
|
204
|
+
acceptanceMapping: [
|
|
205
|
+
{
|
|
206
|
+
acceptanceId: "reference.video.motion",
|
|
207
|
+
behavior: "Planted contact points stretch across adjacent frames before retargeting.",
|
|
208
|
+
frameIds: ["f000", "f012", "f024"],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
behaviorDecomposition:
|
|
212
|
+
"The video reference is decomposed into body movement, persistent contact points, delayed release, and retargeted anchors. The implementation copies those behaviors rather than a single static frame.",
|
|
213
|
+
extractionEvidence:
|
|
214
|
+
"Extracted frames with ffmpeg and reviewed contact sheets before implementation.",
|
|
215
|
+
referenceLocation: "/fixtures/reference-motion/ref.mp4",
|
|
216
|
+
storyboard: [
|
|
217
|
+
{
|
|
218
|
+
behaviorObservation:
|
|
219
|
+
"Body starts moving while several endpoints remain planted in canvas space.",
|
|
220
|
+
frameId: "f000",
|
|
221
|
+
frameSource: "frames/frame_000.png",
|
|
222
|
+
timeSeconds: 0,
|
|
223
|
+
visualObservation: "The body is near the left side with legs extended outward.",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
behaviorObservation:
|
|
227
|
+
"Body advances and planted endpoints stretch instead of following immediately.",
|
|
228
|
+
frameId: "f012",
|
|
229
|
+
frameSource: "frames/frame_012.png",
|
|
230
|
+
timeSeconds: 0.2,
|
|
231
|
+
visualObservation: "Several endpoints stay near the earlier canvas positions.",
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
behaviorObservation:
|
|
235
|
+
"Over-extended legs begin to release and choose new anchors.",
|
|
236
|
+
frameId: "f024",
|
|
237
|
+
frameSource: "frames/frame_024.png",
|
|
238
|
+
timeSeconds: 0.4,
|
|
239
|
+
visualObservation: "One side shows a retargeted leg group closer to the body.",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
behaviorObservation:
|
|
243
|
+
"The new anchor pattern stabilizes before the next release.",
|
|
244
|
+
frameId: "f036",
|
|
245
|
+
frameSource: "frames/frame_036.png",
|
|
246
|
+
timeSeconds: 0.6,
|
|
247
|
+
visualObservation: "Legs settle into a new spread with the body further right.",
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
transitionAnalysis: [
|
|
251
|
+
{
|
|
252
|
+
behaviorDelta:
|
|
253
|
+
"Between f000 and f012, body position changes faster than several leg endpoints, proving contact memory.",
|
|
254
|
+
fromFrameId: "f000",
|
|
255
|
+
id: "f000-f012",
|
|
256
|
+
toFrameId: "f012",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
behaviorDelta:
|
|
260
|
+
"Between f012 and f024, over-extension triggers release and retargeting instead of continuous sine motion.",
|
|
261
|
+
fromFrameId: "f012",
|
|
262
|
+
id: "f012-f024",
|
|
263
|
+
toFrameId: "f024",
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
behaviorDelta:
|
|
267
|
+
"Between f024 and f036, newly planted anchors remain stable while the body keeps moving.",
|
|
268
|
+
fromFrameId: "f024",
|
|
269
|
+
id: "f024-f036",
|
|
270
|
+
toFrameId: "f036",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
} satisfies ToolcraftVideoReferenceStudyEvidence;
|
|
274
|
+
|
|
139
275
|
function makeControlAcceptance(
|
|
140
276
|
target: string,
|
|
141
277
|
componentType: string,
|
|
@@ -156,7 +292,91 @@ function makeControlAcceptance(
|
|
|
156
292
|
};
|
|
157
293
|
}
|
|
158
294
|
|
|
159
|
-
function
|
|
295
|
+
function makeReferenceFeatureInventory(
|
|
296
|
+
extra: readonly ToolcraftReferenceFeatureInventoryItem[] = [],
|
|
297
|
+
): readonly ToolcraftReferenceFeatureInventoryItem[] {
|
|
298
|
+
return [
|
|
299
|
+
{
|
|
300
|
+
acceptanceId: "reference.canvasSizing",
|
|
301
|
+
behaviorEvidence:
|
|
302
|
+
"Observed the reference output keep its authored dimensions in the browser.",
|
|
303
|
+
featureName: "Canvas sizing",
|
|
304
|
+
id: "canvas-sizing",
|
|
305
|
+
referenceBehavior: "The reference renderer owns the output dimensions.",
|
|
306
|
+
sourceEvidence: "Inspected reference renderer sizing and browser output.",
|
|
307
|
+
status: "ported",
|
|
308
|
+
toolcraftMapping:
|
|
309
|
+
"Toolcraft editable-output canvas starts from the reference dimensions and keeps runtime sizing visible.",
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
acceptanceId: "reference.controlMapping",
|
|
313
|
+
behaviorEvidence:
|
|
314
|
+
"Changed reference controls in the browser and observed renderer parameters update.",
|
|
315
|
+
featureName: "Control mapping",
|
|
316
|
+
id: "control-mapping",
|
|
317
|
+
referenceBehavior: "Reference controls update renderer parameters directly.",
|
|
318
|
+
sourceEvidence: "Inspected reference control state and parameter wiring.",
|
|
319
|
+
status: "ported",
|
|
320
|
+
toolcraftMapping:
|
|
321
|
+
"Toolcraft schema controls write runtime values consumed by the renderer.",
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
acceptanceId: "reference.rendererState",
|
|
325
|
+
behaviorEvidence:
|
|
326
|
+
"Observed reference renderer state persist across multiple animation frames.",
|
|
327
|
+
featureName: "Renderer state",
|
|
328
|
+
id: "renderer-state",
|
|
329
|
+
referenceBehavior: "Reference renderer mutable state persists across frames.",
|
|
330
|
+
sourceEvidence: "Inspected reference renderer lifecycle and frame updates.",
|
|
331
|
+
status: "ported",
|
|
332
|
+
toolcraftMapping:
|
|
333
|
+
"Toolcraft renderer keeps equivalent mutable state and invalidation lifecycle.",
|
|
334
|
+
},
|
|
335
|
+
...extra,
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function makeReferenceCoverageAcceptance(
|
|
340
|
+
id: string,
|
|
341
|
+
referenceCoverage: ToolcraftReferenceCoverage,
|
|
342
|
+
): ToolcraftComponentAcceptance {
|
|
343
|
+
return {
|
|
344
|
+
automated: true,
|
|
345
|
+
automatedTestName: `${id} preserves reference behavior`,
|
|
346
|
+
browser: true,
|
|
347
|
+
browserTestName: `browser: ${id} preserves reference behavior`,
|
|
348
|
+
componentType: "custom-renderer",
|
|
349
|
+
evidence: "product-output",
|
|
350
|
+
expectedObservable: `${id} preserves the reference behavior in Toolcraft output.`,
|
|
351
|
+
fixture: `${id} fixture`,
|
|
352
|
+
id,
|
|
353
|
+
kind: "runtime",
|
|
354
|
+
referenceCoverage,
|
|
355
|
+
userAction: `Exercise ${id}.`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function makeReferenceTimelineCoverageAcceptance(
|
|
360
|
+
id: string,
|
|
361
|
+
referenceTimelineCoverage: ToolcraftReferenceTimelineCoverage,
|
|
362
|
+
): ToolcraftComponentAcceptance {
|
|
363
|
+
return {
|
|
364
|
+
automated: true,
|
|
365
|
+
automatedTestName: `${id} preserves reference timeline behavior`,
|
|
366
|
+
browser: true,
|
|
367
|
+
browserTestName: `browser: ${id} preserves reference timeline behavior`,
|
|
368
|
+
componentType: "custom-timeline",
|
|
369
|
+
evidence: "timeline-output",
|
|
370
|
+
expectedObservable: `${id} preserves the reference timeline behavior.`,
|
|
371
|
+
fixture: `${id} fixture`,
|
|
372
|
+
id,
|
|
373
|
+
kind: "runtime",
|
|
374
|
+
referenceTimelineCoverage,
|
|
375
|
+
userAction: `Exercise ${id}.`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function createMandatorySetupSchema(settingsTransfer: false | "auto" = false) {
|
|
160
380
|
return defineToolcraft({
|
|
161
381
|
canvas: { enabled: true },
|
|
162
382
|
panels: {
|
|
@@ -185,13 +405,13 @@ function createSettingsTransferThresholdSchema(settingsTransfer: false | "auto"
|
|
|
185
405
|
});
|
|
186
406
|
}
|
|
187
407
|
|
|
188
|
-
function
|
|
408
|
+
function createMandatorySetupAcceptance() {
|
|
189
409
|
return Array.from({ length: 12 }, (_, index) =>
|
|
190
410
|
makeControlAcceptance(`settings.control${index}`, "slider"),
|
|
191
411
|
);
|
|
192
412
|
}
|
|
193
413
|
|
|
194
|
-
function
|
|
414
|
+
function createMandatorySetupWithCanvasSizeSchema() {
|
|
195
415
|
return defineToolcraft({
|
|
196
416
|
canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 } },
|
|
197
417
|
panels: {
|
|
@@ -213,14 +433,14 @@ function createSettingsTransferBelowThresholdWithCanvasSizeSchema() {
|
|
|
213
433
|
title: "Transform",
|
|
214
434
|
},
|
|
215
435
|
],
|
|
216
|
-
title: "
|
|
436
|
+
title: "Runtime Setup Settings",
|
|
217
437
|
},
|
|
218
438
|
},
|
|
219
439
|
settingsTransfer: false,
|
|
220
440
|
});
|
|
221
441
|
}
|
|
222
442
|
|
|
223
|
-
function
|
|
443
|
+
function createMandatorySetupWithCanvasSizeAcceptance() {
|
|
224
444
|
return [
|
|
225
445
|
makeControlAcceptance("canvas.size.width", "text"),
|
|
226
446
|
makeControlAcceptance("canvas.size.height", "text"),
|
|
@@ -266,7 +486,7 @@ function stripJsComments(source: string): string {
|
|
|
266
486
|
}
|
|
267
487
|
|
|
268
488
|
function getMarkdownHeadingLevel(line: string): number | null {
|
|
269
|
-
const match = /^(#{1,6})\s+\S/.exec(line);
|
|
489
|
+
const match = /^(#{1,6})\s+\S/.exec(line.trim());
|
|
270
490
|
|
|
271
491
|
return match ? match[1].length : null;
|
|
272
492
|
}
|
|
@@ -332,6 +552,104 @@ function escapeRegExp(value: string): string {
|
|
|
332
552
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
333
553
|
}
|
|
334
554
|
|
|
555
|
+
const incompleteRequiredCheckPattern =
|
|
556
|
+
/\b(?:fail(?:ed|s|ing)?|not complete|not completed|incomplete|pending|blocked|planned|todo|to do|to run|will run|not run|not started|not yet|queued)\b/i;
|
|
557
|
+
const requiredCheckNamePattern =
|
|
558
|
+
/\b(?:(?:pnpm\s+)?(?:verify:final|verify:perf|test:browser:perf|test:browser|verify:quick|test|build)|browser performance checkpoint|agent-browser|playwright-fallback)\b/i;
|
|
559
|
+
const browserPerformanceCheckpointPattern =
|
|
560
|
+
/\b(?:agent-browser|browser performance checkpoint|performance checkpoint|verify:perf|test:browser:perf|playwright-fallback)\b/i;
|
|
561
|
+
const fallbackPerfReasonPattern =
|
|
562
|
+
/\b(?:agent[- ]browser|agent[- ]controlled browser).*\b(?:unavailable|not available|missing|absent)|\bagent-browser-unavailable\b|\b(?:no|without)\s+(?:agent[- ]browser|agent[- ]controlled browser)\b|\b(?:CI|non-agent automation|non-agent run)\b/i;
|
|
563
|
+
|
|
564
|
+
function getWorklogLines(source: string): string[] {
|
|
565
|
+
return source
|
|
566
|
+
.split(/\r?\n/)
|
|
567
|
+
.map((line) => line.trim())
|
|
568
|
+
.filter(Boolean);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function isAllowedPerfSkipLine(line: string): boolean {
|
|
572
|
+
return (
|
|
573
|
+
browserPerformanceCheckpointPattern.test(line) &&
|
|
574
|
+
/\bnot required\b/i.test(line) &&
|
|
575
|
+
/\b(?:post-first|after first|not first|feature loop|non-performance)\b/i.test(line)
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function isIncompleteOrSkippedLine(line: string): boolean {
|
|
580
|
+
return (
|
|
581
|
+
/\b(?:skip|skipped|not required)\b/i.test(line) ||
|
|
582
|
+
incompleteRequiredCheckPattern.test(line)
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function isVerificationEvidenceLine(line: string): boolean {
|
|
587
|
+
return /\b(?:Run|Verification|Browser):/i.test(line);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function isPassedAgentBrowserPerfRunLine(line: string): boolean {
|
|
591
|
+
return (
|
|
592
|
+
isVerificationEvidenceLine(line) &&
|
|
593
|
+
/\bagent-browser\b/i.test(line) &&
|
|
594
|
+
/\b(?:performance|perf|checkpoint)\b/i.test(line) &&
|
|
595
|
+
!isIncompleteOrSkippedLine(line)
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function isPassedPlaywrightFallbackPerfRunLine(line: string): boolean {
|
|
600
|
+
return (
|
|
601
|
+
isVerificationEvidenceLine(line) &&
|
|
602
|
+
(/\bpnpm\s+verify:perf\b/i.test(line) ||
|
|
603
|
+
(/\bplaywright-fallback\b/i.test(line) &&
|
|
604
|
+
/\b(?:performance|perf|checkpoint)\b/i.test(line))) &&
|
|
605
|
+
!isIncompleteOrSkippedLine(line)
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function getAgentWorklogVerificationGateErrors(source: string): string[] {
|
|
610
|
+
const lines = getWorklogLines(source);
|
|
611
|
+
const errors: string[] = [];
|
|
612
|
+
const hasPassedAgentBrowserPerfRun = lines.some(isPassedAgentBrowserPerfRunLine);
|
|
613
|
+
const hasPassedPlaywrightFallbackPerfRun = lines.some(
|
|
614
|
+
isPassedPlaywrightFallbackPerfRunLine,
|
|
615
|
+
);
|
|
616
|
+
const hasFallbackPerfReason = lines.some((line) => fallbackPerfReasonPattern.test(line));
|
|
617
|
+
const hasPassedPerfRun =
|
|
618
|
+
hasPassedAgentBrowserPerfRun ||
|
|
619
|
+
(hasPassedPlaywrightFallbackPerfRun && hasFallbackPerfReason);
|
|
620
|
+
const hasAllowedPerfSkip = lines.some(isAllowedPerfSkipLine);
|
|
621
|
+
const failedRequiredCheckLine = lines.find(
|
|
622
|
+
(line) => requiredCheckNamePattern.test(line) && incompleteRequiredCheckPattern.test(line),
|
|
623
|
+
);
|
|
624
|
+
const skippedRequiredCheckLine = lines.find(
|
|
625
|
+
(line) =>
|
|
626
|
+
/^-?\s*(?:Skipped checks|Skip):/i.test(line) &&
|
|
627
|
+
requiredCheckNamePattern.test(line) &&
|
|
628
|
+
!/\bnone\b/i.test(line) &&
|
|
629
|
+
!isAllowedPerfSkipLine(line),
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
if (failedRequiredCheckLine) {
|
|
633
|
+
errors.push(
|
|
634
|
+
"agent-worklog.md required checks must be passed before final delivery; do not report failed, incomplete, pending, or blocked verification.",
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (skippedRequiredCheckLine) {
|
|
639
|
+
errors.push(
|
|
640
|
+
"agent-worklog.md must not list required checks as skipped unless they are explicitly not required for a post-first-working non-performance edit.",
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!hasPassedPerfRun && !hasAllowedPerfSkip) {
|
|
645
|
+
errors.push(
|
|
646
|
+
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or `pnpm verify:perf` with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return errors;
|
|
651
|
+
}
|
|
652
|
+
|
|
335
653
|
function getAgentWorklogValidationErrors(source: string): string[] {
|
|
336
654
|
const errors: string[] = [];
|
|
337
655
|
|
|
@@ -394,10 +712,21 @@ function getAgentWorklogValidationErrors(source: string): string[] {
|
|
|
394
712
|
errors.push("agent-worklog.md Evidence must name reviewed files, references, or contract rules.");
|
|
395
713
|
}
|
|
396
714
|
|
|
715
|
+
if (
|
|
716
|
+
worklogVideoReferencePattern.test(source) &&
|
|
717
|
+
!worklogVideoReferenceStudyPattern.test(source)
|
|
718
|
+
) {
|
|
719
|
+
errors.push(
|
|
720
|
+
"agent-worklog.md cites a video reference, screen recording, GIF, extracted frames, or contact sheet; record a Video Reference Study with storyboard frames and frame-to-frame transition analysis.",
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
397
724
|
if (!/\bpnpm\s+(verify|test|build|typecheck)|browser|Playwright|perf/i.test(verificationBody)) {
|
|
398
725
|
errors.push("agent-worklog.md Verification must list concrete test/build/browser/performance checks.");
|
|
399
726
|
}
|
|
400
727
|
|
|
728
|
+
errors.push(...getAgentWorklogVerificationGateErrors(source));
|
|
729
|
+
|
|
401
730
|
if (!/\b(Risk|None):\s*\S/i.test(risksBody)) {
|
|
402
731
|
errors.push('agent-worklog.md Risks must include either "Risk:" entries or "None:" with a reason.');
|
|
403
732
|
}
|
|
@@ -454,7 +783,7 @@ function sourceDefinesProductCanvasContent(): boolean {
|
|
|
454
783
|
function schemaHasProductSurface(): boolean {
|
|
455
784
|
return (
|
|
456
785
|
(starterSchema.panels.controls?.sections ?? []).some(
|
|
457
|
-
(section) => Object.keys(section.controls).length > 0,
|
|
786
|
+
(section) => section.title !== "Setup" && Object.keys(section.controls).length > 0,
|
|
458
787
|
) ||
|
|
459
788
|
starterSchema.panels.layers === true ||
|
|
460
789
|
starterSchema.panels.timeline?.enabled === true ||
|
|
@@ -676,12 +1005,93 @@ function sourceHasVideoDurationMetadataCoverage(source: string): boolean {
|
|
|
676
1005
|
);
|
|
677
1006
|
}
|
|
678
1007
|
|
|
1008
|
+
function sourceHasVideoDimensionMetadataCoverage(source: string): boolean {
|
|
1009
|
+
const hasVideoMetadataReader =
|
|
1010
|
+
/\bdocument\.createElement\s*\(\s*["']video["']\s*\)/.test(source) &&
|
|
1011
|
+
/\bURL\.createObjectURL\b/.test(source) &&
|
|
1012
|
+
/\bloadedmetadata\b|\bonloadedmetadata\b/.test(source) &&
|
|
1013
|
+
/\bvideo\.videoWidth\b/.test(source) &&
|
|
1014
|
+
/\bvideo\.videoHeight\b/.test(source);
|
|
1015
|
+
const hasResolutionCoverage =
|
|
1016
|
+
/\b(?:export\.video\.resolution|video resolution|resolution)\b/i.test(source) &&
|
|
1017
|
+
/\bcurrent\b/i.test(source) &&
|
|
1018
|
+
/\b4k\b/i.test(source);
|
|
1019
|
+
const has4kDimensionExpectation =
|
|
1020
|
+
/\b(?:3840|2160)\b/.test(source) || /\bgetToolcraftVideoExportSize\b/.test(source);
|
|
1021
|
+
|
|
1022
|
+
return hasVideoMetadataReader && hasResolutionCoverage && has4kDimensionExpectation;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
679
1025
|
function sourceHasCustomMovOrProResEncoder(source: string): boolean {
|
|
680
1026
|
return /\bVideoEncoder\b|\bffmpeg\b|\bFFmpeg\b|\bProRes\b|\bprores\b|\btranscoder\b/i.test(
|
|
681
1027
|
source,
|
|
682
1028
|
);
|
|
683
1029
|
}
|
|
684
1030
|
|
|
1031
|
+
function sourceUsesVideoExportSizeHelper(source: string): boolean {
|
|
1032
|
+
return /\bgetToolcraftVideoExportSize\b/.test(source);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function sourceHasUnsafeVideoLongEdgeSizing(source: string): boolean {
|
|
1036
|
+
return (
|
|
1037
|
+
/\b(?:export\.video\.resolution|videoResolution|VideoResolution)\b[\s\S]{0,500}\b4096\b/i.test(
|
|
1038
|
+
source,
|
|
1039
|
+
) ||
|
|
1040
|
+
/\b4096\b[\s\S]{0,500}\b(?:export\.video\.resolution|videoResolution|VideoResolution)\b/i.test(
|
|
1041
|
+
source,
|
|
1042
|
+
)
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function sourceHasCaptureStreamBeforeCanvasSizing(source: string): boolean {
|
|
1047
|
+
const captureStreamMatches = Array.from(
|
|
1048
|
+
source.matchAll(/\b([A-Za-z_$][A-Za-z0-9_$]*)\.captureStream\s*\(/g),
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
if (captureStreamMatches.length === 0) {
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
return captureStreamMatches.some((match) => {
|
|
1056
|
+
const canvasName = match[1];
|
|
1057
|
+
const captureStreamIndex = match.index ?? 0;
|
|
1058
|
+
const beforeCaptureStream = source.slice(
|
|
1059
|
+
Math.max(0, captureStreamIndex - 2000),
|
|
1060
|
+
captureStreamIndex,
|
|
1061
|
+
);
|
|
1062
|
+
const escapedCanvasName = escapeRegExp(canvasName);
|
|
1063
|
+
|
|
1064
|
+
return (
|
|
1065
|
+
!new RegExp(`\\b${escapedCanvasName}\\.width\\s*=`).test(beforeCaptureStream) ||
|
|
1066
|
+
!new RegExp(`\\b${escapedCanvasName}\\.height\\s*=`).test(beforeCaptureStream)
|
|
1067
|
+
);
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function sourceHandlesVideoRecorderOrEncoderErrors(source: string): boolean {
|
|
1072
|
+
const usesMediaRecorder = /\bnew\s+MediaRecorder\b/.test(source);
|
|
1073
|
+
const usesVideoEncoder = /\bnew\s+VideoEncoder\b/.test(source);
|
|
1074
|
+
const mediaRecorderRejectsErrors =
|
|
1075
|
+
/\.\s*onerror\s*=[\s\S]{0,700}\b(?:reject|throw|Promise\.reject)\b/.test(source) ||
|
|
1076
|
+
/\.addEventListener\s*\(\s*["']error["'][\s\S]{0,900}\b(?:reject|throw|Promise\.reject)\b/.test(
|
|
1077
|
+
source,
|
|
1078
|
+
);
|
|
1079
|
+
const videoEncoderRejectsErrors =
|
|
1080
|
+
/\bnew\s+VideoEncoder\s*\(\s*\{[\s\S]{0,1200}\berror\s*:[\s\S]{0,700}\b(?:reject|throw|Promise\.reject)\b/.test(
|
|
1081
|
+
source,
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
if (usesMediaRecorder && !mediaRecorderRejectsErrors) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (usesVideoEncoder && !videoEncoderRejectsErrors) {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
685
1095
|
function sourceHasImageExportDimensionCoverage(source: string): boolean {
|
|
686
1096
|
const hasImageDecoder =
|
|
687
1097
|
/\bcreateImageBitmap\b/.test(source) ||
|
|
@@ -806,7 +1216,40 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
806
1216
|
expect(validateToolcraftAcceptanceCoverage(starterSchema, starterAcceptance)).toEqual([]);
|
|
807
1217
|
});
|
|
808
1218
|
|
|
809
|
-
it("
|
|
1219
|
+
it("rejects generated apps without the mandatory runtime setup controls panel", () => {
|
|
1220
|
+
const schema = defineToolcraft({
|
|
1221
|
+
canvas: { enabled: true },
|
|
1222
|
+
panels: {},
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
expect(validateToolcraftAcceptanceCoverage(schema, [])).toEqual(
|
|
1226
|
+
expect.arrayContaining([
|
|
1227
|
+
"Generated Toolcraft apps must define a controls panel so the mandatory runtime Setup section is visible.",
|
|
1228
|
+
]),
|
|
1229
|
+
);
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
it("requires generated product apps to publish a control section inventory", () => {
|
|
1233
|
+
if (!schemaHasProductSurface()) {
|
|
1234
|
+
expect(starterControlSectionInventory).toEqual([]);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
expect(
|
|
1239
|
+
starterControlSectionInventory.length,
|
|
1240
|
+
"Product apps must export starterControlSectionInventory so section grouping decisions are machine-checkable.",
|
|
1241
|
+
).toBeGreaterThan(0);
|
|
1242
|
+
expect(
|
|
1243
|
+
validateToolcraftAcceptanceCoverage(
|
|
1244
|
+
starterSchema,
|
|
1245
|
+
starterAcceptance,
|
|
1246
|
+
starterTransferMode,
|
|
1247
|
+
starterControlSectionInventory,
|
|
1248
|
+
),
|
|
1249
|
+
).toEqual([]);
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it("requires fileDrop acceptance to prove upload, clear, reset, and image transforms", () => {
|
|
810
1253
|
const fileDropSchema = defineToolcraft({
|
|
811
1254
|
canvas: { enabled: true },
|
|
812
1255
|
panels: {
|
|
@@ -849,12 +1292,69 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
849
1292
|
]),
|
|
850
1293
|
).toEqual(
|
|
851
1294
|
expect.arrayContaining([
|
|
852
|
-
"Source / source (media.source) fileDrop acceptance must prove upload/import, clear/remove, and section or global reset remove source media.",
|
|
1295
|
+
"Source / source (media.source) fileDrop acceptance must prove upload/import, clear/remove, and section or global reset restore default source media or remove uploaded source media when no default exists.",
|
|
1296
|
+
"Source / source (media.source) image fileDrop acceptance must prove rotate and flip actions update runtime media transform metadata and that preview, renderer, or export consumes the transform.",
|
|
1297
|
+
]),
|
|
1298
|
+
);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
it("requires predefined fileDrop media acceptance to prove attached default restore", () => {
|
|
1302
|
+
const fileDropSchema = defineToolcraft({
|
|
1303
|
+
canvas: { enabled: true, upload: true },
|
|
1304
|
+
media: {
|
|
1305
|
+
defaultAssets: [
|
|
1306
|
+
{
|
|
1307
|
+
dataUrl: "data:image/png;base64,AAAA",
|
|
1308
|
+
fileName: "default-source.png",
|
|
1309
|
+
sourceTarget: "media.source",
|
|
1310
|
+
},
|
|
1311
|
+
],
|
|
1312
|
+
},
|
|
1313
|
+
panels: {
|
|
1314
|
+
controls: {
|
|
1315
|
+
sections: [
|
|
1316
|
+
{
|
|
1317
|
+
controls: {
|
|
1318
|
+
source: {
|
|
1319
|
+
defaultValue: null,
|
|
1320
|
+
label: "Source image",
|
|
1321
|
+
target: "media.source",
|
|
1322
|
+
type: "fileDrop",
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1325
|
+
title: "Source",
|
|
1326
|
+
},
|
|
1327
|
+
],
|
|
1328
|
+
title: "Controls",
|
|
1329
|
+
},
|
|
1330
|
+
},
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
expect(
|
|
1334
|
+
validateToolcraftAcceptanceCoverage(fileDropSchema, [
|
|
1335
|
+
{
|
|
1336
|
+
automated: true,
|
|
1337
|
+
automatedTestName: "source image upload reset lifecycle",
|
|
1338
|
+
browser: true,
|
|
1339
|
+
browserTestName: "browser: source image upload reset lifecycle",
|
|
1340
|
+
componentType: "fileDrop",
|
|
1341
|
+
evidence: "media-lifecycle",
|
|
1342
|
+
expectedObservable:
|
|
1343
|
+
"Upload, clear, reset, rotate, and flip update the source preview and renderer.",
|
|
1344
|
+
fixture: "source image fixture",
|
|
1345
|
+
id: "media.source",
|
|
1346
|
+
kind: "control",
|
|
1347
|
+
target: "media.source",
|
|
1348
|
+
userAction:
|
|
1349
|
+
"Upload a source image, clear it, use Reset controls, rotate 90°, flip horizontal, and verify runtime media transform metadata is consumed by preview.",
|
|
1350
|
+
},
|
|
853
1351
|
]),
|
|
1352
|
+
).toContain(
|
|
1353
|
+
"Source / source (media.source) fileDrop acceptance must prove predefined media.defaultAssets render as attached files, can be removed to an empty source/canvas state, and are restored by section or global Reset.",
|
|
854
1354
|
);
|
|
855
1355
|
});
|
|
856
1356
|
|
|
857
|
-
it("accepts fileDrop lifecycle coverage that includes reset", () => {
|
|
1357
|
+
it("accepts fileDrop lifecycle coverage that includes reset and image transforms", () => {
|
|
858
1358
|
const fileDropSchema = defineToolcraft({
|
|
859
1359
|
canvas: { enabled: true },
|
|
860
1360
|
panels: {
|
|
@@ -886,13 +1386,13 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
886
1386
|
componentType: "fileDrop",
|
|
887
1387
|
evidence: "media-lifecycle",
|
|
888
1388
|
expectedObservable:
|
|
889
|
-
"Uploading a source image changes the media preview; Clear, section reset, and global Reset controls remove the source media and restore the empty upload state.",
|
|
1389
|
+
"Uploading a source image changes the media preview; rotate and flip actions update runtime media transform metadata and rendered output; Clear, section reset, and global Reset controls remove the source media and restore the empty upload state.",
|
|
890
1390
|
fixture: "source image fixture",
|
|
891
1391
|
id: "media.source",
|
|
892
1392
|
kind: "control",
|
|
893
1393
|
target: "media.source",
|
|
894
1394
|
userAction:
|
|
895
|
-
"Upload a source image, clear it, upload again, then use section reset and Reset controls.",
|
|
1395
|
+
"Upload a source image, rotate it, flip it, clear it, upload again, then use section reset and Reset controls.",
|
|
896
1396
|
},
|
|
897
1397
|
]);
|
|
898
1398
|
|
|
@@ -901,6 +1401,77 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
901
1401
|
).toEqual([]);
|
|
902
1402
|
});
|
|
903
1403
|
|
|
1404
|
+
it("requires multiple fileDrop acceptance to prove runtime media reorder", () => {
|
|
1405
|
+
const fileDropSchema = defineToolcraft({
|
|
1406
|
+
canvas: { enabled: true },
|
|
1407
|
+
panels: {
|
|
1408
|
+
controls: {
|
|
1409
|
+
sections: [
|
|
1410
|
+
{
|
|
1411
|
+
controls: {
|
|
1412
|
+
sources: {
|
|
1413
|
+
defaultValue: [],
|
|
1414
|
+
label: "Source images",
|
|
1415
|
+
multiple: true,
|
|
1416
|
+
target: "media.sources",
|
|
1417
|
+
type: "fileDrop",
|
|
1418
|
+
},
|
|
1419
|
+
},
|
|
1420
|
+
title: "Source",
|
|
1421
|
+
},
|
|
1422
|
+
],
|
|
1423
|
+
title: "Controls",
|
|
1424
|
+
},
|
|
1425
|
+
},
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
expect(
|
|
1429
|
+
validateToolcraftAcceptanceCoverage(fileDropSchema, [
|
|
1430
|
+
{
|
|
1431
|
+
automated: true,
|
|
1432
|
+
automatedTestName: "source images upload clear reset and transform",
|
|
1433
|
+
browser: true,
|
|
1434
|
+
browserTestName: "browser: source images upload clear reset and transform",
|
|
1435
|
+
componentType: "fileDrop",
|
|
1436
|
+
evidence: "media-lifecycle",
|
|
1437
|
+
expectedObservable:
|
|
1438
|
+
"Uploading source images changes the media preview; rotate and flip actions update runtime media transform metadata and rendered output; Clear and Reset controls remove media.",
|
|
1439
|
+
fixture: "source images fixture",
|
|
1440
|
+
id: "media.sources",
|
|
1441
|
+
kind: "control",
|
|
1442
|
+
target: "media.sources",
|
|
1443
|
+
userAction:
|
|
1444
|
+
"Upload two source images, rotate one, flip it, clear the images, and use Reset controls.",
|
|
1445
|
+
},
|
|
1446
|
+
]),
|
|
1447
|
+
).toEqual(
|
|
1448
|
+
expect.arrayContaining([
|
|
1449
|
+
"Source / sources (media.sources) multiple fileDrop acceptance must prove thumbnail/file reorder updates runtime media order and that preview, renderer, or export consumes that order.",
|
|
1450
|
+
]),
|
|
1451
|
+
);
|
|
1452
|
+
|
|
1453
|
+
expect(
|
|
1454
|
+
validateToolcraftAcceptanceCoverage(fileDropSchema, [
|
|
1455
|
+
{
|
|
1456
|
+
automated: true,
|
|
1457
|
+
automatedTestName: "source images upload reorder transform and reset",
|
|
1458
|
+
browser: true,
|
|
1459
|
+
browserTestName: "browser: source images upload reorder transform and reset",
|
|
1460
|
+
componentType: "fileDrop",
|
|
1461
|
+
evidence: "media-lifecycle",
|
|
1462
|
+
expectedObservable:
|
|
1463
|
+
"Uploading source images changes the preview; drag reorder updates runtime media order used by rendered output; rotate and flip update runtime media transform metadata used by export; Clear, section reset, and global Reset controls remove media.",
|
|
1464
|
+
fixture: "source images fixture",
|
|
1465
|
+
id: "media.sources",
|
|
1466
|
+
kind: "control",
|
|
1467
|
+
target: "media.sources",
|
|
1468
|
+
userAction:
|
|
1469
|
+
"Upload two source images, drag to reorder thumbnails, rotate and flip the selected image, clear them, then use section reset and Reset controls.",
|
|
1470
|
+
},
|
|
1471
|
+
]),
|
|
1472
|
+
).toEqual([]);
|
|
1473
|
+
});
|
|
1474
|
+
|
|
904
1475
|
it("rejects fake video duration browser coverage that falls back to expected duration", () => {
|
|
905
1476
|
const fakeCoverage = `
|
|
906
1477
|
async function getVideoDuration(page, buffer, mimeType) {
|
|
@@ -994,7 +1565,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
994
1565
|
|
|
995
1566
|
expect(validateToolcraftAcceptanceCoverage(fixedOutputSchema, starterAcceptance)).toEqual(
|
|
996
1567
|
expect.arrayContaining([
|
|
997
|
-
'canvas.sizing mode "fixed-output" requires a runtime acceptance entry with canvasSizingCoverage "fixed-output-size" explaining why width and height are intentionally non-editable.
|
|
1568
|
+
'canvas.sizing mode "fixed-output" requires a runtime acceptance entry with canvasSizingCoverage "fixed-output-size" explaining why width and height are intentionally non-editable. Product/output apps must use "editable-output"; user-provided, reference, fixed-format, or base/default sizes belong in canvas.size as editable initial values.',
|
|
998
1569
|
]),
|
|
999
1570
|
);
|
|
1000
1571
|
});
|
|
@@ -1047,14 +1618,184 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1047
1618
|
]),
|
|
1048
1619
|
).toEqual(
|
|
1049
1620
|
expect.arrayContaining([
|
|
1050
|
-
'canvas.sizing canvasSizingCoverage "fixed-output-size" must explain why the
|
|
1621
|
+
'canvas.sizing canvasSizingCoverage "fixed-output-size" must explain why the output dimensions are intentionally fixed for a non-product/internal fixture, not merely initialized from a default size.',
|
|
1051
1622
|
]),
|
|
1052
1623
|
);
|
|
1053
1624
|
});
|
|
1054
1625
|
|
|
1055
|
-
it("
|
|
1056
|
-
const
|
|
1057
|
-
canvas: {
|
|
1626
|
+
it("rejects fixed canvas sizing justified only by a missing reference size editor", () => {
|
|
1627
|
+
const fixedOutputSchema = defineToolcraft({
|
|
1628
|
+
canvas: {
|
|
1629
|
+
enabled: true,
|
|
1630
|
+
size: { height: 1080, unit: "px", width: 1920 },
|
|
1631
|
+
sizing: { mode: "fixed-output" },
|
|
1632
|
+
},
|
|
1633
|
+
panels: {
|
|
1634
|
+
controls: {
|
|
1635
|
+
sections: [
|
|
1636
|
+
{
|
|
1637
|
+
controls: {
|
|
1638
|
+
prompt: {
|
|
1639
|
+
defaultValue: "Describe the effect",
|
|
1640
|
+
label: "Prompt",
|
|
1641
|
+
orderRole: "input",
|
|
1642
|
+
target: "generation.prompt",
|
|
1643
|
+
type: "text",
|
|
1644
|
+
},
|
|
1645
|
+
},
|
|
1646
|
+
title: "Generation",
|
|
1647
|
+
},
|
|
1648
|
+
],
|
|
1649
|
+
title: "Controls",
|
|
1650
|
+
},
|
|
1651
|
+
},
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
expect(
|
|
1655
|
+
validateToolcraftAcceptanceCoverage(fixedOutputSchema, [
|
|
1656
|
+
...starterAcceptance,
|
|
1657
|
+
{
|
|
1658
|
+
automated: true,
|
|
1659
|
+
automatedTestName: "canvas output remains fixed",
|
|
1660
|
+
browser: true,
|
|
1661
|
+
browserTestName: "browser: canvas output remains fixed",
|
|
1662
|
+
canvasSizingCoverage: "fixed-output-size",
|
|
1663
|
+
componentType: "fixed-output canvas",
|
|
1664
|
+
evidence: "product-output",
|
|
1665
|
+
expectedObservable:
|
|
1666
|
+
"The WebGL output remains fixed at 1920x1080 because the reference app has no user-facing output size editor.",
|
|
1667
|
+
fixture: "default canvas fixture",
|
|
1668
|
+
id: "canvas.sizing",
|
|
1669
|
+
kind: "runtime",
|
|
1670
|
+
userAction: "Open the app.",
|
|
1671
|
+
},
|
|
1672
|
+
]),
|
|
1673
|
+
).toEqual(
|
|
1674
|
+
expect.arrayContaining([
|
|
1675
|
+
expect.stringContaining("cannot be justified by the reference or previous app lacking a size editor"),
|
|
1676
|
+
]),
|
|
1677
|
+
);
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
it("rejects fixed canvas sizing for exportable product apps even when reference dimensions are fixed", () => {
|
|
1681
|
+
const fixedExportSchema = defineToolcraft({
|
|
1682
|
+
canvas: {
|
|
1683
|
+
enabled: true,
|
|
1684
|
+
size: { height: 900, unit: "px", width: 1440 },
|
|
1685
|
+
sizing: { mode: "fixed-output" },
|
|
1686
|
+
},
|
|
1687
|
+
panels: {
|
|
1688
|
+
controls: {
|
|
1689
|
+
sections: [
|
|
1690
|
+
{
|
|
1691
|
+
controls: {
|
|
1692
|
+
outputActions: {
|
|
1693
|
+
actions: [
|
|
1694
|
+
{
|
|
1695
|
+
icon: "upload-simple",
|
|
1696
|
+
label: "Export PNG",
|
|
1697
|
+
value: "export.png",
|
|
1698
|
+
},
|
|
1699
|
+
],
|
|
1700
|
+
target: "actions.output",
|
|
1701
|
+
type: "panelActions",
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
},
|
|
1705
|
+
],
|
|
1706
|
+
title: "Controls",
|
|
1707
|
+
},
|
|
1708
|
+
},
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
expect(
|
|
1712
|
+
validateToolcraftAcceptanceCoverage(fixedExportSchema, [
|
|
1713
|
+
...starterAcceptance,
|
|
1714
|
+
{
|
|
1715
|
+
automated: true,
|
|
1716
|
+
automatedTestName: "fixed reference size is preserved",
|
|
1717
|
+
browser: true,
|
|
1718
|
+
browserTestName: "browser: fixed reference size is preserved",
|
|
1719
|
+
canvasSizingCoverage: "fixed-output-size",
|
|
1720
|
+
componentType: "fixed-output canvas",
|
|
1721
|
+
evidence: "product-output",
|
|
1722
|
+
expectedObservable:
|
|
1723
|
+
"The exported shader canvas remains reference-defined and fixed at 1440x900.",
|
|
1724
|
+
fixture: "shader reference fixture",
|
|
1725
|
+
id: "canvas.sizing",
|
|
1726
|
+
kind: "runtime",
|
|
1727
|
+
userAction: "Open the app.",
|
|
1728
|
+
},
|
|
1729
|
+
]),
|
|
1730
|
+
).toEqual(
|
|
1731
|
+
expect.arrayContaining([
|
|
1732
|
+
'Product/output apps with export actions must use canvas.sizing mode "editable-output" so Aspect ratio, Canvas width, and Canvas height are always available. Put reference, fixed-format, or user-requested dimensions in canvas.size as the initial value instead of hiding size controls with "fixed-output".',
|
|
1733
|
+
]),
|
|
1734
|
+
);
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
it("requires intrinsic media sizing acceptance for upload apps that let media own canvas size", () => {
|
|
1738
|
+
const intrinsicUploadSchema = defineToolcraft({
|
|
1739
|
+
canvas: {
|
|
1740
|
+
enabled: true,
|
|
1741
|
+
sizing: { mode: "intrinsic-media" },
|
|
1742
|
+
upload: true,
|
|
1743
|
+
},
|
|
1744
|
+
panels: {
|
|
1745
|
+
controls: {
|
|
1746
|
+
sections: [],
|
|
1747
|
+
title: "Controls",
|
|
1748
|
+
},
|
|
1749
|
+
},
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
expect(validateToolcraftAcceptanceCoverage(intrinsicUploadSchema, starterAcceptance)).toEqual(
|
|
1753
|
+
expect.arrayContaining([
|
|
1754
|
+
'canvas.sizing mode "intrinsic-media" with upload requires a runtime acceptance entry with canvasSizingCoverage "intrinsic-media-size" proving the app is a true media-viewer/source-native product where imported media natural dimensions intentionally own canvas.size. Uploaded background/source images inside product canvases must use "editable-output" and keep the current canvas size.',
|
|
1755
|
+
]),
|
|
1756
|
+
);
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
it("accepts explicit intrinsic media sizing when browser coverage proves source-native canvas sizing", () => {
|
|
1760
|
+
const intrinsicUploadSchema = defineToolcraft({
|
|
1761
|
+
canvas: {
|
|
1762
|
+
enabled: true,
|
|
1763
|
+
sizing: { mode: "intrinsic-media" },
|
|
1764
|
+
upload: true,
|
|
1765
|
+
},
|
|
1766
|
+
panels: {
|
|
1767
|
+
controls: {
|
|
1768
|
+
sections: [],
|
|
1769
|
+
title: "Controls",
|
|
1770
|
+
},
|
|
1771
|
+
},
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
expect(
|
|
1775
|
+
validateToolcraftAcceptanceCoverage(intrinsicUploadSchema, [
|
|
1776
|
+
...starterAcceptance,
|
|
1777
|
+
{
|
|
1778
|
+
automated: true,
|
|
1779
|
+
automatedTestName: "media viewer uses uploaded natural dimensions",
|
|
1780
|
+
browser: true,
|
|
1781
|
+
browserTestName: "browser: upload source-native image updates canvas.size",
|
|
1782
|
+
canvasSizingCoverage: "intrinsic-media-size",
|
|
1783
|
+
componentType: "canvas",
|
|
1784
|
+
evidence: "media-lifecycle",
|
|
1785
|
+
expectedObservable:
|
|
1786
|
+
"Uploading a 1400x900 image intentionally changes canvas.size to the image natural dimensions.",
|
|
1787
|
+
fixture: "source-native media viewer fixture",
|
|
1788
|
+
id: "canvas.intrinsicSizing",
|
|
1789
|
+
kind: "runtime",
|
|
1790
|
+
userAction: "Upload an image with known natural dimensions.",
|
|
1791
|
+
},
|
|
1792
|
+
]),
|
|
1793
|
+
).toEqual([]);
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
it("requires browser reload acceptance when localStorage persistence is enabled", () => {
|
|
1797
|
+
const persistentSchema = defineToolcraft({
|
|
1798
|
+
canvas: { enabled: true },
|
|
1058
1799
|
panels: {
|
|
1059
1800
|
controls: {
|
|
1060
1801
|
sections: [
|
|
@@ -1173,67 +1914,29 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1173
1914
|
);
|
|
1174
1915
|
});
|
|
1175
1916
|
|
|
1176
|
-
it("
|
|
1177
|
-
const complexSchema =
|
|
1917
|
+
it("does not require app-authored settings transfer because setup controls are runtime-mandatory", () => {
|
|
1918
|
+
const complexSchema = createMandatorySetupSchema(false);
|
|
1178
1919
|
|
|
1179
1920
|
expect(
|
|
1180
1921
|
validateToolcraftAcceptanceCoverage(
|
|
1181
1922
|
complexSchema,
|
|
1182
|
-
|
|
1923
|
+
createMandatorySetupAcceptance(),
|
|
1183
1924
|
),
|
|
1184
|
-
).toEqual(
|
|
1185
|
-
expect.arrayContaining([
|
|
1186
|
-
expect.stringContaining(
|
|
1187
|
-
"settingsTransfer is required for this complex product app because settings-transfer eligibility was reached.",
|
|
1188
|
-
),
|
|
1189
|
-
]),
|
|
1190
|
-
);
|
|
1925
|
+
).toEqual([]);
|
|
1191
1926
|
});
|
|
1192
1927
|
|
|
1193
|
-
it("
|
|
1194
|
-
const
|
|
1928
|
+
it("accepts small schemas because settings transfer setup is runtime-mandatory", () => {
|
|
1929
|
+
const smallSchema = createMandatorySetupWithCanvasSizeSchema();
|
|
1195
1930
|
const errors = validateToolcraftAcceptanceCoverage(
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
);
|
|
1199
|
-
|
|
1200
|
-
expect(errors).not.toEqual(
|
|
1201
|
-
expect.arrayContaining([
|
|
1202
|
-
expect.stringContaining(
|
|
1203
|
-
"settingsTransfer is required for this complex product app because settings-transfer eligibility was reached.",
|
|
1204
|
-
),
|
|
1205
|
-
]),
|
|
1931
|
+
smallSchema,
|
|
1932
|
+
createMandatorySetupWithCanvasSizeAcceptance(),
|
|
1206
1933
|
);
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
it("allows explicit settings transfer opt-out only with runtime evidence", () => {
|
|
1210
|
-
const complexSchema = createSettingsTransferThresholdSchema(false);
|
|
1211
|
-
const acceptance: ToolcraftComponentAcceptance[] = [
|
|
1212
|
-
...createSettingsTransferThresholdAcceptance(),
|
|
1213
|
-
{
|
|
1214
|
-
automated: true,
|
|
1215
|
-
automatedTestName: "settings transfer intentionally omitted for session-only app",
|
|
1216
|
-
browser: true,
|
|
1217
|
-
browserTestName: "browser: settings transfer intentionally omitted",
|
|
1218
|
-
componentType: "settingsTransfer",
|
|
1219
|
-
evidence: "persistence-state",
|
|
1220
|
-
expectedObservable:
|
|
1221
|
-
"The complex app is session-only and intentionally does not expose portable settings.",
|
|
1222
|
-
fixture: "settings transfer opt-out fixture",
|
|
1223
|
-
id: "settings.transfer.optOut",
|
|
1224
|
-
kind: "runtime",
|
|
1225
|
-
settingsTransferCoverage: "opt-out",
|
|
1226
|
-
target: "runtime.settingsTransfer",
|
|
1227
|
-
userAction:
|
|
1228
|
-
"Open the session-only app, change temporary settings, and verify no portable settings workflow is required.",
|
|
1229
|
-
},
|
|
1230
|
-
];
|
|
1231
1934
|
|
|
1232
|
-
expect(
|
|
1935
|
+
expect(errors).toEqual([]);
|
|
1233
1936
|
});
|
|
1234
1937
|
|
|
1235
1938
|
it("passes complex schemas with auto settings transfer enabled", () => {
|
|
1236
|
-
const complexSchema =
|
|
1939
|
+
const complexSchema = createMandatorySetupSchema("auto");
|
|
1237
1940
|
const acceptance: ToolcraftComponentAcceptance[] = [
|
|
1238
1941
|
{
|
|
1239
1942
|
automated: true,
|
|
@@ -1251,12 +1954,63 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1251
1954
|
userAction:
|
|
1252
1955
|
"Change one complex setting, export settings, change it again, import the JSON, and observe the restored value.",
|
|
1253
1956
|
},
|
|
1254
|
-
...
|
|
1957
|
+
...createMandatorySetupAcceptance(),
|
|
1255
1958
|
];
|
|
1256
1959
|
|
|
1257
1960
|
expect(validateToolcraftAcceptanceCoverage(complexSchema, acceptance)).toEqual([]);
|
|
1258
1961
|
});
|
|
1259
1962
|
|
|
1963
|
+
it("rejects app-authored controls that try to own runtime setup targets", () => {
|
|
1964
|
+
const schema = defineToolcraft({
|
|
1965
|
+
canvas: {
|
|
1966
|
+
enabled: true,
|
|
1967
|
+
renderScale: true,
|
|
1968
|
+
size: { height: 1080, unit: "px", width: 1920 },
|
|
1969
|
+
},
|
|
1970
|
+
panels: {
|
|
1971
|
+
controls: {
|
|
1972
|
+
sections: [
|
|
1973
|
+
{
|
|
1974
|
+
controls: {
|
|
1975
|
+
manualWidth: {
|
|
1976
|
+
defaultValue: 1200,
|
|
1977
|
+
label: "Width",
|
|
1978
|
+
target: "canvas.size.width",
|
|
1979
|
+
type: "text",
|
|
1980
|
+
},
|
|
1981
|
+
manualRenderScale: {
|
|
1982
|
+
defaultValue: 1,
|
|
1983
|
+
label: "Scale",
|
|
1984
|
+
max: 2,
|
|
1985
|
+
min: 1,
|
|
1986
|
+
target: "canvas.renderScale",
|
|
1987
|
+
type: "slider",
|
|
1988
|
+
},
|
|
1989
|
+
manualTimeline: {
|
|
1990
|
+
defaultValue: true,
|
|
1991
|
+
label: "Timeline",
|
|
1992
|
+
target: "panels.timeline.extended",
|
|
1993
|
+
type: "switch",
|
|
1994
|
+
},
|
|
1995
|
+
},
|
|
1996
|
+
title: "Runtime Duplicates",
|
|
1997
|
+
},
|
|
1998
|
+
],
|
|
1999
|
+
title: "Controls",
|
|
2000
|
+
},
|
|
2001
|
+
},
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
expect(validateToolcraftAcceptanceCoverage(schema, [])).toEqual(
|
|
2005
|
+
expect.arrayContaining([
|
|
2006
|
+
'Runtime Setup must not include the Timeline switch unless panels.timeline is enabled.',
|
|
2007
|
+
'Runtime Duplicates / manualWidth uses runtime Setup target "canvas.size.width". Runtime Setup owns Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, Resolution scale, and Timeline; do not declare these controls in app-authored sections.',
|
|
2008
|
+
'Runtime Duplicates / manualRenderScale uses runtime Setup target "canvas.renderScale". Runtime Setup owns Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, Resolution scale, and Timeline; do not declare these controls in app-authored sections.',
|
|
2009
|
+
'Runtime Duplicates / manualTimeline uses runtime Setup target "panels.timeline.extended". Runtime Setup owns Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, Resolution scale, and Timeline; do not declare these controls in app-authored sections.',
|
|
2010
|
+
]),
|
|
2011
|
+
);
|
|
2012
|
+
});
|
|
2013
|
+
|
|
1260
2014
|
it("requires compound controls to cover every semantic value part", () => {
|
|
1261
2015
|
const compoundSchema = defineToolcraft({
|
|
1262
2016
|
canvas: { enabled: true },
|
|
@@ -1370,7 +2124,6 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1370
2124
|
});
|
|
1371
2125
|
|
|
1372
2126
|
const acceptance = [
|
|
1373
|
-
"runtime.settingsTransfer",
|
|
1374
2127
|
"mesh.anchor",
|
|
1375
2128
|
"mesh.focus",
|
|
1376
2129
|
"mesh.gradient",
|
|
@@ -1386,9 +2139,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1386
2139
|
browser: true,
|
|
1387
2140
|
browserTestName: `browser: ${target} changes output`,
|
|
1388
2141
|
componentType:
|
|
1389
|
-
target === "
|
|
1390
|
-
? "settingsTransfer"
|
|
1391
|
-
: target === "mesh.anchor"
|
|
2142
|
+
target === "mesh.anchor"
|
|
1392
2143
|
? "anchorGrid"
|
|
1393
2144
|
: target === "mesh.focus"
|
|
1394
2145
|
? "vector"
|
|
@@ -2109,7 +2860,6 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2109
2860
|
step: 0.1,
|
|
2110
2861
|
target: "field.speed",
|
|
2111
2862
|
type: "slider",
|
|
2112
|
-
unit: "x",
|
|
2113
2863
|
},
|
|
2114
2864
|
},
|
|
2115
2865
|
layoutGroups: [
|
|
@@ -2167,6 +2917,57 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2167
2917
|
);
|
|
2168
2918
|
});
|
|
2169
2919
|
|
|
2920
|
+
it("rejects x units on slider value labels", () => {
|
|
2921
|
+
const schema = defineToolcraft({
|
|
2922
|
+
canvas: { enabled: true },
|
|
2923
|
+
panels: {
|
|
2924
|
+
controls: {
|
|
2925
|
+
sections: [
|
|
2926
|
+
{
|
|
2927
|
+
controls: {
|
|
2928
|
+
strength: {
|
|
2929
|
+
defaultValue: 0.4,
|
|
2930
|
+
label: "Strength",
|
|
2931
|
+
max: 1,
|
|
2932
|
+
min: 0,
|
|
2933
|
+
step: 0.01,
|
|
2934
|
+
target: "glass.strength",
|
|
2935
|
+
type: "slider",
|
|
2936
|
+
unit: "x",
|
|
2937
|
+
},
|
|
2938
|
+
},
|
|
2939
|
+
title: "Glass",
|
|
2940
|
+
},
|
|
2941
|
+
],
|
|
2942
|
+
title: "Controls",
|
|
2943
|
+
},
|
|
2944
|
+
},
|
|
2945
|
+
});
|
|
2946
|
+
|
|
2947
|
+
expect(
|
|
2948
|
+
validateToolcraftAcceptanceCoverage(schema, [
|
|
2949
|
+
{
|
|
2950
|
+
automated: true,
|
|
2951
|
+
automatedTestName: "strength changes glass output",
|
|
2952
|
+
browser: true,
|
|
2953
|
+
browserTestName: "browser: strength slider changes glass output",
|
|
2954
|
+
componentType: "slider",
|
|
2955
|
+
evidence: "rendered-pixels",
|
|
2956
|
+
expectedObservable: "Changing Strength changes the rendered glass strength.",
|
|
2957
|
+
fixture: "glass strength fixture",
|
|
2958
|
+
id: "glass.strength",
|
|
2959
|
+
kind: "control",
|
|
2960
|
+
target: "glass.strength",
|
|
2961
|
+
userAction: "Drag the Strength slider.",
|
|
2962
|
+
},
|
|
2963
|
+
]),
|
|
2964
|
+
).toEqual(
|
|
2965
|
+
expect.arrayContaining([
|
|
2966
|
+
'Glass / strength (glass.strength) uses unit "x", but Toolcraft slider values do not use x suffixes. Omit unit for scale, multiplier, intensity, opacity, strength, depth, and shader amount values unless a real measurement unit applies.',
|
|
2967
|
+
]),
|
|
2968
|
+
);
|
|
2969
|
+
});
|
|
2970
|
+
|
|
2170
2971
|
it("accepts compound controls only when every semantic value part is declared", () => {
|
|
2171
2972
|
const gradientSchema = defineToolcraft({
|
|
2172
2973
|
canvas: { enabled: true },
|
|
@@ -2354,6 +3155,11 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2354
3155
|
schemaHasProductSurface(),
|
|
2355
3156
|
"Product readiness requires product surface: controls, layers, timeline, canvasContent, or acceptance coverage.",
|
|
2356
3157
|
).toBe(true);
|
|
3158
|
+
expect(
|
|
3159
|
+
starterSchema.panels.controls,
|
|
3160
|
+
"Generated product apps must define a controls panel so runtime Setup, product controls, background, export settings, and sticky export actions are visible.",
|
|
3161
|
+
).toBeTruthy();
|
|
3162
|
+
expect(starterSchema.panels.controls?.sections[0]?.title).toBe("Setup");
|
|
2357
3163
|
return;
|
|
2358
3164
|
}
|
|
2359
3165
|
|
|
@@ -2407,26 +3213,267 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2407
3213
|
expect(getAgentWorklogValidationErrors(worklogSource)).toEqual([]);
|
|
2408
3214
|
});
|
|
2409
3215
|
|
|
2410
|
-
it("
|
|
2411
|
-
const
|
|
3216
|
+
it("requires product worklogs to record storyboard evidence for video references", () => {
|
|
3217
|
+
const videoWorklogWithoutStudy = `
|
|
2412
3218
|
# Implementation Worklog
|
|
2413
3219
|
|
|
2414
3220
|
## Status
|
|
2415
3221
|
|
|
2416
|
-
Mode:
|
|
3222
|
+
Mode: product
|
|
3223
|
+
|
|
3224
|
+
## Decision Trail
|
|
3225
|
+
|
|
3226
|
+
### Iteration 1 — Product build
|
|
3227
|
+
|
|
3228
|
+
- Request: Build a product from a motion reference.
|
|
3229
|
+
- Task type: Toolcraft product build.
|
|
3230
|
+
- User-visible result: The product renders the requested motion.
|
|
3231
|
+
- Source/reference checked: User prompt and /fixtures/reference-motion/ref.mp4 video.
|
|
3232
|
+
- Reference inputs: /fixtures/reference-motion/ref.mp4 video reference.
|
|
3233
|
+
- Docs/contracts read: workflow, assembly, acceptance, performance.
|
|
3234
|
+
- Contract rules applied: runtime-shell-required and acceptance-product-observable.
|
|
3235
|
+
- Decision: Use a Toolcraft renderer.
|
|
3236
|
+
- Alternatives rejected: Raw DOM because renderer output needs canvas behavior.
|
|
3237
|
+
- State/output mapping: Runtime values drive canvasContent.
|
|
3238
|
+
- Files changed: src/app/app-schema.ts and src/app/product-renderer.tsx.
|
|
3239
|
+
- Verification: pnpm verify:final passed; browser performance checkpoint used agent-browser.
|
|
3240
|
+
- Skipped checks: None.
|
|
3241
|
+
- Risks: None.
|
|
2417
3242
|
|
|
2418
3243
|
## Decisions
|
|
2419
3244
|
|
|
2420
3245
|
### Renderer
|
|
2421
|
-
|
|
2422
|
-
-
|
|
2423
|
-
-
|
|
2424
|
-
- Evidence: No canvasContent.
|
|
3246
|
+
- Decision: Use SVG.
|
|
3247
|
+
- Reason: Vector output.
|
|
3248
|
+
- Evidence: src/app/product-renderer.tsx.
|
|
2425
3249
|
|
|
2426
3250
|
### Timeline
|
|
3251
|
+
- Decision: Use playback.
|
|
3252
|
+
- Reason: Motion output.
|
|
3253
|
+
- Evidence: src/app/app-schema.ts.
|
|
2427
3254
|
|
|
2428
|
-
|
|
2429
|
-
-
|
|
3255
|
+
### Layers
|
|
3256
|
+
- Decision: No layers.
|
|
3257
|
+
- Reason: Single output.
|
|
3258
|
+
- Evidence: src/app/app-schema.ts.
|
|
3259
|
+
|
|
3260
|
+
### Controls
|
|
3261
|
+
- Decision: Use schema controls.
|
|
3262
|
+
- Reason: Runtime owns controls.
|
|
3263
|
+
- Evidence: src/app/app-schema.ts.
|
|
3264
|
+
|
|
3265
|
+
### Export
|
|
3266
|
+
- Decision: Use panel actions.
|
|
3267
|
+
- Reason: Product output export.
|
|
3268
|
+
- Evidence: src/routes/index.tsx.
|
|
3269
|
+
|
|
3270
|
+
### Performance
|
|
3271
|
+
- Decision: Measure motion.
|
|
3272
|
+
- Reason: Motion can lag.
|
|
3273
|
+
- Evidence: src/app/app-performance.ts.
|
|
3274
|
+
|
|
3275
|
+
## Evidence
|
|
3276
|
+
|
|
3277
|
+
- Source reviewed: user prompt, ref.mp4, src/app/product-renderer.tsx.
|
|
3278
|
+
- Contract applied: Toolcraft workflow.
|
|
3279
|
+
- Evidence: pnpm verify:final passed.
|
|
3280
|
+
|
|
3281
|
+
## Verification
|
|
3282
|
+
|
|
3283
|
+
- Run: pnpm verify:final passed.
|
|
3284
|
+
- Run: browser performance checkpoint passed with agent-browser.
|
|
3285
|
+
|
|
3286
|
+
## Risks
|
|
3287
|
+
|
|
3288
|
+
- None: no known risk.
|
|
3289
|
+
`;
|
|
3290
|
+
|
|
3291
|
+
expect(getAgentWorklogValidationErrors(videoWorklogWithoutStudy)).toContain(
|
|
3292
|
+
"agent-worklog.md cites a video reference, screen recording, GIF, extracted frames, or contact sheet; record a Video Reference Study with storyboard frames and frame-to-frame transition analysis.",
|
|
3293
|
+
);
|
|
3294
|
+
});
|
|
3295
|
+
|
|
3296
|
+
it("accepts product worklogs that record video reference storyboard and transition evidence", () => {
|
|
3297
|
+
const videoWorklogWithStudy = `
|
|
3298
|
+
# Implementation Worklog
|
|
3299
|
+
|
|
3300
|
+
## Status
|
|
3301
|
+
|
|
3302
|
+
Mode: product
|
|
3303
|
+
|
|
3304
|
+
## Decision Trail
|
|
3305
|
+
|
|
3306
|
+
### Iteration 1 — Product build
|
|
3307
|
+
|
|
3308
|
+
- Request: Build a product from a motion reference.
|
|
3309
|
+
- Task type: Toolcraft product build.
|
|
3310
|
+
- User-visible result: The product renders the requested motion.
|
|
3311
|
+
- Source/reference checked: User prompt, /fixtures/reference-motion/ref.mp4 video, extracted frames, and contact sheet.
|
|
3312
|
+
- Reference inputs: /fixtures/reference-motion/ref.mp4 video reference, extracted frames, contact sheet.
|
|
3313
|
+
- Docs/contracts read: workflow, assembly, acceptance, performance.
|
|
3314
|
+
- Contract rules applied: video-reference-analysis and acceptance-product-observable.
|
|
3315
|
+
- Decision: Use a Toolcraft renderer.
|
|
3316
|
+
- Alternatives rejected: Single screenshot implementation because the video behavior changes frame to frame.
|
|
3317
|
+
- State/output mapping: Runtime values drive canvasContent.
|
|
3318
|
+
- Files changed: src/app/app-schema.ts and src/app/product-renderer.tsx.
|
|
3319
|
+
- Verification: pnpm verify:final passed; browser performance checkpoint used agent-browser.
|
|
3320
|
+
- Skipped checks: None.
|
|
3321
|
+
- Risks: None.
|
|
3322
|
+
|
|
3323
|
+
## Decisions
|
|
3324
|
+
|
|
3325
|
+
### Renderer
|
|
3326
|
+
- Decision: Use SVG.
|
|
3327
|
+
- Reason: Vector output.
|
|
3328
|
+
- Evidence: src/app/product-renderer.tsx.
|
|
3329
|
+
|
|
3330
|
+
### Timeline
|
|
3331
|
+
- Decision: Use playback.
|
|
3332
|
+
- Reason: Motion output.
|
|
3333
|
+
- Evidence: src/app/app-schema.ts.
|
|
3334
|
+
|
|
3335
|
+
### Layers
|
|
3336
|
+
- Decision: No layers.
|
|
3337
|
+
- Reason: Single output.
|
|
3338
|
+
- Evidence: src/app/app-schema.ts.
|
|
3339
|
+
|
|
3340
|
+
### Controls
|
|
3341
|
+
- Decision: Use schema controls.
|
|
3342
|
+
- Reason: Runtime owns controls.
|
|
3343
|
+
- Evidence: src/app/app-schema.ts.
|
|
3344
|
+
|
|
3345
|
+
### Export
|
|
3346
|
+
- Decision: Use panel actions.
|
|
3347
|
+
- Reason: Product output export.
|
|
3348
|
+
- Evidence: src/routes/index.tsx.
|
|
3349
|
+
|
|
3350
|
+
### Performance
|
|
3351
|
+
- Decision: Measure motion.
|
|
3352
|
+
- Reason: Motion can lag.
|
|
3353
|
+
- Evidence: src/app/app-performance.ts.
|
|
3354
|
+
|
|
3355
|
+
### Video Reference Study
|
|
3356
|
+
- Decision: Implement from storyboard frames and frame-to-frame transition analysis.
|
|
3357
|
+
- Reason: The video reference defines behavior, not only a static visual state.
|
|
3358
|
+
- Evidence: extracted frames f000/f012/f024/f036 and transition analysis between adjacent frames.
|
|
3359
|
+
|
|
3360
|
+
## Evidence
|
|
3361
|
+
|
|
3362
|
+
- Source reviewed: user prompt, ref.mp4, extracted frames, contact sheet, src/app/product-renderer.tsx.
|
|
3363
|
+
- Contract applied: Toolcraft workflow and video-reference-analysis.
|
|
3364
|
+
- Evidence: pnpm verify:final passed.
|
|
3365
|
+
|
|
3366
|
+
## Verification
|
|
3367
|
+
|
|
3368
|
+
- Run: pnpm verify:final passed.
|
|
3369
|
+
- Run: browser performance checkpoint passed with agent-browser.
|
|
3370
|
+
|
|
3371
|
+
## Risks
|
|
3372
|
+
|
|
3373
|
+
- None: no known risk.
|
|
3374
|
+
`;
|
|
3375
|
+
|
|
3376
|
+
expect(getAgentWorklogValidationErrors(videoWorklogWithStudy)).toEqual([]);
|
|
3377
|
+
});
|
|
3378
|
+
|
|
3379
|
+
it("does not treat ordinary video export worklog evidence as a video reference", () => {
|
|
3380
|
+
const videoExportWorklog = `
|
|
3381
|
+
# Implementation Worklog
|
|
3382
|
+
|
|
3383
|
+
## Status
|
|
3384
|
+
|
|
3385
|
+
Mode: product
|
|
3386
|
+
|
|
3387
|
+
## Decision Trail
|
|
3388
|
+
|
|
3389
|
+
### Iteration 1 — Product build
|
|
3390
|
+
|
|
3391
|
+
- Request: Build an animated product with export.
|
|
3392
|
+
- Task type: Toolcraft product build.
|
|
3393
|
+
- User-visible result: The product renders and exports video.
|
|
3394
|
+
- Source/reference checked: User prompt, app schema, export handler, and browser export behavior.
|
|
3395
|
+
- Reference inputs: None.
|
|
3396
|
+
- Docs/contracts read: workflow, assembly, acceptance, performance.
|
|
3397
|
+
- Contract rules applied: output-export-required and acceptance-product-observable.
|
|
3398
|
+
- Decision: Use Toolcraft export helpers.
|
|
3399
|
+
- Alternatives rejected: Blob-only checks because metadata must prove exported video behavior.
|
|
3400
|
+
- State/output mapping: Runtime timeline state drives preview and export frames.
|
|
3401
|
+
- Files changed: src/app/app-schema.ts and src/app/export.ts.
|
|
3402
|
+
- Verification: pnpm verify:final passed; browser performance checkpoint used agent-browser.
|
|
3403
|
+
- Skipped checks: None.
|
|
3404
|
+
- Risks: None.
|
|
3405
|
+
|
|
3406
|
+
## Decisions
|
|
3407
|
+
|
|
3408
|
+
### Renderer
|
|
3409
|
+
- Decision: Use Canvas 2D.
|
|
3410
|
+
- Reason: Product output is simple animated geometry.
|
|
3411
|
+
- Evidence: src/app/product-renderer.tsx.
|
|
3412
|
+
|
|
3413
|
+
### Timeline
|
|
3414
|
+
- Decision: Use playback.
|
|
3415
|
+
- Reason: Export Video requires runtime timeline time.
|
|
3416
|
+
- Evidence: src/app/app-schema.ts.
|
|
3417
|
+
|
|
3418
|
+
### Layers
|
|
3419
|
+
- Decision: No layers.
|
|
3420
|
+
- Reason: Single output.
|
|
3421
|
+
- Evidence: src/app/app-schema.ts.
|
|
3422
|
+
|
|
3423
|
+
### Controls
|
|
3424
|
+
- Decision: Use schema controls.
|
|
3425
|
+
- Reason: Runtime owns controls.
|
|
3426
|
+
- Evidence: src/app/app-schema.ts.
|
|
3427
|
+
|
|
3428
|
+
### Export
|
|
3429
|
+
- Decision: Expose Export PNG and Export Video.
|
|
3430
|
+
- Reason: Animated products need still and video output.
|
|
3431
|
+
- Evidence: src/app/export.ts.
|
|
3432
|
+
|
|
3433
|
+
### Performance
|
|
3434
|
+
- Decision: Measure export and animation.
|
|
3435
|
+
- Reason: Video export can be expensive.
|
|
3436
|
+
- Evidence: src/app/app-performance.ts.
|
|
3437
|
+
|
|
3438
|
+
## Evidence
|
|
3439
|
+
|
|
3440
|
+
- Source reviewed: user prompt, app schema, export handler, browser export behavior.
|
|
3441
|
+
- Contract applied: Toolcraft workflow.
|
|
3442
|
+
- Evidence: pnpm verify:final passed.
|
|
3443
|
+
|
|
3444
|
+
## Verification
|
|
3445
|
+
|
|
3446
|
+
- Run: pnpm verify:final passed.
|
|
3447
|
+
- Run: browser performance checkpoint passed with agent-browser.
|
|
3448
|
+
|
|
3449
|
+
## Risks
|
|
3450
|
+
|
|
3451
|
+
- None: no known risk.
|
|
3452
|
+
`;
|
|
3453
|
+
|
|
3454
|
+
expect(getAgentWorklogValidationErrors(videoExportWorklog)).toEqual([]);
|
|
3455
|
+
});
|
|
3456
|
+
|
|
3457
|
+
it("rejects stale or incomplete product worklogs", () => {
|
|
3458
|
+
const staleWorklog = `
|
|
3459
|
+
# Implementation Worklog
|
|
3460
|
+
|
|
3461
|
+
## Status
|
|
3462
|
+
|
|
3463
|
+
Mode: starter
|
|
3464
|
+
|
|
3465
|
+
## Decisions
|
|
3466
|
+
|
|
3467
|
+
### Renderer
|
|
3468
|
+
|
|
3469
|
+
- Decision: No product renderer yet.
|
|
3470
|
+
- Reason: The starter is neutral.
|
|
3471
|
+
- Evidence: No canvasContent.
|
|
3472
|
+
|
|
3473
|
+
### Timeline
|
|
3474
|
+
|
|
3475
|
+
- Decision: No timeline yet.
|
|
3476
|
+
- Reason: No animation.
|
|
2430
3477
|
- Evidence: panels.timeline is omitted.
|
|
2431
3478
|
|
|
2432
3479
|
### Layers
|
|
@@ -2518,7 +3565,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2518
3565
|
|
|
2519
3566
|
- Decision: Run viewport-stability and animation drag budgets.
|
|
2520
3567
|
- Reason: Animation and canvas movement are performance-sensitive.
|
|
2521
|
-
- Evidence:
|
|
3568
|
+
- Evidence: agent-browser performance checkpoint.
|
|
2522
3569
|
|
|
2523
3570
|
## Decision Trail
|
|
2524
3571
|
|
|
@@ -2528,13 +3575,14 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2528
3575
|
- Task type: Renderer, schema, timeline, export, and performance.
|
|
2529
3576
|
- User-visible result: The canvas renders the animated poster, the top timeline controls playback, and sticky footer actions export video and PNG.
|
|
2530
3577
|
- Source/reference checked: User prompt plus the generated product renderer requirements.
|
|
3578
|
+
- Reference inputs: None.
|
|
2531
3579
|
- Docs/contracts read: workflow.md, assembly-workflow.md, schema-reference.md, component-rules.md, renderer-technique.md, performance.md.
|
|
2532
3580
|
- Contract rules applied: runtime-shell-required, controls-layout-heuristics, timeline-enabled-behavior, output-export-required, performance-coverage-levels.
|
|
2533
3581
|
- Decision: Use SVG foreground with Canvas 2D background and Toolcraft playback timeline.
|
|
2534
3582
|
- Alternatives rejected: DOM-only output because dense animation can jank; app-local timeline because playback UI is runtime-owned.
|
|
2535
3583
|
- State/output mapping: Background and motion controls update runtime values consumed by product-renderer.tsx; timeline state drives rendered frame time; footer actions read the same renderer state for export.
|
|
2536
3584
|
- Files changed: src/app/app-schema.ts, src/app/app-performance.ts, src/app/product-renderer.tsx, e2e/app-controls.spec.ts.
|
|
2537
|
-
- Verification: pnpm verify:quick;
|
|
3585
|
+
- Verification: pnpm verify:quick; agent-browser performance checkpoint; browser timeline pause/resume and canvas drag stress.
|
|
2538
3586
|
- Skipped checks: None.
|
|
2539
3587
|
- Risks: None; browser and performance gates cover the touched surfaces.
|
|
2540
3588
|
|
|
@@ -2546,7 +3594,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2546
3594
|
## Verification
|
|
2547
3595
|
|
|
2548
3596
|
- Run: pnpm verify:quick
|
|
2549
|
-
- Run:
|
|
3597
|
+
- Run: agent-browser performance checkpoint
|
|
2550
3598
|
- Browser: timeline pause/resume and canvas drag stress.
|
|
2551
3599
|
|
|
2552
3600
|
## Risks
|
|
@@ -2557,8 +3605,8 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2557
3605
|
expect(getAgentWorklogValidationErrors(productWorklog)).toEqual([]);
|
|
2558
3606
|
});
|
|
2559
3607
|
|
|
2560
|
-
it("rejects product worklogs
|
|
2561
|
-
const
|
|
3608
|
+
it("rejects product worklogs that report incomplete required performance gates", () => {
|
|
3609
|
+
const incompletePerfWorklog = `
|
|
2562
3610
|
# Implementation Worklog
|
|
2563
3611
|
|
|
2564
3612
|
## Status
|
|
@@ -2569,116 +3617,591 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2569
3617
|
|
|
2570
3618
|
### Renderer
|
|
2571
3619
|
|
|
2572
|
-
- Decision: Use
|
|
2573
|
-
- Reason:
|
|
2574
|
-
- Evidence: src/app/app-performance.ts.
|
|
3620
|
+
- Decision: Use Canvas 2D custom renderer.
|
|
3621
|
+
- Reason: The product output is a rasterized text effect.
|
|
3622
|
+
- Evidence: src/app/product-renderer.tsx and src/app/app-performance.ts.
|
|
2575
3623
|
|
|
2576
3624
|
### Timeline
|
|
2577
3625
|
|
|
2578
|
-
- Decision: Use playback timeline.
|
|
2579
|
-
- Reason: The product
|
|
3626
|
+
- Decision: Use Toolcraft playback timeline.
|
|
3627
|
+
- Reason: The product is animated and exportable.
|
|
2580
3628
|
- Evidence: appSchema.panels.timeline.mode.
|
|
2581
3629
|
|
|
2582
3630
|
### Layers
|
|
2583
3631
|
|
|
2584
3632
|
- Decision: Do not enable layers.
|
|
2585
|
-
- Reason: The product
|
|
3633
|
+
- Reason: The product has one generated output.
|
|
2586
3634
|
- Evidence: appSchema.panels.layers is omitted.
|
|
2587
3635
|
|
|
2588
3636
|
### Controls
|
|
2589
3637
|
|
|
2590
3638
|
- Decision: Group controls by product entity.
|
|
2591
|
-
- Reason:
|
|
2592
|
-
- Evidence: src/app/app-schema.ts.
|
|
3639
|
+
- Reason: Controls map to text, motion, effects, and export.
|
|
3640
|
+
- Evidence: src/app/app-schema.ts control targets.
|
|
2593
3641
|
|
|
2594
3642
|
### Export
|
|
2595
3643
|
|
|
2596
3644
|
- Decision: Provide PNG and video export.
|
|
2597
|
-
- Reason: The
|
|
2598
|
-
- Evidence: panelActions.
|
|
3645
|
+
- Reason: The product is animated but still frames are useful.
|
|
3646
|
+
- Evidence: panelActions plus export handlers.
|
|
2599
3647
|
|
|
2600
3648
|
### Performance
|
|
2601
3649
|
|
|
2602
|
-
- Decision:
|
|
2603
|
-
- Reason:
|
|
2604
|
-
- Evidence:
|
|
3650
|
+
- Decision: Declare performance scenarios for renderer and controls.
|
|
3651
|
+
- Reason: Canvas effects are performance-sensitive.
|
|
3652
|
+
- Evidence: src/app/app-performance.ts.
|
|
2605
3653
|
|
|
2606
3654
|
## Decision Trail
|
|
2607
3655
|
|
|
2608
|
-
### Iteration 1 —
|
|
3656
|
+
### Iteration 1 — Initial product build
|
|
2609
3657
|
|
|
2610
|
-
- Request: Build an animated
|
|
2611
|
-
- Task type: Renderer,
|
|
2612
|
-
-
|
|
2613
|
-
-
|
|
2614
|
-
-
|
|
2615
|
-
-
|
|
2616
|
-
-
|
|
3658
|
+
- Request: Build an animated raster text app.
|
|
3659
|
+
- Task type: Renderer, schema, timeline, export, and performance.
|
|
3660
|
+
- User-visible result: The app renders the animated text effect and exposes export actions.
|
|
3661
|
+
- Source/reference checked: User prompt and product renderer requirements.
|
|
3662
|
+
- Reference inputs: None.
|
|
3663
|
+
- Docs/contracts read: workflow.md, assembly-workflow.md, schema-reference.md, performance.md.
|
|
3664
|
+
- Contract rules applied: runtime-shell-required, output-export-required, performance-coverage-levels.
|
|
3665
|
+
- Decision: Use Canvas 2D renderer and Toolcraft timeline.
|
|
3666
|
+
- Alternatives rejected: DOM-only output because the final effect is pixel-composited.
|
|
3667
|
+
- State/output mapping: Schema values feed product-renderer.tsx and panel actions read the same state for export.
|
|
3668
|
+
- Files changed: src/app/app-schema.ts, src/app/product-renderer.tsx, src/app/app-performance.ts.
|
|
3669
|
+
- Verification: pnpm test failed; pnpm verify:perf not complete.
|
|
3670
|
+
- Skipped checks: pnpm verify:final and pnpm verify:perf are not complete yet.
|
|
3671
|
+
- Risks: Product contract tests still need updates before final delivery.
|
|
2617
3672
|
|
|
2618
3673
|
## Evidence
|
|
2619
3674
|
|
|
2620
|
-
- Source reviewed: src/app/app-schema.ts.
|
|
2621
|
-
- Contract applied:
|
|
3675
|
+
- Source reviewed: src/app/app-schema.ts, src/app/app-performance.ts, e2e/app-controls.spec.ts.
|
|
3676
|
+
- Contract applied: performance-coverage-levels.
|
|
2622
3677
|
|
|
2623
3678
|
## Verification
|
|
2624
3679
|
|
|
2625
|
-
- Run: pnpm
|
|
3680
|
+
- Run: pnpm test failed.
|
|
3681
|
+
- Run: pnpm verify:perf not complete.
|
|
2626
3682
|
|
|
2627
3683
|
## Risks
|
|
2628
3684
|
|
|
2629
|
-
- Risk:
|
|
3685
|
+
- Risk: Required checks are not complete.
|
|
2630
3686
|
`;
|
|
2631
3687
|
|
|
2632
|
-
expect(getAgentWorklogValidationErrors(
|
|
3688
|
+
expect(getAgentWorklogValidationErrors(incompletePerfWorklog)).toEqual(
|
|
2633
3689
|
expect.arrayContaining([
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "Alternatives rejected:".',
|
|
2638
|
-
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "State/output mapping:".',
|
|
2639
|
-
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "Skipped checks:".',
|
|
3690
|
+
"agent-worklog.md required checks must be passed before final delivery; do not report failed, incomplete, pending, or blocked verification.",
|
|
3691
|
+
"agent-worklog.md must not list required checks as skipped unless they are explicitly not required for a post-first-working non-performance edit.",
|
|
3692
|
+
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or `pnpm verify:perf` with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
|
|
2640
3693
|
]),
|
|
2641
3694
|
);
|
|
2642
3695
|
});
|
|
2643
3696
|
|
|
2644
|
-
it("requires product
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
}
|
|
3697
|
+
it("requires first working product worklogs to record a browser performance checkpoint", () => {
|
|
3698
|
+
const missingPerfWorklog = `
|
|
3699
|
+
# Implementation Worklog
|
|
2648
3700
|
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
3701
|
+
## Status
|
|
3702
|
+
|
|
3703
|
+
Mode: product
|
|
3704
|
+
|
|
3705
|
+
## Decisions
|
|
3706
|
+
|
|
3707
|
+
### Renderer
|
|
3708
|
+
|
|
3709
|
+
- Decision: Use SVG renderer.
|
|
3710
|
+
- Reason: The product output is vector-native.
|
|
3711
|
+
- Evidence: src/app/product-renderer.tsx.
|
|
3712
|
+
|
|
3713
|
+
### Timeline
|
|
3714
|
+
|
|
3715
|
+
- Decision: Do not enable timeline.
|
|
3716
|
+
- Reason: The product is still output.
|
|
3717
|
+
- Evidence: appSchema.panels.timeline is omitted.
|
|
3718
|
+
|
|
3719
|
+
### Layers
|
|
3720
|
+
|
|
3721
|
+
- Decision: Do not enable layers.
|
|
3722
|
+
- Reason: The product edits one output.
|
|
3723
|
+
- Evidence: appSchema.panels.layers is omitted.
|
|
3724
|
+
|
|
3725
|
+
### Controls
|
|
3726
|
+
|
|
3727
|
+
- Decision: Group controls by product entity.
|
|
3728
|
+
- Reason: Each section maps to visible output behavior.
|
|
3729
|
+
- Evidence: src/app/app-schema.ts.
|
|
3730
|
+
|
|
3731
|
+
### Export
|
|
3732
|
+
|
|
3733
|
+
- Decision: Provide PNG export.
|
|
3734
|
+
- Reason: The product is a still image.
|
|
3735
|
+
- Evidence: panelActions.
|
|
3736
|
+
|
|
3737
|
+
### Performance
|
|
3738
|
+
|
|
3739
|
+
- Decision: Cover visible control responsiveness.
|
|
3740
|
+
- Reason: First working product delivery must prove performance coverage.
|
|
3741
|
+
- Evidence: app-performance.ts.
|
|
3742
|
+
|
|
3743
|
+
## Decision Trail
|
|
3744
|
+
|
|
3745
|
+
### Iteration 1 — Initial product build
|
|
3746
|
+
|
|
3747
|
+
- Request: Build a still vector poster app.
|
|
3748
|
+
- Task type: Schema, renderer, export, acceptance, and performance.
|
|
3749
|
+
- User-visible result: The canvas renders the poster and exports PNG.
|
|
3750
|
+
- Source/reference checked: User prompt.
|
|
3751
|
+
- Reference inputs: None.
|
|
3752
|
+
- Docs/contracts read: workflow.md, assembly-workflow.md, performance.md.
|
|
3753
|
+
- Contract rules applied: runtime-shell-required, output-export-required, performance-coverage-levels.
|
|
3754
|
+
- Decision: Use SVG renderer with Toolcraft controls.
|
|
3755
|
+
- Alternatives rejected: Canvas output because vector output must stay crisp.
|
|
3756
|
+
- State/output mapping: Schema values feed product-renderer.tsx and export uses runtime state.
|
|
3757
|
+
- Files changed: src/app/app-schema.ts, src/app/product-renderer.tsx, src/app/app-performance.ts.
|
|
3758
|
+
- Verification: pnpm verify:quick; pnpm verify:final.
|
|
3759
|
+
- Skipped checks: None.
|
|
3760
|
+
- Risks: None; final functional gate passed.
|
|
3761
|
+
|
|
3762
|
+
## Evidence
|
|
3763
|
+
|
|
3764
|
+
- Source reviewed: src/app/app-schema.ts, src/app/product-renderer.tsx.
|
|
3765
|
+
- Contract applied: runtime-shell-required, performance-coverage-levels.
|
|
3766
|
+
|
|
3767
|
+
## Verification
|
|
3768
|
+
|
|
3769
|
+
- Run: pnpm verify:quick
|
|
3770
|
+
- Run: pnpm verify:final
|
|
3771
|
+
|
|
3772
|
+
## Risks
|
|
3773
|
+
|
|
3774
|
+
- None: functional checks passed.
|
|
3775
|
+
`;
|
|
3776
|
+
|
|
3777
|
+
expect(getAgentWorklogValidationErrors(missingPerfWorklog)).toContain(
|
|
3778
|
+
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or `pnpm verify:perf` with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
|
|
2656
3779
|
);
|
|
3780
|
+
});
|
|
2657
3781
|
|
|
3782
|
+
it("recognizes supported browser performance checkpoint evidence line formats", () => {
|
|
2658
3783
|
expect(
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
3784
|
+
isPassedAgentBrowserPerfRunLine(
|
|
3785
|
+
"- Browser: performance checkpoint runner agent-browser",
|
|
3786
|
+
),
|
|
3787
|
+
).toBe(true);
|
|
2662
3788
|
expect(
|
|
2663
|
-
|
|
2664
|
-
|
|
3789
|
+
isPassedAgentBrowserPerfRunLine(
|
|
3790
|
+
"- Verification: agent-browser perf checkpoint",
|
|
3791
|
+
),
|
|
2665
3792
|
).toBe(true);
|
|
2666
3793
|
expect(
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
3794
|
+
isPassedPlaywrightFallbackPerfRunLine(
|
|
3795
|
+
"- Verification: playwright-fallback browser performance checkpoint",
|
|
3796
|
+
),
|
|
3797
|
+
).toBe(true);
|
|
3798
|
+
expect(isPassedPlaywrightFallbackPerfRunLine("- Run: pnpm verify:perf")).toBe(true);
|
|
2670
3799
|
expect(
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
3800
|
+
isPassedPlaywrightFallbackPerfRunLine(
|
|
3801
|
+
"- Run: pnpm verify:perf not complete",
|
|
3802
|
+
),
|
|
3803
|
+
).toBe(false);
|
|
2674
3804
|
expect(
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
3805
|
+
isPassedAgentBrowserPerfRunLine(
|
|
3806
|
+
"- Verification: agent-browser perf checkpoint planned after build",
|
|
3807
|
+
),
|
|
3808
|
+
).toBe(false);
|
|
2678
3809
|
expect(
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
3810
|
+
isPassedAgentBrowserPerfRunLine(
|
|
3811
|
+
"- Browser: will run agent-browser performance checkpoint",
|
|
3812
|
+
),
|
|
3813
|
+
).toBe(false);
|
|
3814
|
+
expect(
|
|
3815
|
+
isPassedPlaywrightFallbackPerfRunLine("- Run: pnpm verify:perf not run"),
|
|
3816
|
+
).toBe(false);
|
|
3817
|
+
});
|
|
3818
|
+
|
|
3819
|
+
it("recognizes explicit agent-browser fallback reason aliases", () => {
|
|
3820
|
+
expect(
|
|
3821
|
+
fallbackPerfReasonPattern.test("- Fallback reason: agent-browser-unavailable."),
|
|
3822
|
+
).toBe(true);
|
|
3823
|
+
expect(
|
|
3824
|
+
fallbackPerfReasonPattern.test("- Fallback reason: agent-browser unavailable."),
|
|
3825
|
+
).toBe(true);
|
|
3826
|
+
});
|
|
3827
|
+
|
|
3828
|
+
it("allows Playwright fallback perf only with an explicit fallback reason", () => {
|
|
3829
|
+
const playwrightFallbackWorklog = `
|
|
3830
|
+
# Implementation Worklog
|
|
3831
|
+
|
|
3832
|
+
## Status
|
|
3833
|
+
|
|
3834
|
+
Mode: product
|
|
3835
|
+
|
|
3836
|
+
## Decisions
|
|
3837
|
+
|
|
3838
|
+
### Renderer
|
|
3839
|
+
|
|
3840
|
+
- Decision: Use SVG renderer.
|
|
3841
|
+
- Reason: The product output is vector-native.
|
|
3842
|
+
- Evidence: src/app/product-renderer.tsx.
|
|
3843
|
+
|
|
3844
|
+
### Timeline
|
|
3845
|
+
|
|
3846
|
+
- Decision: Do not enable timeline.
|
|
3847
|
+
- Reason: The product is still output.
|
|
3848
|
+
- Evidence: appSchema.panels.timeline is omitted.
|
|
3849
|
+
|
|
3850
|
+
### Layers
|
|
3851
|
+
|
|
3852
|
+
- Decision: Do not enable layers.
|
|
3853
|
+
- Reason: The product edits one output.
|
|
3854
|
+
- Evidence: appSchema.panels.layers is omitted.
|
|
3855
|
+
|
|
3856
|
+
### Controls
|
|
3857
|
+
|
|
3858
|
+
- Decision: Group controls by product entity.
|
|
3859
|
+
- Reason: Each section maps to visible output behavior.
|
|
3860
|
+
- Evidence: src/app/app-schema.ts.
|
|
3861
|
+
|
|
3862
|
+
### Export
|
|
3863
|
+
|
|
3864
|
+
- Decision: Provide PNG export.
|
|
3865
|
+
- Reason: The product is a still image.
|
|
3866
|
+
- Evidence: panelActions.
|
|
3867
|
+
|
|
3868
|
+
### Performance
|
|
3869
|
+
|
|
3870
|
+
- Decision: Use Playwright fallback for the browser performance checkpoint.
|
|
3871
|
+
- Reason: The run is CI/non-agent automation with no agent browser available.
|
|
3872
|
+
- Evidence: pnpm verify:perf.
|
|
3873
|
+
|
|
3874
|
+
## Decision Trail
|
|
3875
|
+
|
|
3876
|
+
### Iteration 1 — Initial product build
|
|
3877
|
+
|
|
3878
|
+
- Request: Build a still vector poster app.
|
|
3879
|
+
- Task type: Schema, renderer, export, acceptance, and performance.
|
|
3880
|
+
- User-visible result: The canvas renders the poster and exports PNG.
|
|
3881
|
+
- Source/reference checked: User prompt.
|
|
3882
|
+
- Reference inputs: None.
|
|
3883
|
+
- Docs/contracts read: workflow.md, assembly-workflow.md, performance.md.
|
|
3884
|
+
- Contract rules applied: runtime-shell-required, output-export-required, performance-coverage-levels.
|
|
3885
|
+
- Decision: Use SVG renderer with Toolcraft controls.
|
|
3886
|
+
- Alternatives rejected: Canvas output because vector output must stay crisp.
|
|
3887
|
+
- State/output mapping: Schema values feed product-renderer.tsx and export uses runtime state.
|
|
3888
|
+
- Files changed: src/app/app-schema.ts, src/app/product-renderer.tsx, src/app/app-performance.ts.
|
|
3889
|
+
- Verification: pnpm verify:quick; pnpm verify:final; pnpm verify:perf.
|
|
3890
|
+
- Skipped checks: None.
|
|
3891
|
+
- Risks: None; final functional and fallback performance gates passed.
|
|
3892
|
+
|
|
3893
|
+
## Evidence
|
|
3894
|
+
|
|
3895
|
+
- Source reviewed: src/app/app-schema.ts, src/app/product-renderer.tsx.
|
|
3896
|
+
- Contract applied: runtime-shell-required, performance-coverage-levels.
|
|
3897
|
+
|
|
3898
|
+
## Verification
|
|
3899
|
+
|
|
3900
|
+
- Run: pnpm verify:quick
|
|
3901
|
+
- Run: pnpm verify:final
|
|
3902
|
+
- Run: pnpm verify:perf
|
|
3903
|
+
- Fallback reason: no agent browser available in CI/non-agent automation.
|
|
3904
|
+
|
|
3905
|
+
## Risks
|
|
3906
|
+
|
|
3907
|
+
- None: fallback performance evidence is recorded with reason.
|
|
3908
|
+
`;
|
|
3909
|
+
|
|
3910
|
+
expect(getAgentWorklogValidationErrors(playwrightFallbackWorklog)).toEqual([]);
|
|
3911
|
+
});
|
|
3912
|
+
|
|
3913
|
+
it("rejects Playwright fallback perf without an explicit fallback reason", () => {
|
|
3914
|
+
const missingFallbackReasonWorklog = `
|
|
3915
|
+
# Implementation Worklog
|
|
3916
|
+
|
|
3917
|
+
## Status
|
|
3918
|
+
|
|
3919
|
+
Mode: product
|
|
3920
|
+
|
|
3921
|
+
## Decisions
|
|
3922
|
+
|
|
3923
|
+
### Renderer
|
|
3924
|
+
|
|
3925
|
+
- Decision: Use SVG renderer.
|
|
3926
|
+
- Reason: The product output is vector-native.
|
|
3927
|
+
- Evidence: src/app/product-renderer.tsx.
|
|
3928
|
+
|
|
3929
|
+
### Timeline
|
|
3930
|
+
|
|
3931
|
+
- Decision: Do not enable timeline.
|
|
3932
|
+
- Reason: The product is still output.
|
|
3933
|
+
- Evidence: appSchema.panels.timeline is omitted.
|
|
3934
|
+
|
|
3935
|
+
### Layers
|
|
3936
|
+
|
|
3937
|
+
- Decision: Do not enable layers.
|
|
3938
|
+
- Reason: The product edits one output.
|
|
3939
|
+
- Evidence: appSchema.panels.layers is omitted.
|
|
3940
|
+
|
|
3941
|
+
### Controls
|
|
3942
|
+
|
|
3943
|
+
- Decision: Group controls by product entity.
|
|
3944
|
+
- Reason: Each section maps to visible output behavior.
|
|
3945
|
+
- Evidence: src/app/app-schema.ts.
|
|
3946
|
+
|
|
3947
|
+
### Export
|
|
3948
|
+
|
|
3949
|
+
- Decision: Provide PNG export.
|
|
3950
|
+
- Reason: The product is a still image.
|
|
3951
|
+
- Evidence: panelActions.
|
|
3952
|
+
|
|
3953
|
+
### Performance
|
|
3954
|
+
|
|
3955
|
+
- Decision: Run performance coverage.
|
|
3956
|
+
- Reason: First working product delivery must prove performance.
|
|
3957
|
+
- Evidence: pnpm verify:perf.
|
|
3958
|
+
|
|
3959
|
+
## Decision Trail
|
|
3960
|
+
|
|
3961
|
+
### Iteration 1 — Initial product build
|
|
3962
|
+
|
|
3963
|
+
- Request: Build a still vector poster app.
|
|
3964
|
+
- Task type: Schema, renderer, export, acceptance, and performance.
|
|
3965
|
+
- User-visible result: The canvas renders the poster and exports PNG.
|
|
3966
|
+
- Source/reference checked: User prompt.
|
|
3967
|
+
- Reference inputs: None.
|
|
3968
|
+
- Docs/contracts read: workflow.md, assembly-workflow.md, performance.md.
|
|
3969
|
+
- Contract rules applied: runtime-shell-required, output-export-required, performance-coverage-levels.
|
|
3970
|
+
- Decision: Use SVG renderer with Toolcraft controls.
|
|
3971
|
+
- Alternatives rejected: Canvas output because vector output must stay crisp.
|
|
3972
|
+
- State/output mapping: Schema values feed product-renderer.tsx and export uses runtime state.
|
|
3973
|
+
- Files changed: src/app/app-schema.ts, src/app/product-renderer.tsx, src/app/app-performance.ts.
|
|
3974
|
+
- Verification: pnpm verify:quick; pnpm verify:final; pnpm verify:perf.
|
|
3975
|
+
- Skipped checks: None.
|
|
3976
|
+
- Risks: None; final functional and performance gates passed.
|
|
3977
|
+
|
|
3978
|
+
## Evidence
|
|
3979
|
+
|
|
3980
|
+
- Source reviewed: src/app/app-schema.ts, src/app/product-renderer.tsx.
|
|
3981
|
+
- Contract applied: runtime-shell-required, performance-coverage-levels.
|
|
3982
|
+
|
|
3983
|
+
## Verification
|
|
3984
|
+
|
|
3985
|
+
- Run: pnpm verify:quick
|
|
3986
|
+
- Run: pnpm verify:final
|
|
3987
|
+
- Run: pnpm verify:perf
|
|
3988
|
+
|
|
3989
|
+
## Risks
|
|
3990
|
+
|
|
3991
|
+
- None: checks passed.
|
|
3992
|
+
`;
|
|
3993
|
+
|
|
3994
|
+
expect(getAgentWorklogValidationErrors(missingFallbackReasonWorklog)).toContain(
|
|
3995
|
+
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or `pnpm verify:perf` with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
|
|
3996
|
+
);
|
|
3997
|
+
});
|
|
3998
|
+
|
|
3999
|
+
it("allows explicit post-first-working non-performance skips of the full performance suite", () => {
|
|
4000
|
+
const postFirstWorklog = `
|
|
4001
|
+
# Implementation Worklog
|
|
4002
|
+
|
|
4003
|
+
## Status
|
|
4004
|
+
|
|
4005
|
+
Mode: product
|
|
4006
|
+
|
|
4007
|
+
## Decisions
|
|
4008
|
+
|
|
4009
|
+
### Renderer
|
|
4010
|
+
|
|
4011
|
+
- Decision: Keep the existing SVG renderer.
|
|
4012
|
+
- Reason: This pass changes copy only.
|
|
4013
|
+
- Evidence: No renderer files changed.
|
|
4014
|
+
|
|
4015
|
+
### Timeline
|
|
4016
|
+
|
|
4017
|
+
- Decision: Keep timeline disabled.
|
|
4018
|
+
- Reason: The product remains still output.
|
|
4019
|
+
- Evidence: appSchema.panels.timeline is omitted.
|
|
4020
|
+
|
|
4021
|
+
### Layers
|
|
4022
|
+
|
|
4023
|
+
- Decision: Keep layers disabled.
|
|
4024
|
+
- Reason: No layer workflow changed.
|
|
4025
|
+
- Evidence: appSchema.panels.layers is omitted.
|
|
4026
|
+
|
|
4027
|
+
### Controls
|
|
4028
|
+
|
|
4029
|
+
- Decision: Keep existing controls.
|
|
4030
|
+
- Reason: This pass only updates labels.
|
|
4031
|
+
- Evidence: src/app/app-schema.ts labels.
|
|
4032
|
+
|
|
4033
|
+
### Export
|
|
4034
|
+
|
|
4035
|
+
- Decision: Keep PNG export.
|
|
4036
|
+
- Reason: Export behavior did not change.
|
|
4037
|
+
- Evidence: panelActions unchanged.
|
|
4038
|
+
|
|
4039
|
+
### Performance
|
|
4040
|
+
|
|
4041
|
+
- Decision: Skip full perf for this post-first-working copy pass.
|
|
4042
|
+
- Reason: No renderer, workload, viewport, animation, export, or performance-sensitive control changed.
|
|
4043
|
+
- Evidence: Prior iteration recorded agent-browser performance checkpoint.
|
|
4044
|
+
|
|
4045
|
+
## Decision Trail
|
|
4046
|
+
|
|
4047
|
+
### Iteration 2 — Copy refinement
|
|
4048
|
+
|
|
4049
|
+
- Request: Rename two labels in an already delivered app.
|
|
4050
|
+
- Task type: Tier 0 copy update after the first working version.
|
|
4051
|
+
- User-visible result: The labels are clearer and product output is unchanged.
|
|
4052
|
+
- Source/reference checked: Existing app schema.
|
|
4053
|
+
- Reference inputs: None.
|
|
4054
|
+
- Docs/contracts read: workflow.md and component-rules.md.
|
|
4055
|
+
- Contract rules applied: controls-layout-heuristics.
|
|
4056
|
+
- Decision: Keep renderer, export, and performance matrix unchanged.
|
|
4057
|
+
- Alternatives rejected: Running full perf because this is a post-first-working non-performance edit.
|
|
4058
|
+
- State/output mapping: Label text changes do not alter runtime values or renderer state.
|
|
4059
|
+
- Files changed: src/app/app-schema.ts.
|
|
4060
|
+
- Verification: pnpm verify:quick.
|
|
4061
|
+
- Skipped checks: full performance checkpoint not required for this post-first-working non-performance feature loop.
|
|
4062
|
+
- Risks: None; output behavior is unchanged.
|
|
4063
|
+
|
|
4064
|
+
## Evidence
|
|
4065
|
+
|
|
4066
|
+
- Source reviewed: src/app/app-schema.ts.
|
|
4067
|
+
- Contract applied: controls-layout-heuristics.
|
|
4068
|
+
|
|
4069
|
+
## Verification
|
|
4070
|
+
|
|
4071
|
+
- Run: pnpm verify:quick
|
|
4072
|
+
- Skipped checks: full performance checkpoint not required for this post-first-working non-performance feature loop.
|
|
4073
|
+
|
|
4074
|
+
## Risks
|
|
4075
|
+
|
|
4076
|
+
- None: no performance-sensitive behavior changed.
|
|
4077
|
+
`;
|
|
4078
|
+
|
|
4079
|
+
expect(getAgentWorklogValidationErrors(postFirstWorklog)).toEqual([]);
|
|
4080
|
+
});
|
|
4081
|
+
|
|
4082
|
+
it("rejects product worklogs without an actionable decision trail", () => {
|
|
4083
|
+
const worklog = `
|
|
4084
|
+
# Implementation Worklog
|
|
4085
|
+
|
|
4086
|
+
## Status
|
|
4087
|
+
|
|
4088
|
+
Mode: product
|
|
4089
|
+
|
|
4090
|
+
## Decisions
|
|
4091
|
+
|
|
4092
|
+
### Renderer
|
|
4093
|
+
|
|
4094
|
+
- Decision: Use WebGL.
|
|
4095
|
+
- Reason: Dense animation needs batched rendering.
|
|
4096
|
+
- Evidence: src/app/app-performance.ts.
|
|
4097
|
+
|
|
4098
|
+
### Timeline
|
|
4099
|
+
|
|
4100
|
+
- Decision: Use playback timeline.
|
|
4101
|
+
- Reason: The product exposes play and scrub.
|
|
4102
|
+
- Evidence: appSchema.panels.timeline.mode.
|
|
4103
|
+
|
|
4104
|
+
### Layers
|
|
4105
|
+
|
|
4106
|
+
- Decision: Do not enable layers.
|
|
4107
|
+
- Reason: The product is single-output.
|
|
4108
|
+
- Evidence: appSchema.panels.layers is omitted.
|
|
4109
|
+
|
|
4110
|
+
### Controls
|
|
4111
|
+
|
|
4112
|
+
- Decision: Group controls by product entity.
|
|
4113
|
+
- Reason: Sections map to product behavior.
|
|
4114
|
+
- Evidence: src/app/app-schema.ts.
|
|
4115
|
+
|
|
4116
|
+
### Export
|
|
4117
|
+
|
|
4118
|
+
- Decision: Provide PNG and video export.
|
|
4119
|
+
- Reason: The app is animated.
|
|
4120
|
+
- Evidence: panelActions.
|
|
4121
|
+
|
|
4122
|
+
### Performance
|
|
4123
|
+
|
|
4124
|
+
- Decision: Run perf coverage.
|
|
4125
|
+
- Reason: Renderer is animated.
|
|
4126
|
+
- Evidence: pnpm verify:perf.
|
|
4127
|
+
|
|
4128
|
+
## Decision Trail
|
|
4129
|
+
|
|
4130
|
+
### Iteration 1 — Product build
|
|
4131
|
+
|
|
4132
|
+
- Request: Build an animated product app.
|
|
4133
|
+
- Task type: Renderer, controls, and export.
|
|
4134
|
+
- Docs/contracts read: workflow.md and renderer-technique.md.
|
|
4135
|
+
- Decision: Use WebGL and playback timeline.
|
|
4136
|
+
- Files changed: src/app/app-schema.ts and src/app/product-renderer.tsx.
|
|
4137
|
+
- Verification: pnpm verify:quick.
|
|
4138
|
+
- Risks: Performance still needs browser proof.
|
|
4139
|
+
|
|
4140
|
+
## Evidence
|
|
4141
|
+
|
|
4142
|
+
- Source reviewed: src/app/app-schema.ts.
|
|
4143
|
+
- Contract applied: runtime-shell-required.
|
|
4144
|
+
|
|
4145
|
+
## Verification
|
|
4146
|
+
|
|
4147
|
+
- Run: pnpm verify:quick
|
|
4148
|
+
|
|
4149
|
+
## Risks
|
|
4150
|
+
|
|
4151
|
+
- Risk: performance needs a follow-up run.
|
|
4152
|
+
`;
|
|
4153
|
+
|
|
4154
|
+
expect(getAgentWorklogValidationErrors(worklog)).toEqual(
|
|
4155
|
+
expect.arrayContaining([
|
|
4156
|
+
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "User-visible result:".',
|
|
4157
|
+
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "Source/reference checked:".',
|
|
4158
|
+
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "Reference inputs:".',
|
|
4159
|
+
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "Contract rules applied:".',
|
|
4160
|
+
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "Alternatives rejected:".',
|
|
4161
|
+
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "State/output mapping:".',
|
|
4162
|
+
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "Skipped checks:".',
|
|
4163
|
+
]),
|
|
4164
|
+
);
|
|
4165
|
+
});
|
|
4166
|
+
|
|
4167
|
+
it("requires product output apps to expose export actions in the sticky footer", () => {
|
|
4168
|
+
if (starterProductReadiness.mode !== "product" && !schemaHasProductSurface()) {
|
|
4169
|
+
return;
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
const panelActionTexts = getSchemaPanelActionSearchTexts();
|
|
4173
|
+
const browserTestSources = readBrowserTestSources();
|
|
4174
|
+
const productImplementationSource = getProductImplementationSource();
|
|
4175
|
+
const productRuntimeImplementationSource = getProductRuntimeImplementationSource();
|
|
4176
|
+
const backgroundColorTargets = getSchemaBackgroundControlTargets(new Set(["color"]));
|
|
4177
|
+
const backgroundToggleTargets = getSchemaBackgroundControlTargets(
|
|
4178
|
+
new Set(["checkbox", "select", "segmented", "switch"]),
|
|
4179
|
+
);
|
|
4180
|
+
|
|
4181
|
+
expect(
|
|
4182
|
+
panelActionTexts.length,
|
|
4183
|
+
"Product apps must define panelActions in the controls panel sticky footer.",
|
|
4184
|
+
).toBeGreaterThan(0);
|
|
4185
|
+
expect(
|
|
4186
|
+
panelActionTexts.some(textLooksLikePngExport),
|
|
4187
|
+
"Every product app must expose Export PNG or Download PNG through panelActions.",
|
|
4188
|
+
).toBe(true);
|
|
4189
|
+
expect(
|
|
4190
|
+
productImplementationSource,
|
|
4191
|
+
"PNG export must use createToolcraftPngExportCanvas so background transparency and retina sizing follow the standard runtime contract.",
|
|
4192
|
+
).toMatch(/\bcreateToolcraftPngExportCanvas\b/);
|
|
4193
|
+
expect(
|
|
4194
|
+
productImplementationSource,
|
|
4195
|
+
"PNG export must pass includeBackground from runtime state to createToolcraftPngExportCanvas; do not hardcode PNG transparency or background inclusion in schema only.",
|
|
4196
|
+
).toMatch(/\bcreateToolcraftPngExportCanvas\s*\(\s*\{[\s\S]*\bincludeBackground\s*:/);
|
|
4197
|
+
expect(
|
|
4198
|
+
productRuntimeImplementationSource,
|
|
4199
|
+
"Live preview must read include-background state through shouldIncludeToolcraftPreviewBackground(state) so turning Include off hides the product preview background without affecting video export.",
|
|
4200
|
+
).toMatch(/\bshouldIncludeToolcraftPreviewBackground\b/);
|
|
4201
|
+
expect(
|
|
4202
|
+
backgroundColorTargets.length,
|
|
4203
|
+
"Every product app with Export PNG must expose a user-facing background color control.",
|
|
4204
|
+
).toBeGreaterThan(0);
|
|
2682
4205
|
expect(
|
|
2683
4206
|
backgroundToggleTargets.length,
|
|
2684
4207
|
'Every product app with Export PNG must expose export.includeBackground in the required "Background" section as a Switch labeled "Include".',
|
|
@@ -2812,8 +4335,24 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2812
4335
|
).toBeGreaterThanOrEqual(2);
|
|
2813
4336
|
expect(
|
|
2814
4337
|
productImplementationSource,
|
|
2815
|
-
"Video export must use
|
|
2816
|
-
).toMatch(/\
|
|
4338
|
+
"Video export must use getToolcraftVideoExportSize so current and 4K dimensions follow the standard encoder-safe export contract.",
|
|
4339
|
+
).toMatch(/\bgetToolcraftVideoExportSize\b/);
|
|
4340
|
+
expect(
|
|
4341
|
+
sourceUsesVideoExportSizeHelper(productImplementationSource),
|
|
4342
|
+
"Video export must use getToolcraftVideoExportSize instead of custom video dimension math.",
|
|
4343
|
+
).toBe(true);
|
|
4344
|
+
expect(
|
|
4345
|
+
sourceHasUnsafeVideoLongEdgeSizing(productImplementationSource),
|
|
4346
|
+
"Video export must not use PNG-style 4096px long-edge sizing for 4K video; getToolcraftVideoExportSize fits inside 3840x2160.",
|
|
4347
|
+
).toBe(false);
|
|
4348
|
+
expect(
|
|
4349
|
+
sourceHasCaptureStreamBeforeCanvasSizing(productImplementationSource),
|
|
4350
|
+
"When using captureStream, video export must set canvas width/height before captureStream/MediaRecorder setup.",
|
|
4351
|
+
).toBe(false);
|
|
4352
|
+
expect(
|
|
4353
|
+
sourceHandlesVideoRecorderOrEncoderErrors(productImplementationSource),
|
|
4354
|
+
"Video export must reject MediaRecorder/VideoEncoder errors instead of returning corrupt video blobs.",
|
|
4355
|
+
).toBe(true);
|
|
2817
4356
|
expect(
|
|
2818
4357
|
productImplementationSource,
|
|
2819
4358
|
"Video export must use shouldIncludeToolcraftExportBackground so PNG transparency does not remove the video background.",
|
|
@@ -2892,6 +4431,10 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2892
4431
|
sourceHasVideoDurationMetadataCoverage(browserTestSources),
|
|
2893
4432
|
"Video export browser coverage must load the exported blob as a <video>, wait for loadedmetadata, and compare video.duration with the edited timeline duration. blobSize/blobType checks alone do not prove timeline-length export.",
|
|
2894
4433
|
).toBe(true);
|
|
4434
|
+
expect(
|
|
4435
|
+
sourceHasVideoDimensionMetadataCoverage(browserTestSources),
|
|
4436
|
+
"Video export browser coverage must load exported video metadata and assert video.videoWidth/video.videoHeight for both Current and 4K resolution paths. Blob size or selected control values alone do not prove conversion dimensions.",
|
|
4437
|
+
).toBe(true);
|
|
2895
4438
|
if (hasMovOrProResFormat) {
|
|
2896
4439
|
expect(
|
|
2897
4440
|
sourceHasCustomMovOrProResEncoder(productImplementationSource),
|
|
@@ -2920,7 +4463,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2920
4463
|
value: "reset",
|
|
2921
4464
|
},
|
|
2922
4465
|
{
|
|
2923
|
-
icon: "
|
|
4466
|
+
icon: "upload-simple",
|
|
2924
4467
|
label: "Export PNG",
|
|
2925
4468
|
value: "export.png",
|
|
2926
4469
|
},
|
|
@@ -2975,7 +4518,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2975
4518
|
outputActions: {
|
|
2976
4519
|
actions: [
|
|
2977
4520
|
{
|
|
2978
|
-
icon: "
|
|
4521
|
+
icon: "upload-simple",
|
|
2979
4522
|
label: "Export PNG",
|
|
2980
4523
|
value: "export.png",
|
|
2981
4524
|
},
|
|
@@ -3044,7 +4587,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3044
4587
|
outputActions: {
|
|
3045
4588
|
actions: [
|
|
3046
4589
|
{
|
|
3047
|
-
icon: "
|
|
4590
|
+
icon: "upload-simple",
|
|
3048
4591
|
label: "Export PNG",
|
|
3049
4592
|
value: "export.png",
|
|
3050
4593
|
},
|
|
@@ -3110,7 +4653,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3110
4653
|
outputActions: {
|
|
3111
4654
|
actions: [
|
|
3112
4655
|
{
|
|
3113
|
-
icon: "
|
|
4656
|
+
icon: "upload-simple",
|
|
3114
4657
|
label: "Export PNG",
|
|
3115
4658
|
value: "export.png",
|
|
3116
4659
|
},
|
|
@@ -3177,7 +4720,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3177
4720
|
outputActions: {
|
|
3178
4721
|
actions: [
|
|
3179
4722
|
{
|
|
3180
|
-
icon: "
|
|
4723
|
+
icon: "upload-simple",
|
|
3181
4724
|
label: "Export PNG",
|
|
3182
4725
|
value: "export.png",
|
|
3183
4726
|
},
|
|
@@ -3240,7 +4783,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3240
4783
|
outputActions: {
|
|
3241
4784
|
actions: [
|
|
3242
4785
|
{
|
|
3243
|
-
icon: "
|
|
4786
|
+
icon: "upload-simple",
|
|
3244
4787
|
label: "Export PNG",
|
|
3245
4788
|
value: "export.png",
|
|
3246
4789
|
},
|
|
@@ -3305,7 +4848,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3305
4848
|
outputActions: {
|
|
3306
4849
|
actions: [
|
|
3307
4850
|
{
|
|
3308
|
-
icon: "
|
|
4851
|
+
icon: "upload-simple",
|
|
3309
4852
|
label: "Export PNG",
|
|
3310
4853
|
value: "export.png",
|
|
3311
4854
|
},
|
|
@@ -3387,12 +4930,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3387
4930
|
outputActions: {
|
|
3388
4931
|
actions: [
|
|
3389
4932
|
{
|
|
3390
|
-
icon: "
|
|
4933
|
+
icon: "upload-simple",
|
|
3391
4934
|
label: "Export Video",
|
|
3392
4935
|
value: "export.video",
|
|
3393
4936
|
},
|
|
3394
4937
|
{
|
|
3395
|
-
icon: "
|
|
4938
|
+
icon: "upload-simple",
|
|
3396
4939
|
label: "Export PNG",
|
|
3397
4940
|
value: "export.png",
|
|
3398
4941
|
},
|
|
@@ -3406,7 +4949,6 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3406
4949
|
],
|
|
3407
4950
|
title: "Controls",
|
|
3408
4951
|
},
|
|
3409
|
-
timeline: { enabled: true, mode: "playback" },
|
|
3410
4952
|
},
|
|
3411
4953
|
});
|
|
3412
4954
|
|
|
@@ -3461,12 +5003,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3461
5003
|
outputActions: {
|
|
3462
5004
|
actions: [
|
|
3463
5005
|
{
|
|
3464
|
-
icon: "
|
|
5006
|
+
icon: "upload-simple",
|
|
3465
5007
|
label: "Export Video",
|
|
3466
5008
|
value: "export.video",
|
|
3467
5009
|
},
|
|
3468
5010
|
{
|
|
3469
|
-
icon: "
|
|
5011
|
+
icon: "upload-simple",
|
|
3470
5012
|
label: "Export PNG",
|
|
3471
5013
|
value: "export.png",
|
|
3472
5014
|
},
|
|
@@ -3480,7 +5022,6 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3480
5022
|
],
|
|
3481
5023
|
title: "Controls",
|
|
3482
5024
|
},
|
|
3483
|
-
timeline: { enabled: true, mode: "playback" },
|
|
3484
5025
|
},
|
|
3485
5026
|
});
|
|
3486
5027
|
const imageFormatAcceptance = makeControlAcceptance("export.image.format", "select");
|
|
@@ -3532,7 +5073,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3532
5073
|
);
|
|
3533
5074
|
});
|
|
3534
5075
|
|
|
3535
|
-
it("
|
|
5076
|
+
it("rejects disabled product controls and requires visibleWhen for unavailable controls", () => {
|
|
3536
5077
|
const schemaWithDisabledDependency = defineToolcraft({
|
|
3537
5078
|
canvas: { enabled: true },
|
|
3538
5079
|
panels: {
|
|
@@ -3582,12 +5123,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3582
5123
|
"disabledWhen target distribution.mode does not match another schema control target",
|
|
3583
5124
|
),
|
|
3584
5125
|
expect.stringContaining(
|
|
3585
|
-
"uses disabledWhen
|
|
5126
|
+
"uses disabledWhen. Generated product panels should show only controls usable in the current state",
|
|
3586
5127
|
),
|
|
3587
5128
|
]),
|
|
3588
5129
|
);
|
|
3589
5130
|
|
|
3590
|
-
const
|
|
5131
|
+
const schemaWithBranchDisabledDependency = defineToolcraft({
|
|
3591
5132
|
canvas: { enabled: true },
|
|
3592
5133
|
panels: {
|
|
3593
5134
|
controls: {
|
|
@@ -3606,10 +5147,52 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3606
5147
|
},
|
|
3607
5148
|
fillAmount: {
|
|
3608
5149
|
defaultValue: 50,
|
|
3609
|
-
disabledWhen: {
|
|
3610
|
-
equals: "full",
|
|
3611
|
-
target: "distribution.fillMode",
|
|
3612
|
-
},
|
|
5150
|
+
disabledWhen: {
|
|
5151
|
+
equals: "full",
|
|
5152
|
+
target: "distribution.fillMode",
|
|
5153
|
+
},
|
|
5154
|
+
label: "Fill level",
|
|
5155
|
+
max: 100,
|
|
5156
|
+
min: 0,
|
|
5157
|
+
target: "distribution.fillAmount",
|
|
5158
|
+
type: "slider",
|
|
5159
|
+
},
|
|
5160
|
+
},
|
|
5161
|
+
title: "Distribution",
|
|
5162
|
+
},
|
|
5163
|
+
],
|
|
5164
|
+
title: "Controls",
|
|
5165
|
+
},
|
|
5166
|
+
},
|
|
5167
|
+
});
|
|
5168
|
+
const branchErrors = validateToolcraftAcceptanceCoverage(schemaWithBranchDisabledDependency, [
|
|
5169
|
+
makeControlAcceptance("distribution.fillMode", "segmented"),
|
|
5170
|
+
{
|
|
5171
|
+
...makeControlAcceptance("distribution.fillAmount", "slider"),
|
|
5172
|
+
expectedObservable:
|
|
5173
|
+
"Fill level changes partial fill output and becomes disabled when Fill mode is Full.",
|
|
5174
|
+
userAction: "Switch Fill mode to Full, verify Fill level is disabled, then switch to Partial and drag it.",
|
|
5175
|
+
},
|
|
5176
|
+
]);
|
|
5177
|
+
|
|
5178
|
+
expect(branchErrors).toEqual(
|
|
5179
|
+
expect.arrayContaining([
|
|
5180
|
+
expect.stringContaining(
|
|
5181
|
+
"uses disabledWhen. Generated product panels should show only controls usable in the current state",
|
|
5182
|
+
),
|
|
5183
|
+
]),
|
|
5184
|
+
);
|
|
5185
|
+
|
|
5186
|
+
const schemaWithDisabledControl = defineToolcraft({
|
|
5187
|
+
canvas: { enabled: true },
|
|
5188
|
+
panels: {
|
|
5189
|
+
controls: {
|
|
5190
|
+
sections: [
|
|
5191
|
+
{
|
|
5192
|
+
controls: {
|
|
5193
|
+
fillAmount: {
|
|
5194
|
+
defaultValue: 50,
|
|
5195
|
+
disabled: true,
|
|
3613
5196
|
label: "Fill level",
|
|
3614
5197
|
max: 100,
|
|
3615
5198
|
min: 0,
|
|
@@ -3624,20 +5207,16 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3624
5207
|
},
|
|
3625
5208
|
},
|
|
3626
5209
|
});
|
|
3627
|
-
const errors = validateToolcraftAcceptanceCoverage(schemaWithValidDependency, [
|
|
3628
|
-
makeControlAcceptance("distribution.fillMode", "segmented"),
|
|
3629
|
-
{
|
|
3630
|
-
...makeControlAcceptance("distribution.fillAmount", "slider"),
|
|
3631
|
-
expectedObservable:
|
|
3632
|
-
"Fill level changes partial fill output and becomes disabled when Fill mode is Full.",
|
|
3633
|
-
userAction: "Switch Fill mode to Full, verify Fill level is disabled, then switch to Partial and drag it.",
|
|
3634
|
-
},
|
|
3635
|
-
]);
|
|
3636
5210
|
|
|
3637
|
-
expect(
|
|
5211
|
+
expect(
|
|
5212
|
+
validateToolcraftAcceptanceCoverage(schemaWithDisabledControl, [
|
|
5213
|
+
makeControlAcceptance("distribution.fillAmount", "slider"),
|
|
5214
|
+
]),
|
|
5215
|
+
).toEqual(
|
|
3638
5216
|
expect.arrayContaining([
|
|
3639
|
-
expect.stringContaining(
|
|
3640
|
-
|
|
5217
|
+
expect.stringContaining(
|
|
5218
|
+
"sets disabled: true. Generated product panels should show only controls usable in the current state",
|
|
5219
|
+
),
|
|
3641
5220
|
]),
|
|
3642
5221
|
);
|
|
3643
5222
|
});
|
|
@@ -3761,18 +5340,271 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3761
5340
|
controls: {
|
|
3762
5341
|
crt: {
|
|
3763
5342
|
defaultValue: true,
|
|
3764
|
-
label: "Enable CRT",
|
|
3765
|
-
target: "style.crt",
|
|
5343
|
+
label: "Enable CRT",
|
|
5344
|
+
target: "style.crt",
|
|
5345
|
+
type: "switch",
|
|
5346
|
+
},
|
|
5347
|
+
guides: {
|
|
5348
|
+
defaultValue: false,
|
|
5349
|
+
label: "Disable guides",
|
|
5350
|
+
target: "overlay.guides",
|
|
5351
|
+
type: "checkbox",
|
|
5352
|
+
},
|
|
5353
|
+
},
|
|
5354
|
+
title: "Style",
|
|
5355
|
+
},
|
|
5356
|
+
],
|
|
5357
|
+
title: "Controls",
|
|
5358
|
+
},
|
|
5359
|
+
},
|
|
5360
|
+
});
|
|
5361
|
+
|
|
5362
|
+
expect(
|
|
5363
|
+
validateToolcraftAcceptanceCoverage(schemaWithActionLabels, [
|
|
5364
|
+
makeControlAcceptance("style.crt", "switch"),
|
|
5365
|
+
makeControlAcceptance("overlay.guides", "checkbox"),
|
|
5366
|
+
]),
|
|
5367
|
+
).toEqual(
|
|
5368
|
+
expect.arrayContaining([
|
|
5369
|
+
expect.stringContaining(
|
|
5370
|
+
'style.crt) toggle labels must name the setting context only; use "CRT", "Background", "Glow", or "Loop" instead of "Enable CRT".',
|
|
5371
|
+
),
|
|
5372
|
+
expect.stringContaining(
|
|
5373
|
+
'overlay.guides) toggle labels must name the setting context only; use "CRT", "Background", "Glow", or "Loop" instead of "Disable guides".',
|
|
5374
|
+
),
|
|
5375
|
+
]),
|
|
5376
|
+
);
|
|
5377
|
+
|
|
5378
|
+
const schemaWithContextLabels = defineToolcraft({
|
|
5379
|
+
canvas: { enabled: true },
|
|
5380
|
+
panels: {
|
|
5381
|
+
controls: {
|
|
5382
|
+
sections: [
|
|
5383
|
+
{
|
|
5384
|
+
controls: {
|
|
5385
|
+
crt: {
|
|
5386
|
+
defaultValue: true,
|
|
5387
|
+
label: "CRT",
|
|
5388
|
+
target: "style.crt",
|
|
5389
|
+
type: "switch",
|
|
5390
|
+
},
|
|
5391
|
+
guides: {
|
|
5392
|
+
defaultValue: false,
|
|
5393
|
+
label: "Guides",
|
|
5394
|
+
target: "overlay.guides",
|
|
5395
|
+
type: "checkbox",
|
|
5396
|
+
},
|
|
5397
|
+
},
|
|
5398
|
+
title: "Style",
|
|
5399
|
+
},
|
|
5400
|
+
],
|
|
5401
|
+
title: "Controls",
|
|
5402
|
+
},
|
|
5403
|
+
},
|
|
5404
|
+
});
|
|
5405
|
+
|
|
5406
|
+
const errors = validateToolcraftAcceptanceCoverage(schemaWithContextLabels, [
|
|
5407
|
+
makeControlAcceptance("style.crt", "switch"),
|
|
5408
|
+
makeControlAcceptance("overlay.guides", "checkbox"),
|
|
5409
|
+
]);
|
|
5410
|
+
|
|
5411
|
+
expect(errors).not.toEqual(
|
|
5412
|
+
expect.arrayContaining([
|
|
5413
|
+
expect.stringContaining("toggle labels must name the setting context only"),
|
|
5414
|
+
]),
|
|
5415
|
+
);
|
|
5416
|
+
});
|
|
5417
|
+
|
|
5418
|
+
it("rejects binary control labels that duplicate their section title", () => {
|
|
5419
|
+
const schemaWithDuplicateToggleLabel = defineToolcraft({
|
|
5420
|
+
canvas: { enabled: true },
|
|
5421
|
+
panels: {
|
|
5422
|
+
controls: {
|
|
5423
|
+
sections: [
|
|
5424
|
+
{
|
|
5425
|
+
controls: {
|
|
5426
|
+
includeBackground: {
|
|
5427
|
+
defaultValue: true,
|
|
5428
|
+
label: "Background",
|
|
5429
|
+
target: "export.includeBackground",
|
|
5430
|
+
type: "switch",
|
|
5431
|
+
},
|
|
5432
|
+
},
|
|
5433
|
+
title: "Background",
|
|
5434
|
+
},
|
|
5435
|
+
],
|
|
5436
|
+
title: "Controls",
|
|
5437
|
+
},
|
|
5438
|
+
},
|
|
5439
|
+
});
|
|
5440
|
+
|
|
5441
|
+
expect(
|
|
5442
|
+
validateToolcraftAcceptanceCoverage(schemaWithDuplicateToggleLabel, [
|
|
5443
|
+
makeControlAcceptance("export.includeBackground", "switch"),
|
|
5444
|
+
]),
|
|
5445
|
+
).toEqual(
|
|
5446
|
+
expect.arrayContaining([
|
|
5447
|
+
expect.stringContaining(
|
|
5448
|
+
'export.includeBackground) toggle label "Background" duplicates section title "Background". Use a shorter contextual label such as "Include" or rename the toggle to a more specific setting.',
|
|
5449
|
+
),
|
|
5450
|
+
]),
|
|
5451
|
+
);
|
|
5452
|
+
});
|
|
5453
|
+
|
|
5454
|
+
it("rejects single Actions controls that duplicate their only button label", () => {
|
|
5455
|
+
const schemaWithDuplicateActionLabel = defineToolcraft({
|
|
5456
|
+
canvas: { enabled: true },
|
|
5457
|
+
panels: {
|
|
5458
|
+
controls: {
|
|
5459
|
+
sections: [
|
|
5460
|
+
{
|
|
5461
|
+
controls: {
|
|
5462
|
+
wash: {
|
|
5463
|
+
actions: [{ label: "Wash", value: "wash" }],
|
|
5464
|
+
defaultValue: null,
|
|
5465
|
+
label: "Wash",
|
|
5466
|
+
target: "flow.washSignal",
|
|
5467
|
+
type: "actions",
|
|
5468
|
+
},
|
|
5469
|
+
},
|
|
5470
|
+
title: "Flow",
|
|
5471
|
+
},
|
|
5472
|
+
],
|
|
5473
|
+
title: "Controls",
|
|
5474
|
+
},
|
|
5475
|
+
},
|
|
5476
|
+
});
|
|
5477
|
+
|
|
5478
|
+
expect(
|
|
5479
|
+
validateToolcraftAcceptanceCoverage(schemaWithDuplicateActionLabel, [
|
|
5480
|
+
makeControlAcceptance("flow.washSignal", "actions"),
|
|
5481
|
+
]),
|
|
5482
|
+
).toEqual(
|
|
5483
|
+
expect.arrayContaining([
|
|
5484
|
+
expect.stringContaining(
|
|
5485
|
+
'flow.washSignal) single Actions control label "Wash" duplicates its only button label "Wash". Keep the button as the command and use a short context label such as "Ink wash", "Palette action", or "Current layer".',
|
|
5486
|
+
),
|
|
5487
|
+
]),
|
|
5488
|
+
);
|
|
5489
|
+
});
|
|
5490
|
+
|
|
5491
|
+
it("allows single Actions controls with a concise context label", () => {
|
|
5492
|
+
const schemaWithContextActionLabel = defineToolcraft({
|
|
5493
|
+
canvas: { enabled: true },
|
|
5494
|
+
panels: {
|
|
5495
|
+
controls: {
|
|
5496
|
+
sections: [
|
|
5497
|
+
{
|
|
5498
|
+
controls: {
|
|
5499
|
+
wash: {
|
|
5500
|
+
actions: [{ label: "Wash", value: "wash" }],
|
|
5501
|
+
defaultValue: null,
|
|
5502
|
+
label: "Ink wash",
|
|
5503
|
+
target: "flow.washSignal",
|
|
5504
|
+
type: "actions",
|
|
5505
|
+
},
|
|
5506
|
+
},
|
|
5507
|
+
title: "Flow",
|
|
5508
|
+
},
|
|
5509
|
+
],
|
|
5510
|
+
title: "Controls",
|
|
5511
|
+
},
|
|
5512
|
+
},
|
|
5513
|
+
});
|
|
5514
|
+
|
|
5515
|
+
const errors = validateToolcraftAcceptanceCoverage(schemaWithContextActionLabel, [
|
|
5516
|
+
makeControlAcceptance("flow.washSignal", "actions"),
|
|
5517
|
+
]);
|
|
5518
|
+
|
|
5519
|
+
expect(errors).not.toEqual(
|
|
5520
|
+
expect.arrayContaining([
|
|
5521
|
+
expect.stringContaining("single Actions control label"),
|
|
5522
|
+
]),
|
|
5523
|
+
);
|
|
5524
|
+
});
|
|
5525
|
+
|
|
5526
|
+
it("allows short Include toggle plus an unlabeled background color in the required row", () => {
|
|
5527
|
+
const schema = defineToolcraft({
|
|
5528
|
+
canvas: { enabled: true },
|
|
5529
|
+
panels: {
|
|
5530
|
+
controls: {
|
|
5531
|
+
sections: [
|
|
5532
|
+
{
|
|
5533
|
+
controls: {
|
|
5534
|
+
includeBackground: {
|
|
5535
|
+
defaultValue: true,
|
|
5536
|
+
description:
|
|
5537
|
+
"Controls preview and PNG background visibility while video keeps the background.",
|
|
5538
|
+
label: "Include",
|
|
5539
|
+
target: "export.includeBackground",
|
|
5540
|
+
type: "switch",
|
|
5541
|
+
},
|
|
5542
|
+
background: {
|
|
5543
|
+
defaultValue: "#0F0F0F",
|
|
5544
|
+
label: false,
|
|
5545
|
+
target: "appearance.background",
|
|
5546
|
+
type: "color",
|
|
5547
|
+
},
|
|
5548
|
+
},
|
|
5549
|
+
layoutGroups: [
|
|
5550
|
+
{
|
|
5551
|
+
columns: 2,
|
|
5552
|
+
controls: ["includeBackground", "background"],
|
|
5553
|
+
layout: "inline",
|
|
5554
|
+
},
|
|
5555
|
+
],
|
|
5556
|
+
title: "Background",
|
|
5557
|
+
},
|
|
5558
|
+
],
|
|
5559
|
+
title: "Controls",
|
|
5560
|
+
},
|
|
5561
|
+
},
|
|
5562
|
+
});
|
|
5563
|
+
|
|
5564
|
+
const errors = validateToolcraftAcceptanceCoverage(schema, [
|
|
5565
|
+
makeControlAcceptance("export.includeBackground", "switch"),
|
|
5566
|
+
makeControlAcceptance("appearance.background", "color"),
|
|
5567
|
+
]);
|
|
5568
|
+
|
|
5569
|
+
expect(errors).not.toEqual(
|
|
5570
|
+
expect.arrayContaining([
|
|
5571
|
+
expect.stringContaining("duplicates section title"),
|
|
5572
|
+
expect.stringContaining("too long for a two-column toggle row"),
|
|
5573
|
+
expect.stringContaining("must use the short visible label"),
|
|
5574
|
+
expect.stringContaining("must use label false"),
|
|
5575
|
+
]),
|
|
5576
|
+
);
|
|
5577
|
+
});
|
|
5578
|
+
|
|
5579
|
+
it("allows short visible toggle labels beside one related parameter", () => {
|
|
5580
|
+
const schema = defineToolcraft({
|
|
5581
|
+
canvas: { enabled: true },
|
|
5582
|
+
panels: {
|
|
5583
|
+
controls: {
|
|
5584
|
+
sections: [
|
|
5585
|
+
{
|
|
5586
|
+
controls: {
|
|
5587
|
+
loop: {
|
|
5588
|
+
defaultValue: true,
|
|
5589
|
+
label: "Loop",
|
|
5590
|
+
target: "animation.loop",
|
|
3766
5591
|
type: "switch",
|
|
3767
5592
|
},
|
|
3768
|
-
|
|
3769
|
-
defaultValue:
|
|
3770
|
-
label:
|
|
3771
|
-
target: "
|
|
3772
|
-
type: "
|
|
5593
|
+
duration: {
|
|
5594
|
+
defaultValue: "8",
|
|
5595
|
+
label: false,
|
|
5596
|
+
target: "animation.duration",
|
|
5597
|
+
type: "text",
|
|
3773
5598
|
},
|
|
3774
5599
|
},
|
|
3775
|
-
|
|
5600
|
+
layoutGroups: [
|
|
5601
|
+
{
|
|
5602
|
+
columns: 2,
|
|
5603
|
+
controls: ["loop", "duration"],
|
|
5604
|
+
layout: "inline",
|
|
5605
|
+
},
|
|
5606
|
+
],
|
|
5607
|
+
title: "Playback",
|
|
3776
5608
|
},
|
|
3777
5609
|
],
|
|
3778
5610
|
title: "Controls",
|
|
@@ -3780,43 +5612,32 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3780
5612
|
},
|
|
3781
5613
|
});
|
|
3782
5614
|
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
).toEqual(
|
|
3789
|
-
expect.arrayContaining([
|
|
3790
|
-
expect.stringContaining(
|
|
3791
|
-
'style.crt) toggle labels must name the setting context only; use "CRT", "Background", "Glow", or "Loop" instead of "Enable CRT".',
|
|
3792
|
-
),
|
|
3793
|
-
expect.stringContaining(
|
|
3794
|
-
'overlay.guides) toggle labels must name the setting context only; use "CRT", "Background", "Glow", or "Loop" instead of "Disable guides".',
|
|
3795
|
-
),
|
|
3796
|
-
]),
|
|
5615
|
+
const errors = validateToolcraftAcceptanceCoverage(schema, [
|
|
5616
|
+
makeControlAcceptance("animation.loop", "switch"),
|
|
5617
|
+
makeControlAcceptance("animation.duration", "text"),
|
|
5618
|
+
]);
|
|
5619
|
+
|
|
5620
|
+
expect(errors).not.toEqual(
|
|
5621
|
+
expect.arrayContaining([expect.stringContaining("compact toggle-plus-parameter row")]),
|
|
3797
5622
|
);
|
|
5623
|
+
});
|
|
3798
5624
|
|
|
3799
|
-
|
|
5625
|
+
it("rejects CodeTextarea for short single-line text content", () => {
|
|
5626
|
+
const schema = defineToolcraft({
|
|
3800
5627
|
canvas: { enabled: true },
|
|
3801
5628
|
panels: {
|
|
3802
5629
|
controls: {
|
|
3803
5630
|
sections: [
|
|
3804
5631
|
{
|
|
3805
5632
|
controls: {
|
|
3806
|
-
|
|
3807
|
-
defaultValue:
|
|
3808
|
-
label: "
|
|
3809
|
-
target: "
|
|
3810
|
-
type: "
|
|
3811
|
-
},
|
|
3812
|
-
guides: {
|
|
3813
|
-
defaultValue: false,
|
|
3814
|
-
label: "Guides",
|
|
3815
|
-
target: "overlay.guides",
|
|
3816
|
-
type: "checkbox",
|
|
5633
|
+
buttonText: {
|
|
5634
|
+
defaultValue: "Glass",
|
|
5635
|
+
label: "Text",
|
|
5636
|
+
target: "button.text",
|
|
5637
|
+
type: "code",
|
|
3817
5638
|
},
|
|
3818
5639
|
},
|
|
3819
|
-
title: "
|
|
5640
|
+
title: "Button",
|
|
3820
5641
|
},
|
|
3821
5642
|
],
|
|
3822
5643
|
title: "Controls",
|
|
@@ -3824,34 +5645,35 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3824
5645
|
},
|
|
3825
5646
|
});
|
|
3826
5647
|
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
expect(errors).not.toEqual(
|
|
5648
|
+
expect(
|
|
5649
|
+
validateToolcraftAcceptanceCoverage(schema, [
|
|
5650
|
+
makeControlAcceptance("button.text", "code"),
|
|
5651
|
+
]),
|
|
5652
|
+
).toEqual(
|
|
3833
5653
|
expect.arrayContaining([
|
|
3834
|
-
expect.stringContaining("
|
|
5654
|
+
expect.stringContaining("uses CodeTextarea for short single-line text"),
|
|
3835
5655
|
]),
|
|
3836
5656
|
);
|
|
3837
5657
|
});
|
|
3838
5658
|
|
|
3839
|
-
it("
|
|
3840
|
-
const
|
|
5659
|
+
it("allows CodeTextarea when a short default documents long multiline intent", () => {
|
|
5660
|
+
const schema = defineToolcraft({
|
|
3841
5661
|
canvas: { enabled: true },
|
|
3842
5662
|
panels: {
|
|
3843
5663
|
controls: {
|
|
3844
5664
|
sections: [
|
|
3845
5665
|
{
|
|
3846
5666
|
controls: {
|
|
3847
|
-
|
|
3848
|
-
defaultValue:
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
5667
|
+
prompt: {
|
|
5668
|
+
defaultValue: "Describe the scene",
|
|
5669
|
+
description:
|
|
5670
|
+
"Long multiline prompt content for generated output.",
|
|
5671
|
+
label: "Prompt",
|
|
5672
|
+
target: "generation.prompt",
|
|
5673
|
+
type: "code",
|
|
3852
5674
|
},
|
|
3853
5675
|
},
|
|
3854
|
-
title: "
|
|
5676
|
+
title: "Generation",
|
|
3855
5677
|
},
|
|
3856
5678
|
],
|
|
3857
5679
|
title: "Controls",
|
|
@@ -3860,19 +5682,17 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3860
5682
|
});
|
|
3861
5683
|
|
|
3862
5684
|
expect(
|
|
3863
|
-
validateToolcraftAcceptanceCoverage(
|
|
3864
|
-
makeControlAcceptance("
|
|
5685
|
+
validateToolcraftAcceptanceCoverage(schema, [
|
|
5686
|
+
makeControlAcceptance("generation.prompt", "code"),
|
|
3865
5687
|
]),
|
|
3866
|
-
).toEqual(
|
|
5688
|
+
).not.toEqual(
|
|
3867
5689
|
expect.arrayContaining([
|
|
3868
|
-
expect.stringContaining(
|
|
3869
|
-
'export.includeBackground) toggle label "Background" duplicates section title "Background". Use a shorter contextual label such as "Include" or rename the toggle to a more specific setting.',
|
|
3870
|
-
),
|
|
5690
|
+
expect.stringContaining("uses CodeTextarea for short single-line text"),
|
|
3871
5691
|
]),
|
|
3872
5692
|
);
|
|
3873
5693
|
});
|
|
3874
5694
|
|
|
3875
|
-
it("
|
|
5695
|
+
it("rejects visible parameter labels beside one related toggle", () => {
|
|
3876
5696
|
const schema = defineToolcraft({
|
|
3877
5697
|
canvas: { enabled: true },
|
|
3878
5698
|
panels: {
|
|
@@ -3880,29 +5700,27 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3880
5700
|
sections: [
|
|
3881
5701
|
{
|
|
3882
5702
|
controls: {
|
|
3883
|
-
|
|
5703
|
+
loop: {
|
|
3884
5704
|
defaultValue: true,
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
label: "Include",
|
|
3888
|
-
target: "export.includeBackground",
|
|
5705
|
+
label: "Loop",
|
|
5706
|
+
target: "animation.loop",
|
|
3889
5707
|
type: "switch",
|
|
3890
5708
|
},
|
|
3891
|
-
|
|
3892
|
-
defaultValue: "
|
|
3893
|
-
label:
|
|
3894
|
-
target: "
|
|
3895
|
-
type: "
|
|
5709
|
+
duration: {
|
|
5710
|
+
defaultValue: "8",
|
|
5711
|
+
label: "Duration",
|
|
5712
|
+
target: "animation.duration",
|
|
5713
|
+
type: "text",
|
|
3896
5714
|
},
|
|
3897
5715
|
},
|
|
3898
5716
|
layoutGroups: [
|
|
3899
5717
|
{
|
|
3900
5718
|
columns: 2,
|
|
3901
|
-
controls: ["
|
|
5719
|
+
controls: ["loop", "duration"],
|
|
3902
5720
|
layout: "inline",
|
|
3903
5721
|
},
|
|
3904
5722
|
],
|
|
3905
|
-
title: "
|
|
5723
|
+
title: "Playback",
|
|
3906
5724
|
},
|
|
3907
5725
|
],
|
|
3908
5726
|
title: "Controls",
|
|
@@ -3910,22 +5728,21 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3910
5728
|
},
|
|
3911
5729
|
});
|
|
3912
5730
|
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
5731
|
+
expect(
|
|
5732
|
+
validateToolcraftAcceptanceCoverage(schema, [
|
|
5733
|
+
makeControlAcceptance("animation.loop", "switch"),
|
|
5734
|
+
makeControlAcceptance("animation.duration", "text"),
|
|
5735
|
+
]),
|
|
5736
|
+
).toEqual(
|
|
3919
5737
|
expect.arrayContaining([
|
|
3920
|
-
expect.stringContaining(
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
expect.stringContaining("must use label false"),
|
|
5738
|
+
expect.stringContaining(
|
|
5739
|
+
'Playback layoutGroups inline row "loop, duration" pairs a toggle with parameter labels duration "Duration". In toggle-plus-parameter rows, the non-toggle parameter must use label false; if that label is needed, stack the controls instead.',
|
|
5740
|
+
),
|
|
3924
5741
|
]),
|
|
3925
5742
|
);
|
|
3926
5743
|
});
|
|
3927
5744
|
|
|
3928
|
-
it("
|
|
5745
|
+
it("rejects segmented controls in inline half-width layout rows", () => {
|
|
3929
5746
|
const schema = defineToolcraft({
|
|
3930
5747
|
canvas: { enabled: true },
|
|
3931
5748
|
panels: {
|
|
@@ -3933,27 +5750,31 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3933
5750
|
sections: [
|
|
3934
5751
|
{
|
|
3935
5752
|
controls: {
|
|
3936
|
-
|
|
5753
|
+
includeText: {
|
|
3937
5754
|
defaultValue: true,
|
|
3938
|
-
label: "
|
|
3939
|
-
target: "
|
|
5755
|
+
label: "Include",
|
|
5756
|
+
target: "text.enabled",
|
|
3940
5757
|
type: "switch",
|
|
3941
5758
|
},
|
|
3942
|
-
|
|
3943
|
-
defaultValue: "
|
|
3944
|
-
label: "
|
|
3945
|
-
|
|
3946
|
-
|
|
5759
|
+
dragTarget: {
|
|
5760
|
+
defaultValue: "glass",
|
|
5761
|
+
label: "Drag",
|
|
5762
|
+
options: [
|
|
5763
|
+
{ label: "Glass", value: "glass" },
|
|
5764
|
+
{ label: "Text", value: "text" },
|
|
5765
|
+
],
|
|
5766
|
+
target: "text.dragTarget",
|
|
5767
|
+
type: "segmented",
|
|
3947
5768
|
},
|
|
3948
5769
|
},
|
|
3949
5770
|
layoutGroups: [
|
|
3950
5771
|
{
|
|
3951
5772
|
columns: 2,
|
|
3952
|
-
controls: ["
|
|
5773
|
+
controls: ["includeText", "dragTarget"],
|
|
3953
5774
|
layout: "inline",
|
|
3954
5775
|
},
|
|
3955
5776
|
],
|
|
3956
|
-
title: "
|
|
5777
|
+
title: "Glass Text",
|
|
3957
5778
|
},
|
|
3958
5779
|
],
|
|
3959
5780
|
title: "Controls",
|
|
@@ -3961,13 +5782,17 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3961
5782
|
},
|
|
3962
5783
|
});
|
|
3963
5784
|
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
expect.arrayContaining([
|
|
5785
|
+
expect(
|
|
5786
|
+
validateToolcraftAcceptanceCoverage(schema, [
|
|
5787
|
+
makeControlAcceptance("text.enabled", "switch"),
|
|
5788
|
+
makeControlAcceptance("text.dragTarget", "segmented"),
|
|
5789
|
+
]),
|
|
5790
|
+
).toEqual(
|
|
5791
|
+
expect.arrayContaining([
|
|
5792
|
+
expect.stringContaining(
|
|
5793
|
+
'Glass Text layoutGroups inline row "includeText, dragTarget" includes segmented control dragTarget. Segmented is full-width and must not share a two-column or half-width row; use Select when a finite choice must fit beside another control.',
|
|
5794
|
+
),
|
|
5795
|
+
]),
|
|
3971
5796
|
);
|
|
3972
5797
|
});
|
|
3973
5798
|
|
|
@@ -4185,44 +6010,227 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4185
6010
|
title: "Output",
|
|
4186
6011
|
},
|
|
4187
6012
|
],
|
|
4188
|
-
title: "Controls",
|
|
6013
|
+
title: "Controls",
|
|
6014
|
+
},
|
|
6015
|
+
},
|
|
6016
|
+
});
|
|
6017
|
+
|
|
6018
|
+
expect(
|
|
6019
|
+
validateToolcraftAcceptanceCoverage(schema, [
|
|
6020
|
+
makeControlAcceptance("output.background", "switch"),
|
|
6021
|
+
makeControlAcceptance("debug.diagnosticOverlay", "switch"),
|
|
6022
|
+
]),
|
|
6023
|
+
).toEqual(
|
|
6024
|
+
expect.arrayContaining([
|
|
6025
|
+
'Output layoutGroups inline row "background, diagnosticOverlay" includes switch labels diagnosticOverlay "Diagnostic overlay" that are too long for a two-column toggle row. Switches share a row only when every visible label fits without truncation; shorten labels or stack them.',
|
|
6026
|
+
]),
|
|
6027
|
+
);
|
|
6028
|
+
});
|
|
6029
|
+
|
|
6030
|
+
it("requires explicit reference behavior coverage in reference-runtime-clone mode", () => {
|
|
6031
|
+
expect(
|
|
6032
|
+
validateToolcraftAcceptanceCoverage(starterSchema, starterAcceptance, {
|
|
6033
|
+
behaviorCoverage: [
|
|
6034
|
+
"canvas-sizing",
|
|
6035
|
+
"control-mapping",
|
|
6036
|
+
"renderer-state",
|
|
6037
|
+
"renderer-loop",
|
|
6038
|
+
],
|
|
6039
|
+
mode: "reference-runtime-clone",
|
|
6040
|
+
referenceName: "legacy badge wall",
|
|
6041
|
+
sourceOfTruth: "reference-runtime",
|
|
6042
|
+
} as never),
|
|
6043
|
+
).toEqual(
|
|
6044
|
+
expect.arrayContaining([
|
|
6045
|
+
'reference-runtime-clone behaviorCoverage "canvas-sizing" is missing an acceptance entry with referenceCoverage "canvas-sizing".',
|
|
6046
|
+
'reference-runtime-clone behaviorCoverage "control-mapping" is missing an acceptance entry with referenceCoverage "control-mapping".',
|
|
6047
|
+
'reference-runtime-clone behaviorCoverage "renderer-state" is missing an acceptance entry with referenceCoverage "renderer-state".',
|
|
6048
|
+
'reference-runtime-clone behaviorCoverage "renderer-loop" is missing an acceptance entry with referenceCoverage "renderer-loop".',
|
|
6049
|
+
'reference-runtime-clone transferMode must declare referenceTimeline with mode "none", "toolcraft-playback", "toolcraft-keyframes", or "custom-reference-timeline".',
|
|
6050
|
+
]),
|
|
6051
|
+
);
|
|
6052
|
+
});
|
|
6053
|
+
|
|
6054
|
+
it("requires a reference feature inventory before accepting a reference-runtime-clone", () => {
|
|
6055
|
+
const referenceAcceptance = [
|
|
6056
|
+
...starterAcceptance,
|
|
6057
|
+
makeReferenceCoverageAcceptance("reference.canvasSizing", "canvas-sizing"),
|
|
6058
|
+
makeReferenceCoverageAcceptance("reference.controlMapping", "control-mapping"),
|
|
6059
|
+
makeReferenceCoverageAcceptance("reference.rendererState", "renderer-state"),
|
|
6060
|
+
];
|
|
6061
|
+
|
|
6062
|
+
expect(
|
|
6063
|
+
validateToolcraftAcceptanceCoverage(starterSchema, referenceAcceptance, {
|
|
6064
|
+
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
6065
|
+
mode: "reference-runtime-clone",
|
|
6066
|
+
referenceName: "legacy badge wall",
|
|
6067
|
+
referenceTimeline: { behaviorCoverage: [], mode: "none" },
|
|
6068
|
+
sourceOfTruth: "reference-runtime",
|
|
6069
|
+
}),
|
|
6070
|
+
).toEqual(
|
|
6071
|
+
expect.arrayContaining([
|
|
6072
|
+
"reference-runtime-clone transferMode must declare referenceFeatureInventory with every user-visible and output-affecting behavior from the inspected reference, mapped to Toolcraft implementation and acceptance coverage.",
|
|
6073
|
+
'reference-runtime-clone behaviorCoverage "canvas-sizing" must be represented in referenceFeatureInventory by an item whose acceptanceId points to that referenceCoverage.',
|
|
6074
|
+
'reference-runtime-clone behaviorCoverage "control-mapping" must be represented in referenceFeatureInventory by an item whose acceptanceId points to that referenceCoverage.',
|
|
6075
|
+
'reference-runtime-clone behaviorCoverage "renderer-state" must be represented in referenceFeatureInventory by an item whose acceptanceId points to that referenceCoverage.',
|
|
6076
|
+
"reference.canvasSizing declares reference coverage but is not mapped from referenceFeatureInventory. Every reference acceptance row must correspond to an inventoried reference feature.",
|
|
6077
|
+
]),
|
|
6078
|
+
);
|
|
6079
|
+
});
|
|
6080
|
+
|
|
6081
|
+
it("requires reference study evidence before accepting a reference-runtime-clone", () => {
|
|
6082
|
+
expect(
|
|
6083
|
+
validateToolcraftAcceptanceCoverage(
|
|
6084
|
+
starterSchema,
|
|
6085
|
+
[
|
|
6086
|
+
...starterAcceptance,
|
|
6087
|
+
makeReferenceCoverageAcceptance("reference.canvasSizing", "canvas-sizing"),
|
|
6088
|
+
makeReferenceCoverageAcceptance("reference.controlMapping", "control-mapping"),
|
|
6089
|
+
makeReferenceCoverageAcceptance("reference.rendererState", "renderer-state"),
|
|
6090
|
+
],
|
|
6091
|
+
{
|
|
6092
|
+
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
6093
|
+
mode: "reference-runtime-clone",
|
|
6094
|
+
referenceFeatureInventory: makeReferenceFeatureInventory(),
|
|
6095
|
+
referenceName: "legacy badge wall",
|
|
6096
|
+
referenceTimeline: { behaviorCoverage: [], mode: "none" },
|
|
6097
|
+
sourceOfTruth: "reference-runtime",
|
|
6098
|
+
},
|
|
6099
|
+
),
|
|
6100
|
+
).toContain(
|
|
6101
|
+
"reference-runtime-clone transferMode must declare referenceStudy proving the reference was inspected and, when runnable or reconstructable, run or restored locally before implementation.",
|
|
6102
|
+
);
|
|
6103
|
+
});
|
|
6104
|
+
|
|
6105
|
+
it("requires a concrete blocker for source-only reference study", () => {
|
|
6106
|
+
expect(
|
|
6107
|
+
validateToolcraftAcceptanceCoverage(
|
|
6108
|
+
starterSchema,
|
|
6109
|
+
[
|
|
6110
|
+
...starterAcceptance,
|
|
6111
|
+
makeReferenceCoverageAcceptance("reference.canvasSizing", "canvas-sizing"),
|
|
6112
|
+
makeReferenceCoverageAcceptance("reference.controlMapping", "control-mapping"),
|
|
6113
|
+
makeReferenceCoverageAcceptance("reference.rendererState", "renderer-state"),
|
|
6114
|
+
],
|
|
6115
|
+
{
|
|
6116
|
+
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
6117
|
+
mode: "reference-runtime-clone",
|
|
6118
|
+
referenceFeatureInventory: makeReferenceFeatureInventory(),
|
|
6119
|
+
referenceName: "legacy badge wall",
|
|
6120
|
+
referenceStudy: {
|
|
6121
|
+
...referenceStudyEvidence,
|
|
6122
|
+
sourceOnlyReason: "Source review was enough.",
|
|
6123
|
+
status: "source-inspection-only",
|
|
6124
|
+
},
|
|
6125
|
+
referenceTimeline: { behaviorCoverage: [], mode: "none" },
|
|
6126
|
+
sourceOfTruth: "reference-runtime",
|
|
6127
|
+
},
|
|
6128
|
+
),
|
|
6129
|
+
).toContain(
|
|
6130
|
+
"referenceStudy.sourceOnlyReason must state the concrete blocker that made running or restoring the reference unavailable.",
|
|
6131
|
+
);
|
|
6132
|
+
});
|
|
6133
|
+
|
|
6134
|
+
it("rejects reference feature inventory items that are not backed by reference acceptance coverage", () => {
|
|
6135
|
+
const ordinaryControlAcceptance = makeControlAcceptance("appearance.opacity", "slider");
|
|
6136
|
+
|
|
6137
|
+
expect(
|
|
6138
|
+
validateToolcraftAcceptanceCoverage(
|
|
6139
|
+
starterSchema,
|
|
6140
|
+
[
|
|
6141
|
+
...starterAcceptance,
|
|
6142
|
+
ordinaryControlAcceptance,
|
|
6143
|
+
makeReferenceCoverageAcceptance("reference.controlMapping", "control-mapping"),
|
|
6144
|
+
makeReferenceCoverageAcceptance("reference.rendererState", "renderer-state"),
|
|
6145
|
+
],
|
|
6146
|
+
{
|
|
6147
|
+
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
6148
|
+
mode: "reference-runtime-clone",
|
|
6149
|
+
referenceFeatureInventory: [
|
|
6150
|
+
{
|
|
6151
|
+
acceptanceId: "appearance.opacity",
|
|
6152
|
+
behaviorEvidence:
|
|
6153
|
+
"Observed the reference browser output preserve its canvas dimensions.",
|
|
6154
|
+
featureName: "Canvas sizing",
|
|
6155
|
+
id: "canvas-sizing",
|
|
6156
|
+
referenceBehavior:
|
|
6157
|
+
"The reference renderer owns output sizing and canvas dimensions.",
|
|
6158
|
+
sourceEvidence: "Inspected reference renderer sizing source.",
|
|
6159
|
+
status: "ported",
|
|
6160
|
+
toolcraftMapping:
|
|
6161
|
+
"Toolcraft editable-output sizing preserves the reference dimensions.",
|
|
6162
|
+
},
|
|
6163
|
+
...makeReferenceFeatureInventory().slice(1),
|
|
6164
|
+
],
|
|
6165
|
+
referenceName: "legacy badge wall",
|
|
6166
|
+
referenceTimeline: { behaviorCoverage: [], mode: "none" },
|
|
6167
|
+
sourceOfTruth: "reference-runtime",
|
|
4189
6168
|
},
|
|
4190
|
-
|
|
4191
|
-
});
|
|
4192
|
-
|
|
4193
|
-
expect(
|
|
4194
|
-
validateToolcraftAcceptanceCoverage(schema, [
|
|
4195
|
-
makeControlAcceptance("output.background", "switch"),
|
|
4196
|
-
makeControlAcceptance("debug.diagnosticOverlay", "switch"),
|
|
4197
|
-
]),
|
|
6169
|
+
),
|
|
4198
6170
|
).toEqual(
|
|
4199
6171
|
expect.arrayContaining([
|
|
4200
|
-
'
|
|
6172
|
+
'referenceFeatureInventory "canvas-sizing" acceptanceId "appearance.opacity" must point to an acceptance entry with referenceCoverage or referenceTimelineCoverage.',
|
|
6173
|
+
'reference-runtime-clone behaviorCoverage "canvas-sizing" must be represented in referenceFeatureInventory by an item whose acceptanceId points to that referenceCoverage.',
|
|
4201
6174
|
]),
|
|
4202
6175
|
);
|
|
4203
6176
|
});
|
|
4204
6177
|
|
|
4205
|
-
it("requires
|
|
6178
|
+
it("requires feature-level behavior evidence for reference inventory items", () => {
|
|
4206
6179
|
expect(
|
|
4207
|
-
validateToolcraftAcceptanceCoverage(
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
"
|
|
4212
|
-
"
|
|
6180
|
+
validateToolcraftAcceptanceCoverage(
|
|
6181
|
+
starterSchema,
|
|
6182
|
+
[
|
|
6183
|
+
...starterAcceptance,
|
|
6184
|
+
makeReferenceCoverageAcceptance("reference.canvasSizing", "canvas-sizing"),
|
|
6185
|
+
makeReferenceCoverageAcceptance("reference.controlMapping", "control-mapping"),
|
|
6186
|
+
makeReferenceCoverageAcceptance("reference.rendererState", "renderer-state"),
|
|
4213
6187
|
],
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
6188
|
+
{
|
|
6189
|
+
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
6190
|
+
mode: "reference-runtime-clone",
|
|
6191
|
+
referenceFeatureInventory: [
|
|
6192
|
+
{ ...makeReferenceFeatureInventory()[0], behaviorEvidence: "" },
|
|
6193
|
+
...makeReferenceFeatureInventory().slice(1),
|
|
6194
|
+
],
|
|
6195
|
+
referenceName: "legacy badge wall",
|
|
6196
|
+
referenceStudy: referenceStudyEvidence,
|
|
6197
|
+
referenceTimeline: { behaviorCoverage: [], mode: "none" },
|
|
6198
|
+
sourceOfTruth: "reference-runtime",
|
|
6199
|
+
},
|
|
6200
|
+
),
|
|
6201
|
+
).toContain(
|
|
6202
|
+
'referenceFeatureInventory "canvas-sizing" must include behaviorEvidence from the original, restored, or source-only reference study proving this feature was observed before Toolcraft mapping.',
|
|
6203
|
+
);
|
|
6204
|
+
});
|
|
6205
|
+
|
|
6206
|
+
it("requires explicit user evidence before marking reference behavior intentionally changed", () => {
|
|
6207
|
+
expect(
|
|
6208
|
+
validateToolcraftAcceptanceCoverage(
|
|
6209
|
+
starterSchema,
|
|
6210
|
+
[
|
|
6211
|
+
...starterAcceptance,
|
|
6212
|
+
makeReferenceCoverageAcceptance("reference.canvasSizing", "canvas-sizing"),
|
|
6213
|
+
makeReferenceCoverageAcceptance("reference.controlMapping", "control-mapping"),
|
|
6214
|
+
makeReferenceCoverageAcceptance("reference.rendererState", "renderer-state"),
|
|
6215
|
+
],
|
|
6216
|
+
{
|
|
6217
|
+
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
6218
|
+
mode: "reference-runtime-clone",
|
|
6219
|
+
referenceFeatureInventory: [
|
|
6220
|
+
{
|
|
6221
|
+
...makeReferenceFeatureInventory()[0],
|
|
6222
|
+
status: "intentionally-changed",
|
|
6223
|
+
userApprovedChangeReason: "Cleaner implementation.",
|
|
6224
|
+
},
|
|
6225
|
+
...makeReferenceFeatureInventory().slice(1),
|
|
6226
|
+
],
|
|
6227
|
+
referenceName: "legacy badge wall",
|
|
6228
|
+
referenceTimeline: { behaviorCoverage: [], mode: "none" },
|
|
6229
|
+
sourceOfTruth: "reference-runtime",
|
|
6230
|
+
},
|
|
6231
|
+
),
|
|
6232
|
+
).toContain(
|
|
6233
|
+
'referenceFeatureInventory "canvas-sizing" userApprovedChangeReason must cite explicit user approval or redesign/change-request evidence.',
|
|
4226
6234
|
);
|
|
4227
6235
|
});
|
|
4228
6236
|
|
|
@@ -4360,7 +6368,9 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4360
6368
|
{
|
|
4361
6369
|
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
4362
6370
|
mode: "reference-runtime-clone",
|
|
6371
|
+
referenceFeatureInventory: makeReferenceFeatureInventory(),
|
|
4363
6372
|
referenceName: "legacy badge wall",
|
|
6373
|
+
referenceStudy: referenceStudyEvidence,
|
|
4364
6374
|
referenceTimeline: { behaviorCoverage: [], mode: "none" },
|
|
4365
6375
|
sourceOfTruth: "reference-runtime",
|
|
4366
6376
|
},
|
|
@@ -4421,6 +6431,130 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4421
6431
|
);
|
|
4422
6432
|
});
|
|
4423
6433
|
|
|
6434
|
+
it("requires Toolcraft reference playback timelines to declare loop duration provenance", () => {
|
|
6435
|
+
const schemaWithPlaybackTimeline = {
|
|
6436
|
+
...starterSchema,
|
|
6437
|
+
panels: {
|
|
6438
|
+
...starterSchema.panels,
|
|
6439
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
6440
|
+
},
|
|
6441
|
+
};
|
|
6442
|
+
|
|
6443
|
+
expect(
|
|
6444
|
+
validateToolcraftAcceptanceCoverage(
|
|
6445
|
+
schemaWithPlaybackTimeline,
|
|
6446
|
+
[
|
|
6447
|
+
...starterAcceptance,
|
|
6448
|
+
{
|
|
6449
|
+
...playbackTimelineAcceptance,
|
|
6450
|
+
id: "reference.timeline.playback",
|
|
6451
|
+
referenceTimelineCoverage: "playback",
|
|
6452
|
+
},
|
|
6453
|
+
],
|
|
6454
|
+
{
|
|
6455
|
+
behaviorCoverage: [
|
|
6456
|
+
"canvas-sizing",
|
|
6457
|
+
"control-mapping",
|
|
6458
|
+
"renderer-state",
|
|
6459
|
+
"pause-resume",
|
|
6460
|
+
],
|
|
6461
|
+
mode: "reference-runtime-clone",
|
|
6462
|
+
referenceName: "legacy playback animation",
|
|
6463
|
+
referenceTimeline: { behaviorCoverage: ["playback"], mode: "toolcraft-playback" },
|
|
6464
|
+
sourceOfTruth: "reference-runtime",
|
|
6465
|
+
},
|
|
6466
|
+
),
|
|
6467
|
+
).toContain(
|
|
6468
|
+
'referenceTimeline mode "toolcraft-playback" must declare loopDuration with source, seconds, and evidence. Do not let runtime/template fallback duration such as 8s stand in for reference loop intent.',
|
|
6469
|
+
);
|
|
6470
|
+
});
|
|
6471
|
+
|
|
6472
|
+
it("requires Toolcraft reference timeline loop duration to match the timeline default", () => {
|
|
6473
|
+
const schemaWithPlaybackTimeline = {
|
|
6474
|
+
...starterSchema,
|
|
6475
|
+
panels: {
|
|
6476
|
+
...starterSchema.panels,
|
|
6477
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
6478
|
+
},
|
|
6479
|
+
};
|
|
6480
|
+
|
|
6481
|
+
expect(
|
|
6482
|
+
validateToolcraftAcceptanceCoverage(
|
|
6483
|
+
schemaWithPlaybackTimeline,
|
|
6484
|
+
[
|
|
6485
|
+
...starterAcceptance,
|
|
6486
|
+
{
|
|
6487
|
+
...playbackTimelineAcceptance,
|
|
6488
|
+
id: "reference.timeline.playback",
|
|
6489
|
+
referenceTimelineCoverage: "playback",
|
|
6490
|
+
},
|
|
6491
|
+
],
|
|
6492
|
+
{
|
|
6493
|
+
behaviorCoverage: [
|
|
6494
|
+
"canvas-sizing",
|
|
6495
|
+
"control-mapping",
|
|
6496
|
+
"renderer-state",
|
|
6497
|
+
"pause-resume",
|
|
6498
|
+
],
|
|
6499
|
+
mode: "reference-runtime-clone",
|
|
6500
|
+
referenceName: "legacy playback animation",
|
|
6501
|
+
referenceTimeline: {
|
|
6502
|
+
behaviorCoverage: ["playback"],
|
|
6503
|
+
loopDuration: {
|
|
6504
|
+
evidence:
|
|
6505
|
+
"The reference app completes one seamless forward playback cycle over 6s.",
|
|
6506
|
+
seconds: 6,
|
|
6507
|
+
source: "reference",
|
|
6508
|
+
},
|
|
6509
|
+
mode: "toolcraft-playback",
|
|
6510
|
+
},
|
|
6511
|
+
sourceOfTruth: "reference-runtime",
|
|
6512
|
+
},
|
|
6513
|
+
),
|
|
6514
|
+
).toContain(
|
|
6515
|
+
"panels.timeline.defaultDurationSeconds (8) must match referenceTimeline.loopDuration.seconds (6).",
|
|
6516
|
+
);
|
|
6517
|
+
});
|
|
6518
|
+
|
|
6519
|
+
it("requires Toolcraft reference keyframe timelines to declare loop duration provenance", () => {
|
|
6520
|
+
const schemaWithKeyframesTimeline = {
|
|
6521
|
+
...starterSchema,
|
|
6522
|
+
panels: {
|
|
6523
|
+
...starterSchema.panels,
|
|
6524
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "keyframes" as const },
|
|
6525
|
+
},
|
|
6526
|
+
};
|
|
6527
|
+
|
|
6528
|
+
expect(
|
|
6529
|
+
validateToolcraftAcceptanceCoverage(
|
|
6530
|
+
schemaWithKeyframesTimeline,
|
|
6531
|
+
[
|
|
6532
|
+
...starterAcceptance,
|
|
6533
|
+
playbackTimelineAcceptance,
|
|
6534
|
+
{
|
|
6535
|
+
...keyframesTimelineAcceptance,
|
|
6536
|
+
id: "reference.timeline.keyframes",
|
|
6537
|
+
referenceTimelineCoverage: "keyframes",
|
|
6538
|
+
},
|
|
6539
|
+
],
|
|
6540
|
+
{
|
|
6541
|
+
behaviorCoverage: [
|
|
6542
|
+
"canvas-sizing",
|
|
6543
|
+
"control-mapping",
|
|
6544
|
+
"renderer-state",
|
|
6545
|
+
"time-progress",
|
|
6546
|
+
],
|
|
6547
|
+
mode: "reference-runtime-clone",
|
|
6548
|
+
referenceName: "legacy keyframe animation",
|
|
6549
|
+
referenceTimeline: { behaviorCoverage: ["keyframes"], mode: "toolcraft-keyframes" },
|
|
6550
|
+
sourceOfTruth: "reference-runtime",
|
|
6551
|
+
},
|
|
6552
|
+
),
|
|
6553
|
+
).toContain(
|
|
6554
|
+
'referenceTimeline mode "toolcraft-keyframes" must declare loopDuration with source, seconds, and evidence. Do not let runtime/template fallback duration such as 8s stand in for reference loop intent.',
|
|
6555
|
+
);
|
|
6556
|
+
});
|
|
6557
|
+
|
|
4424
6558
|
it("rejects right-panel transport controls when timeline playback owns transport", () => {
|
|
4425
6559
|
const schemaWithPanelTransportControls = {
|
|
4426
6560
|
...starterSchema,
|
|
@@ -4457,7 +6591,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4457
6591
|
],
|
|
4458
6592
|
title: "Controls",
|
|
4459
6593
|
},
|
|
4460
|
-
timeline: { enabled: true, mode: "playback" as const },
|
|
6594
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
4461
6595
|
},
|
|
4462
6596
|
} as const;
|
|
4463
6597
|
|
|
@@ -4589,10 +6723,13 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4589
6723
|
});
|
|
4590
6724
|
|
|
4591
6725
|
it("accepts explicit autonomous animation intent for decorative self-running output", () => {
|
|
4592
|
-
const schemaWithAutonomousAnimation = {
|
|
4593
|
-
|
|
6726
|
+
const schemaWithAutonomousAnimation = defineToolcraft({
|
|
6727
|
+
canvas: {
|
|
6728
|
+
enabled: true,
|
|
6729
|
+
sizing: { mode: "editable-output" },
|
|
6730
|
+
upload: true,
|
|
6731
|
+
},
|
|
4594
6732
|
panels: {
|
|
4595
|
-
...starterSchema.panels,
|
|
4596
6733
|
controls: {
|
|
4597
6734
|
sections: [
|
|
4598
6735
|
{
|
|
@@ -4615,7 +6752,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4615
6752
|
},
|
|
4616
6753
|
timeline: undefined,
|
|
4617
6754
|
},
|
|
4618
|
-
|
|
6755
|
+
toolbar: {
|
|
6756
|
+
history: true,
|
|
6757
|
+
radar: true,
|
|
6758
|
+
zoom: true,
|
|
6759
|
+
},
|
|
6760
|
+
});
|
|
4619
6761
|
|
|
4620
6762
|
expect(
|
|
4621
6763
|
validateToolcraftAcceptanceCoverage(
|
|
@@ -4633,12 +6775,109 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4633
6775
|
],
|
|
4634
6776
|
mode: "autonomous",
|
|
4635
6777
|
reason:
|
|
4636
|
-
"The shimmer is decorative self-running output and does not expose product time transport.",
|
|
6778
|
+
"The shimmer is decorative self-running output and does not expose product time transport.",
|
|
6779
|
+
},
|
|
6780
|
+
mode: "new-toolcraft-app",
|
|
6781
|
+
},
|
|
6782
|
+
),
|
|
6783
|
+
).toEqual([]);
|
|
6784
|
+
});
|
|
6785
|
+
|
|
6786
|
+
it("requires a top timeline when video export exists", () => {
|
|
6787
|
+
const schemaWithVideoExportWithoutTimeline = defineToolcraft({
|
|
6788
|
+
canvas: {
|
|
6789
|
+
enabled: true,
|
|
6790
|
+
sizing: { mode: "editable-output" },
|
|
6791
|
+
},
|
|
6792
|
+
panels: {
|
|
6793
|
+
controls: {
|
|
6794
|
+
sections: [
|
|
6795
|
+
makeVideoExportSection(),
|
|
6796
|
+
{
|
|
6797
|
+
actionGroup: "secondary",
|
|
6798
|
+
controls: {
|
|
6799
|
+
outputActions: {
|
|
6800
|
+
actions: [
|
|
6801
|
+
{
|
|
6802
|
+
icon: "upload-simple",
|
|
6803
|
+
label: "Export Video",
|
|
6804
|
+
value: "export.video",
|
|
6805
|
+
},
|
|
6806
|
+
],
|
|
6807
|
+
target: "actions.output",
|
|
6808
|
+
type: "panelActions",
|
|
6809
|
+
},
|
|
6810
|
+
},
|
|
6811
|
+
title: "Export",
|
|
6812
|
+
},
|
|
6813
|
+
],
|
|
6814
|
+
title: "Controls",
|
|
6815
|
+
},
|
|
6816
|
+
},
|
|
6817
|
+
});
|
|
6818
|
+
|
|
6819
|
+
expect(validateToolcraftAcceptanceCoverage(schemaWithVideoExportWithoutTimeline)).toContain(
|
|
6820
|
+
'Apps with Export Video must enable the top Toolcraft timeline. Use panels.timeline mode "playback" for product animation transport, or mode "keyframes" when exported animation is driven by keyframes; autonomous no-timeline animation is only allowed when there is no video export.',
|
|
6821
|
+
);
|
|
6822
|
+
});
|
|
6823
|
+
|
|
6824
|
+
it("rejects autonomous animation intent when video export exists", () => {
|
|
6825
|
+
const schemaWithAutonomousVideoExport = defineToolcraft({
|
|
6826
|
+
canvas: {
|
|
6827
|
+
enabled: true,
|
|
6828
|
+
sizing: { mode: "editable-output" },
|
|
6829
|
+
},
|
|
6830
|
+
panels: {
|
|
6831
|
+
controls: {
|
|
6832
|
+
sections: [
|
|
6833
|
+
makeVideoExportSection(),
|
|
6834
|
+
{
|
|
6835
|
+
actionGroup: "secondary",
|
|
6836
|
+
controls: {
|
|
6837
|
+
outputActions: {
|
|
6838
|
+
actions: [
|
|
6839
|
+
{
|
|
6840
|
+
icon: "upload-simple",
|
|
6841
|
+
label: "Export Video",
|
|
6842
|
+
value: "export.video",
|
|
6843
|
+
},
|
|
6844
|
+
],
|
|
6845
|
+
target: "actions.output",
|
|
6846
|
+
type: "panelActions",
|
|
6847
|
+
},
|
|
6848
|
+
},
|
|
6849
|
+
title: "Export",
|
|
6850
|
+
},
|
|
6851
|
+
],
|
|
6852
|
+
title: "Controls",
|
|
6853
|
+
},
|
|
6854
|
+
},
|
|
6855
|
+
});
|
|
6856
|
+
|
|
6857
|
+
expect(
|
|
6858
|
+
validateToolcraftAcceptanceCoverage(
|
|
6859
|
+
schemaWithAutonomousVideoExport,
|
|
6860
|
+
[],
|
|
6861
|
+
{
|
|
6862
|
+
animationIntent: {
|
|
6863
|
+
behaviorCoverage: [
|
|
6864
|
+
"no-user-facing-transport",
|
|
6865
|
+
"no-play-pause",
|
|
6866
|
+
"no-scrub",
|
|
6867
|
+
"no-duration-control",
|
|
6868
|
+
"no-loop-control",
|
|
6869
|
+
"no-export-at-time",
|
|
6870
|
+
],
|
|
6871
|
+
mode: "autonomous",
|
|
6872
|
+
reason:
|
|
6873
|
+
"The shader is decorative self-running output with no transport controls.",
|
|
4637
6874
|
},
|
|
4638
6875
|
mode: "new-toolcraft-app",
|
|
4639
6876
|
},
|
|
4640
6877
|
),
|
|
4641
|
-
).
|
|
6878
|
+
).toContain(
|
|
6879
|
+
'starterTransferMode.animationIntent mode "autonomous" conflicts with Export Video. Video export creates product-time behavior, so the renderer and export must use the top Toolcraft timeline duration, loop, and deterministic timestamps.',
|
|
6880
|
+
);
|
|
4642
6881
|
});
|
|
4643
6882
|
|
|
4644
6883
|
it("rejects timeline animation intent when the timeline mode does not match", () => {
|
|
@@ -4646,7 +6885,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4646
6885
|
...starterSchema,
|
|
4647
6886
|
panels: {
|
|
4648
6887
|
...starterSchema.panels,
|
|
4649
|
-
timeline: { enabled: true, mode: "playback" as const },
|
|
6888
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
4650
6889
|
},
|
|
4651
6890
|
};
|
|
4652
6891
|
|
|
@@ -4655,7 +6894,10 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4655
6894
|
schemaWithPlaybackTimeline,
|
|
4656
6895
|
[playbackTimelineAcceptance],
|
|
4657
6896
|
{
|
|
4658
|
-
animationIntent: {
|
|
6897
|
+
animationIntent: {
|
|
6898
|
+
loopDuration: productDerivedEightSecondLoop,
|
|
6899
|
+
mode: "timeline-keyframes",
|
|
6900
|
+
},
|
|
4659
6901
|
mode: "new-toolcraft-app",
|
|
4660
6902
|
},
|
|
4661
6903
|
),
|
|
@@ -4771,7 +7013,36 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4771
7013
|
{
|
|
4772
7014
|
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
4773
7015
|
mode: "reference-runtime-clone",
|
|
7016
|
+
referenceFeatureInventory: makeReferenceFeatureInventory([
|
|
7017
|
+
{
|
|
7018
|
+
acceptanceId: "reference.timeline.stateJump",
|
|
7019
|
+
behaviorEvidence:
|
|
7020
|
+
"Clicked the reference timeline state buttons and observed discrete renderer state jumps.",
|
|
7021
|
+
featureName: "Timeline state jump",
|
|
7022
|
+
id: "timeline-state-jump",
|
|
7023
|
+
referenceBehavior:
|
|
7024
|
+
"The reference timeline state buttons jump to discrete renderer states.",
|
|
7025
|
+
sourceEvidence: "Inspected reference timeline state button handlers.",
|
|
7026
|
+
status: "ported",
|
|
7027
|
+
toolcraftMapping:
|
|
7028
|
+
"The custom reference timeline control keeps the same state-jump behavior.",
|
|
7029
|
+
},
|
|
7030
|
+
{
|
|
7031
|
+
acceptanceId: "reference.timeline.trimRange",
|
|
7032
|
+
behaviorEvidence:
|
|
7033
|
+
"Dragged the reference trim handles and observed the active playback/render range change.",
|
|
7034
|
+
featureName: "Timeline trim range",
|
|
7035
|
+
id: "timeline-trim-range",
|
|
7036
|
+
referenceBehavior:
|
|
7037
|
+
"The reference trim handles edit the active playback/render range.",
|
|
7038
|
+
sourceEvidence: "Inspected reference trim handle drag behavior.",
|
|
7039
|
+
status: "ported",
|
|
7040
|
+
toolcraftMapping:
|
|
7041
|
+
"The custom reference timeline control keeps the same trim range behavior.",
|
|
7042
|
+
},
|
|
7043
|
+
]),
|
|
4774
7044
|
referenceName: "legacy state timeline",
|
|
7045
|
+
referenceStudy: referenceStudyEvidence,
|
|
4775
7046
|
referenceTimeline: {
|
|
4776
7047
|
behaviorCoverage: ["state-jump", "trim-range"],
|
|
4777
7048
|
mode: "custom-reference-timeline",
|
|
@@ -5023,7 +7294,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5023
7294
|
}
|
|
5024
7295
|
});
|
|
5025
7296
|
|
|
5026
|
-
it("requires each browser acceptance entry to point at a Playwright test", () => {
|
|
7297
|
+
it("requires each browser acceptance entry to point at a fallback Playwright test", () => {
|
|
5027
7298
|
const browserTestSources = readBrowserTestSources();
|
|
5028
7299
|
|
|
5029
7300
|
for (const entry of starterAcceptance) {
|
|
@@ -5033,7 +7304,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5033
7304
|
|
|
5034
7305
|
expect(
|
|
5035
7306
|
browserTestSources,
|
|
5036
|
-
`${entry.id} must be backed by a Playwright test named "${entry.browserTestName}".`,
|
|
7307
|
+
`${entry.id} must be backed by a fallback Playwright test named "${entry.browserTestName}".`,
|
|
5037
7308
|
).toContain(entry.browserTestName);
|
|
5038
7309
|
|
|
5039
7310
|
if (entry.timelineCoverage === "playback" && acceptanceCoversTimelineDurationEdit(entry)) {
|
|
@@ -5110,10 +7381,13 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5110
7381
|
});
|
|
5111
7382
|
|
|
5112
7383
|
it("accepts stepped continuous sliders without forcing the discrete visual variant", () => {
|
|
5113
|
-
const schemaWithAmbiguousSlider = {
|
|
5114
|
-
|
|
7384
|
+
const schemaWithAmbiguousSlider = defineToolcraft({
|
|
7385
|
+
canvas: {
|
|
7386
|
+
enabled: true,
|
|
7387
|
+
sizing: { mode: "editable-output" },
|
|
7388
|
+
upload: true,
|
|
7389
|
+
},
|
|
5115
7390
|
panels: {
|
|
5116
|
-
...starterSchema.panels,
|
|
5117
7391
|
controls: {
|
|
5118
7392
|
sections: [
|
|
5119
7393
|
{
|
|
@@ -5136,7 +7410,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5136
7410
|
},
|
|
5137
7411
|
timeline: undefined,
|
|
5138
7412
|
},
|
|
5139
|
-
|
|
7413
|
+
toolbar: {
|
|
7414
|
+
history: true,
|
|
7415
|
+
radar: true,
|
|
7416
|
+
zoom: true,
|
|
7417
|
+
},
|
|
7418
|
+
});
|
|
5140
7419
|
|
|
5141
7420
|
expect(
|
|
5142
7421
|
validateToolcraftAcceptanceCoverage(schemaWithAmbiguousSlider, [
|
|
@@ -5252,7 +7531,6 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5252
7531
|
step: 0.1,
|
|
5253
7532
|
target: "animation.speed",
|
|
5254
7533
|
type: "slider",
|
|
5255
|
-
unit: "x",
|
|
5256
7534
|
variant: "continuous",
|
|
5257
7535
|
},
|
|
5258
7536
|
depth: {
|
|
@@ -5626,8 +7904,8 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5626
7904
|
controls: schema.panels.controls
|
|
5627
7905
|
? {
|
|
5628
7906
|
...schema.panels.controls,
|
|
5629
|
-
sections: schema.panels.controls.sections.map((section
|
|
5630
|
-
|
|
7907
|
+
sections: schema.panels.controls.sections.map((section) =>
|
|
7908
|
+
section.title === "Shader" ? { ...section, title: undefined } : section,
|
|
5631
7909
|
),
|
|
5632
7910
|
}
|
|
5633
7911
|
: schema.panels.controls,
|
|
@@ -5640,7 +7918,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5640
7918
|
]),
|
|
5641
7919
|
).toEqual(
|
|
5642
7920
|
expect.arrayContaining([
|
|
5643
|
-
"untitled section
|
|
7921
|
+
"untitled section 2 is missing a controls section title. Every visible controls-panel section must name the product entity, workflow stage, or behavior it edits.",
|
|
5644
7922
|
]),
|
|
5645
7923
|
);
|
|
5646
7924
|
});
|
|
@@ -6145,7 +8423,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6145
8423
|
]),
|
|
6146
8424
|
).toEqual(
|
|
6147
8425
|
expect.arrayContaining([
|
|
6148
|
-
'Controls for product entity "squares.right" are split across sections: Square 1 (Right), Appearance. Keep controls for the same product entity in one semantic section unless the
|
|
8426
|
+
'Controls for product entity "squares.right" are split across sections: Square 1 (Right), Appearance. Keep controls for the same product entity in one semantic section unless the Control Section Inventory declares workflowStage and splitReason for every split section.',
|
|
6149
8427
|
]),
|
|
6150
8428
|
);
|
|
6151
8429
|
});
|
|
@@ -6201,6 +8479,252 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6201
8479
|
).toEqual([]);
|
|
6202
8480
|
});
|
|
6203
8481
|
|
|
8482
|
+
it("rejects stale control section inventory entries", () => {
|
|
8483
|
+
const schemaWithInventoryMismatch = defineToolcraft({
|
|
8484
|
+
canvas: { enabled: true },
|
|
8485
|
+
panels: {
|
|
8486
|
+
controls: {
|
|
8487
|
+
sections: [
|
|
8488
|
+
{
|
|
8489
|
+
controls: {
|
|
8490
|
+
kind: {
|
|
8491
|
+
defaultValue: "soft",
|
|
8492
|
+
label: "Kind",
|
|
8493
|
+
options: [
|
|
8494
|
+
{ label: "Soft", value: "soft" },
|
|
8495
|
+
{ label: "Sharp", value: "sharp" },
|
|
8496
|
+
],
|
|
8497
|
+
orderRole: "mode",
|
|
8498
|
+
target: "shape.kind",
|
|
8499
|
+
type: "select",
|
|
8500
|
+
},
|
|
8501
|
+
count: {
|
|
8502
|
+
defaultValue: 12,
|
|
8503
|
+
label: "Count",
|
|
8504
|
+
max: 40,
|
|
8505
|
+
min: 1,
|
|
8506
|
+
orderRole: "detail",
|
|
8507
|
+
target: "shape.count",
|
|
8508
|
+
type: "slider",
|
|
8509
|
+
variant: "continuous",
|
|
8510
|
+
},
|
|
8511
|
+
},
|
|
8512
|
+
title: "Shape",
|
|
8513
|
+
},
|
|
8514
|
+
],
|
|
8515
|
+
title: "Controls",
|
|
8516
|
+
},
|
|
8517
|
+
},
|
|
8518
|
+
});
|
|
8519
|
+
|
|
8520
|
+
expect(
|
|
8521
|
+
validateToolcraftAcceptanceCoverage(
|
|
8522
|
+
schemaWithInventoryMismatch,
|
|
8523
|
+
[
|
|
8524
|
+
makeControlAcceptance("shape.kind", "select"),
|
|
8525
|
+
makeControlAcceptance("shape.count", "slider"),
|
|
8526
|
+
],
|
|
8527
|
+
starterTransferMode,
|
|
8528
|
+
[
|
|
8529
|
+
{
|
|
8530
|
+
entity: "Shape",
|
|
8531
|
+
groupingReason: "Shape setup",
|
|
8532
|
+
targets: ["shape.kind"],
|
|
8533
|
+
title: "Shape",
|
|
8534
|
+
},
|
|
8535
|
+
],
|
|
8536
|
+
),
|
|
8537
|
+
).toEqual(
|
|
8538
|
+
expect.arrayContaining([
|
|
8539
|
+
'Control Section Inventory entry "Shape" must include a concrete groupingReason explaining why these controls belong together.',
|
|
8540
|
+
'Control Section Inventory entry "Shape" is missing rendered target "shape.count". The inventory must cover every product control in the section.',
|
|
8541
|
+
]),
|
|
8542
|
+
);
|
|
8543
|
+
});
|
|
8544
|
+
|
|
8545
|
+
it("allows explicit workflow split evidence for one target entity", () => {
|
|
8546
|
+
const schemaWithWorkflowSplit = defineToolcraft({
|
|
8547
|
+
canvas: { enabled: true },
|
|
8548
|
+
panels: {
|
|
8549
|
+
controls: {
|
|
8550
|
+
sections: [
|
|
8551
|
+
{
|
|
8552
|
+
controls: {
|
|
8553
|
+
size: {
|
|
8554
|
+
defaultValue: 64,
|
|
8555
|
+
label: "Size",
|
|
8556
|
+
max: 128,
|
|
8557
|
+
min: 16,
|
|
8558
|
+
orderRole: "primary",
|
|
8559
|
+
target: "object.shape.size",
|
|
8560
|
+
type: "slider",
|
|
8561
|
+
unit: "px",
|
|
8562
|
+
variant: "continuous",
|
|
8563
|
+
},
|
|
8564
|
+
},
|
|
8565
|
+
title: "Shape Structure",
|
|
8566
|
+
},
|
|
8567
|
+
{
|
|
8568
|
+
controls: {
|
|
8569
|
+
count: {
|
|
8570
|
+
defaultValue: 12,
|
|
8571
|
+
label: "Count",
|
|
8572
|
+
max: 40,
|
|
8573
|
+
min: 1,
|
|
8574
|
+
orderRole: "detail",
|
|
8575
|
+
target: "object.shape.count",
|
|
8576
|
+
type: "slider",
|
|
8577
|
+
variant: "continuous",
|
|
8578
|
+
},
|
|
8579
|
+
},
|
|
8580
|
+
title: "Shape Density",
|
|
8581
|
+
},
|
|
8582
|
+
],
|
|
8583
|
+
title: "Controls",
|
|
8584
|
+
},
|
|
8585
|
+
},
|
|
8586
|
+
});
|
|
8587
|
+
|
|
8588
|
+
expect(
|
|
8589
|
+
validateToolcraftAcceptanceCoverage(
|
|
8590
|
+
schemaWithWorkflowSplit,
|
|
8591
|
+
[
|
|
8592
|
+
makeControlAcceptance("object.shape.size", "slider"),
|
|
8593
|
+
makeControlAcceptance("object.shape.count", "slider"),
|
|
8594
|
+
],
|
|
8595
|
+
starterTransferMode,
|
|
8596
|
+
[
|
|
8597
|
+
{
|
|
8598
|
+
entity: "Object shape",
|
|
8599
|
+
groupingReason: "Structure controls tune the physical footprint of the object.",
|
|
8600
|
+
splitReason:
|
|
8601
|
+
"Structure and density are separate workflow stages in this editor.",
|
|
8602
|
+
targets: ["object.shape.size"],
|
|
8603
|
+
title: "Shape Structure",
|
|
8604
|
+
workflowStage: "structure",
|
|
8605
|
+
},
|
|
8606
|
+
{
|
|
8607
|
+
entity: "Object shape",
|
|
8608
|
+
groupingReason: "Density controls tune how many objects appear after structure is set.",
|
|
8609
|
+
splitReason:
|
|
8610
|
+
"Density is separated from structure because users set count after the shape footprint.",
|
|
8611
|
+
targets: ["object.shape.count"],
|
|
8612
|
+
title: "Shape Density",
|
|
8613
|
+
workflowStage: "density",
|
|
8614
|
+
},
|
|
8615
|
+
],
|
|
8616
|
+
),
|
|
8617
|
+
).toEqual([]);
|
|
8618
|
+
});
|
|
8619
|
+
|
|
8620
|
+
it("rejects mode-gated controls split away from their selector section", () => {
|
|
8621
|
+
const schemaWithSplitModeBranch = defineToolcraft({
|
|
8622
|
+
canvas: { enabled: true },
|
|
8623
|
+
panels: {
|
|
8624
|
+
controls: {
|
|
8625
|
+
sections: [
|
|
8626
|
+
{
|
|
8627
|
+
controls: {
|
|
8628
|
+
sourceMode: {
|
|
8629
|
+
defaultValue: "preset",
|
|
8630
|
+
label: "Source",
|
|
8631
|
+
options: [
|
|
8632
|
+
{ label: "Preset", value: "preset" },
|
|
8633
|
+
{ label: "Image", value: "image" },
|
|
8634
|
+
],
|
|
8635
|
+
orderRole: "mode",
|
|
8636
|
+
target: "source.mode",
|
|
8637
|
+
type: "segmented",
|
|
8638
|
+
},
|
|
8639
|
+
},
|
|
8640
|
+
title: "Source",
|
|
8641
|
+
},
|
|
8642
|
+
{
|
|
8643
|
+
controls: {
|
|
8644
|
+
sourceUpload: {
|
|
8645
|
+
accept: "image/*",
|
|
8646
|
+
assetKind: "image",
|
|
8647
|
+
defaultValue: null,
|
|
8648
|
+
label: "Image",
|
|
8649
|
+
orderRole: "input",
|
|
8650
|
+
target: "source.upload",
|
|
8651
|
+
type: "fileDrop",
|
|
8652
|
+
visibleWhen: { equals: "image", target: "source.mode" },
|
|
8653
|
+
},
|
|
8654
|
+
},
|
|
8655
|
+
title: "Image",
|
|
8656
|
+
},
|
|
8657
|
+
],
|
|
8658
|
+
title: "Controls",
|
|
8659
|
+
},
|
|
8660
|
+
},
|
|
8661
|
+
});
|
|
8662
|
+
|
|
8663
|
+
expect(
|
|
8664
|
+
validateToolcraftAcceptanceCoverage(schemaWithSplitModeBranch, [
|
|
8665
|
+
makeControlAcceptance("source.mode", "segmented"),
|
|
8666
|
+
makeControlAcceptance("source.upload", "fileDrop"),
|
|
8667
|
+
]),
|
|
8668
|
+
).toEqual(
|
|
8669
|
+
expect.arrayContaining([
|
|
8670
|
+
'Image / sourceUpload is gated by visibleWhen target "source.mode" in Source, but it belongs to the same dependency group. Keep selectors and their dependent controls in one semantic section when they describe one product entity or branch; use visibleWhen for branch-specific controls inside that section instead of splitting branch controls into their own section. Do not use disabledWhen for product controls.',
|
|
8671
|
+
]),
|
|
8672
|
+
);
|
|
8673
|
+
});
|
|
8674
|
+
|
|
8675
|
+
it("rejects selector branch sections even when targets do not share a prefix", () => {
|
|
8676
|
+
const schemaWithSplitOptionBranch = defineToolcraft({
|
|
8677
|
+
canvas: { enabled: true },
|
|
8678
|
+
panels: {
|
|
8679
|
+
controls: {
|
|
8680
|
+
sections: [
|
|
8681
|
+
{
|
|
8682
|
+
controls: {
|
|
8683
|
+
shapeKind: {
|
|
8684
|
+
defaultValue: "generated",
|
|
8685
|
+
label: "Shape",
|
|
8686
|
+
options: [
|
|
8687
|
+
{ label: "Generated", value: "generated" },
|
|
8688
|
+
{ label: "Library", value: "library" },
|
|
8689
|
+
],
|
|
8690
|
+
orderRole: "mode",
|
|
8691
|
+
target: "shape.kind",
|
|
8692
|
+
type: "segmented",
|
|
8693
|
+
},
|
|
8694
|
+
},
|
|
8695
|
+
title: "Shape",
|
|
8696
|
+
},
|
|
8697
|
+
{
|
|
8698
|
+
controls: {
|
|
8699
|
+
libraryAsset: {
|
|
8700
|
+
defaultValue: null,
|
|
8701
|
+
label: "Library",
|
|
8702
|
+
orderRole: "input",
|
|
8703
|
+
target: "asset.upload",
|
|
8704
|
+
type: "fileDrop",
|
|
8705
|
+
visibleWhen: { equals: "library", target: "shape.kind" },
|
|
8706
|
+
},
|
|
8707
|
+
},
|
|
8708
|
+
title: "Library",
|
|
8709
|
+
},
|
|
8710
|
+
],
|
|
8711
|
+
title: "Controls",
|
|
8712
|
+
},
|
|
8713
|
+
},
|
|
8714
|
+
});
|
|
8715
|
+
|
|
8716
|
+
expect(
|
|
8717
|
+
validateToolcraftAcceptanceCoverage(schemaWithSplitOptionBranch, [
|
|
8718
|
+
makeControlAcceptance("shape.kind", "segmented"),
|
|
8719
|
+
makeControlAcceptance("asset.upload", "fileDrop"),
|
|
8720
|
+
]),
|
|
8721
|
+
).toEqual(
|
|
8722
|
+
expect.arrayContaining([
|
|
8723
|
+
'Library / libraryAsset is gated by visibleWhen target "shape.kind" in Shape, but it belongs to the same dependency group. Keep selectors and their dependent controls in one semantic section when they describe one product entity or branch; use visibleWhen for branch-specific controls inside that section instead of splitting branch controls into their own section. Do not use disabledWhen for product controls.',
|
|
8724
|
+
]),
|
|
8725
|
+
);
|
|
8726
|
+
});
|
|
8727
|
+
|
|
6204
8728
|
it("rejects splitting FontPicker-owned typography into sibling controls", () => {
|
|
6205
8729
|
const schemaWithSplitTypographyBlock = defineToolcraft({
|
|
6206
8730
|
canvas: { enabled: true },
|
|
@@ -6548,38 +9072,109 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6548
9072
|
],
|
|
6549
9073
|
title: "Controls",
|
|
6550
9074
|
},
|
|
6551
|
-
},
|
|
6552
|
-
});
|
|
6553
|
-
|
|
6554
|
-
expect(
|
|
6555
|
-
validateToolcraftAcceptanceCoverage(schemaWithUsefulColorLabels, [
|
|
6556
|
-
makeControlAcceptance("palette.accent1", "color"),
|
|
6557
|
-
makeControlAcceptance("palette.accent2", "color"),
|
|
6558
|
-
makeControlAcceptance("palette.spread", "slider"),
|
|
6559
|
-
makeControlAcceptance("object.fill", "color"),
|
|
6560
|
-
makeControlAcceptance("object.stroke", "color"),
|
|
9075
|
+
},
|
|
9076
|
+
});
|
|
9077
|
+
|
|
9078
|
+
expect(
|
|
9079
|
+
validateToolcraftAcceptanceCoverage(schemaWithUsefulColorLabels, [
|
|
9080
|
+
makeControlAcceptance("palette.accent1", "color"),
|
|
9081
|
+
makeControlAcceptance("palette.accent2", "color"),
|
|
9082
|
+
makeControlAcceptance("palette.spread", "slider"),
|
|
9083
|
+
makeControlAcceptance("object.fill", "color"),
|
|
9084
|
+
makeControlAcceptance("object.stroke", "color"),
|
|
9085
|
+
]),
|
|
9086
|
+
).toEqual([]);
|
|
9087
|
+
});
|
|
9088
|
+
|
|
9089
|
+
it("requires mode selectors to appear before dependent controls", () => {
|
|
9090
|
+
const schemaWithLateModeSelector = {
|
|
9091
|
+
...starterSchema,
|
|
9092
|
+
panels: {
|
|
9093
|
+
...starterSchema.panels,
|
|
9094
|
+
controls: {
|
|
9095
|
+
sections: [
|
|
9096
|
+
{
|
|
9097
|
+
controls: {
|
|
9098
|
+
depth: {
|
|
9099
|
+
defaultValue: 0.64,
|
|
9100
|
+
label: "Depth",
|
|
9101
|
+
max: 1,
|
|
9102
|
+
min: 0,
|
|
9103
|
+
target: "shader.depth",
|
|
9104
|
+
type: "slider",
|
|
9105
|
+
variant: "continuous",
|
|
9106
|
+
},
|
|
9107
|
+
mode: {
|
|
9108
|
+
defaultValue: "liquid",
|
|
9109
|
+
label: "Mode",
|
|
9110
|
+
options: [
|
|
9111
|
+
{ label: "Silk", value: "silk" },
|
|
9112
|
+
{ label: "Liquid", value: "liquid" },
|
|
9113
|
+
{ label: "Crystal", value: "crystal" },
|
|
9114
|
+
],
|
|
9115
|
+
target: "shader.mode",
|
|
9116
|
+
type: "segmented",
|
|
9117
|
+
},
|
|
9118
|
+
},
|
|
9119
|
+
title: "Volume",
|
|
9120
|
+
},
|
|
9121
|
+
],
|
|
9122
|
+
title: "Shader",
|
|
9123
|
+
},
|
|
9124
|
+
},
|
|
9125
|
+
};
|
|
9126
|
+
|
|
9127
|
+
expect(
|
|
9128
|
+
validateToolcraftAcceptanceCoverage(schemaWithLateModeSelector, [
|
|
9129
|
+
{
|
|
9130
|
+
automated: true,
|
|
9131
|
+
automatedTestName: "depth changes rendered output",
|
|
9132
|
+
browser: true,
|
|
9133
|
+
browserTestName: "browser: depth slider changes rendered output",
|
|
9134
|
+
componentType: "slider",
|
|
9135
|
+
evidence: "rendered-pixels",
|
|
9136
|
+
expectedObservable: "Changing Depth changes shader contrast.",
|
|
9137
|
+
fixture: "depth fixture",
|
|
9138
|
+
id: "shader.depth",
|
|
9139
|
+
kind: "control",
|
|
9140
|
+
target: "shader.depth",
|
|
9141
|
+
userAction: "Drag the Depth slider.",
|
|
9142
|
+
},
|
|
9143
|
+
{
|
|
9144
|
+
automated: true,
|
|
9145
|
+
automatedTestName: "mode changes rendered output",
|
|
9146
|
+
browser: true,
|
|
9147
|
+
browserTestName: "browser: mode selector changes rendered output",
|
|
9148
|
+
componentType: "segmented",
|
|
9149
|
+
evidence: "rendered-pixels",
|
|
9150
|
+
expectedObservable: "Changing Mode switches shader pattern.",
|
|
9151
|
+
fixture: "mode fixture",
|
|
9152
|
+
id: "shader.mode",
|
|
9153
|
+
kind: "control",
|
|
9154
|
+
optionCoverage: "each-visible-item",
|
|
9155
|
+
target: "shader.mode",
|
|
9156
|
+
userAction: "Select each Mode option.",
|
|
9157
|
+
},
|
|
6561
9158
|
]),
|
|
6562
|
-
).toEqual(
|
|
9159
|
+
).toEqual(
|
|
9160
|
+
expect.arrayContaining([
|
|
9161
|
+
'Volume / mode (shader.mode) has orderRole "mode" after depth (shader.depth) with orderRole "strength". Move mode/input/primary controls before dependent strength/detail/advanced controls or split them into an earlier section.',
|
|
9162
|
+
]),
|
|
9163
|
+
);
|
|
6563
9164
|
});
|
|
6564
9165
|
|
|
6565
|
-
it("
|
|
6566
|
-
const
|
|
6567
|
-
|
|
9166
|
+
it("accepts explicit order roles when selectors lead dependent controls", () => {
|
|
9167
|
+
const schemaWithOrderedControls = defineToolcraft({
|
|
9168
|
+
canvas: {
|
|
9169
|
+
enabled: true,
|
|
9170
|
+
sizing: { mode: "editable-output" },
|
|
9171
|
+
upload: true,
|
|
9172
|
+
},
|
|
6568
9173
|
panels: {
|
|
6569
|
-
...starterSchema.panels,
|
|
6570
9174
|
controls: {
|
|
6571
9175
|
sections: [
|
|
6572
9176
|
{
|
|
6573
9177
|
controls: {
|
|
6574
|
-
depth: {
|
|
6575
|
-
defaultValue: 0.64,
|
|
6576
|
-
label: "Depth",
|
|
6577
|
-
max: 1,
|
|
6578
|
-
min: 0,
|
|
6579
|
-
target: "shader.depth",
|
|
6580
|
-
type: "slider",
|
|
6581
|
-
variant: "continuous",
|
|
6582
|
-
},
|
|
6583
9178
|
blend: {
|
|
6584
9179
|
defaultValue: "liquid",
|
|
6585
9180
|
label: "Blend",
|
|
@@ -6588,9 +9183,20 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6588
9183
|
{ label: "Liquid", value: "liquid" },
|
|
6589
9184
|
{ label: "Crystal", value: "crystal" },
|
|
6590
9185
|
],
|
|
9186
|
+
orderRole: "mode" as const,
|
|
6591
9187
|
target: "shader.blend",
|
|
6592
9188
|
type: "segmented",
|
|
6593
9189
|
},
|
|
9190
|
+
depth: {
|
|
9191
|
+
defaultValue: 0.64,
|
|
9192
|
+
label: "Depth",
|
|
9193
|
+
max: 1,
|
|
9194
|
+
min: 0,
|
|
9195
|
+
orderRole: "strength" as const,
|
|
9196
|
+
target: "shader.depth",
|
|
9197
|
+
type: "slider",
|
|
9198
|
+
variant: "continuous",
|
|
9199
|
+
},
|
|
6594
9200
|
},
|
|
6595
9201
|
title: "Volume",
|
|
6596
9202
|
},
|
|
@@ -6598,10 +9204,34 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6598
9204
|
title: "Shader",
|
|
6599
9205
|
},
|
|
6600
9206
|
},
|
|
6601
|
-
|
|
9207
|
+
toolbar: {
|
|
9208
|
+
history: true,
|
|
9209
|
+
radar: true,
|
|
9210
|
+
zoom: true,
|
|
9211
|
+
},
|
|
9212
|
+
});
|
|
6602
9213
|
|
|
9214
|
+
expect(getToolcraftControlOrderTargets(schemaWithOrderedControls)).toEqual([
|
|
9215
|
+
"shader.blend",
|
|
9216
|
+
"shader.depth",
|
|
9217
|
+
]);
|
|
6603
9218
|
expect(
|
|
6604
|
-
validateToolcraftAcceptanceCoverage(
|
|
9219
|
+
validateToolcraftAcceptanceCoverage(schemaWithOrderedControls, [
|
|
9220
|
+
{
|
|
9221
|
+
automated: true,
|
|
9222
|
+
automatedTestName: "blend changes rendered output",
|
|
9223
|
+
browser: true,
|
|
9224
|
+
browserTestName: "browser: blend selector changes rendered output",
|
|
9225
|
+
componentType: "segmented",
|
|
9226
|
+
evidence: "rendered-pixels",
|
|
9227
|
+
expectedObservable: "Changing Blend switches shader pattern.",
|
|
9228
|
+
fixture: "blend fixture",
|
|
9229
|
+
id: "shader.blend",
|
|
9230
|
+
kind: "control",
|
|
9231
|
+
optionCoverage: "each-visible-item",
|
|
9232
|
+
target: "shader.blend",
|
|
9233
|
+
userAction: "Select each Blend option.",
|
|
9234
|
+
},
|
|
6605
9235
|
{
|
|
6606
9236
|
automated: true,
|
|
6607
9237
|
automatedTestName: "depth changes rendered output",
|
|
@@ -6616,188 +9246,484 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6616
9246
|
target: "shader.depth",
|
|
6617
9247
|
userAction: "Drag the Depth slider.",
|
|
6618
9248
|
},
|
|
9249
|
+
]),
|
|
9250
|
+
).toEqual([]);
|
|
9251
|
+
});
|
|
9252
|
+
|
|
9253
|
+
it("requires overwide segmented controls to shorten labels or use select", () => {
|
|
9254
|
+
const schemaWithOverwideSegmentedControl = {
|
|
9255
|
+
...starterSchema,
|
|
9256
|
+
panels: {
|
|
9257
|
+
...starterSchema.panels,
|
|
9258
|
+
controls: {
|
|
9259
|
+
sections: [
|
|
9260
|
+
{
|
|
9261
|
+
controls: {
|
|
9262
|
+
preset: {
|
|
9263
|
+
defaultValue: "full-stack",
|
|
9264
|
+
label: "FX Preset",
|
|
9265
|
+
options: [
|
|
9266
|
+
{ label: "Full Stack", value: "full-stack" },
|
|
9267
|
+
{ label: "RGB Split", value: "rgb-split" },
|
|
9268
|
+
{ label: "Shade", value: "shade" },
|
|
9269
|
+
{ label: "Lines", value: "lines" },
|
|
9270
|
+
{ label: "Off", value: "off" },
|
|
9271
|
+
],
|
|
9272
|
+
orderRole: "mode" as const,
|
|
9273
|
+
target: "shader.fxPreset",
|
|
9274
|
+
type: "segmented",
|
|
9275
|
+
},
|
|
9276
|
+
},
|
|
9277
|
+
title: "Style",
|
|
9278
|
+
},
|
|
9279
|
+
],
|
|
9280
|
+
title: "Shader",
|
|
9281
|
+
},
|
|
9282
|
+
},
|
|
9283
|
+
};
|
|
9284
|
+
|
|
9285
|
+
expect(
|
|
9286
|
+
validateToolcraftAcceptanceCoverage(schemaWithOverwideSegmentedControl, [
|
|
6619
9287
|
{
|
|
6620
9288
|
automated: true,
|
|
6621
|
-
automatedTestName: "
|
|
9289
|
+
automatedTestName: "fx preset changes rendered output",
|
|
6622
9290
|
browser: true,
|
|
6623
|
-
browserTestName: "browser:
|
|
9291
|
+
browserTestName: "browser: fx preset selector changes rendered output",
|
|
6624
9292
|
componentType: "segmented",
|
|
6625
9293
|
evidence: "rendered-pixels",
|
|
6626
|
-
expectedObservable: "Changing
|
|
6627
|
-
fixture: "
|
|
6628
|
-
id: "shader.
|
|
9294
|
+
expectedObservable: "Changing FX Preset switches the shader preset.",
|
|
9295
|
+
fixture: "fx preset fixture",
|
|
9296
|
+
id: "shader.fxPreset",
|
|
6629
9297
|
kind: "control",
|
|
6630
9298
|
optionCoverage: "each-visible-item",
|
|
6631
|
-
target: "shader.
|
|
6632
|
-
userAction: "Select each
|
|
9299
|
+
target: "shader.fxPreset",
|
|
9300
|
+
userAction: "Select each FX Preset option.",
|
|
6633
9301
|
},
|
|
6634
9302
|
]),
|
|
6635
9303
|
).toEqual(
|
|
6636
9304
|
expect.arrayContaining([
|
|
6637
|
-
|
|
9305
|
+
"Style / preset (shader.fxPreset) segmented controls must preserve cell padding: use at most 4 short options (max 9 characters per label and 24 total) or shorten labels first; if the compact names still exceed the budget, use a select dropdown instead.",
|
|
9306
|
+
]),
|
|
9307
|
+
);
|
|
9308
|
+
});
|
|
9309
|
+
|
|
9310
|
+
it("requires timeline playback coverage when a playback timeline is enabled", () => {
|
|
9311
|
+
const schemaWithPlaybackTimeline = {
|
|
9312
|
+
...starterSchema,
|
|
9313
|
+
panels: {
|
|
9314
|
+
...starterSchema.panels,
|
|
9315
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
9316
|
+
},
|
|
9317
|
+
};
|
|
9318
|
+
|
|
9319
|
+
expect(validateToolcraftAcceptanceCoverage(schemaWithPlaybackTimeline, [])).toEqual(
|
|
9320
|
+
expect.arrayContaining([
|
|
9321
|
+
'panels.timeline mode "playback" requires a runtime acceptance entry with timelineCoverage "playback" proving pause, scrub, duration/loop, and rendered-frame behavior.',
|
|
9322
|
+
]),
|
|
9323
|
+
);
|
|
9324
|
+
});
|
|
9325
|
+
|
|
9326
|
+
it("requires playback timeline coverage to prove duration drives renderer progress", () => {
|
|
9327
|
+
const schemaWithPlaybackTimeline = {
|
|
9328
|
+
...starterSchema,
|
|
9329
|
+
panels: {
|
|
9330
|
+
...starterSchema.panels,
|
|
9331
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
9332
|
+
},
|
|
9333
|
+
};
|
|
9334
|
+
|
|
9335
|
+
expect(
|
|
9336
|
+
validateToolcraftAcceptanceCoverage(schemaWithPlaybackTimeline, [
|
|
9337
|
+
{
|
|
9338
|
+
automated: true,
|
|
9339
|
+
automatedTestName: "timeline playback controls drive rendered output",
|
|
9340
|
+
browser: true,
|
|
9341
|
+
browserTestName: "browser: timeline playback controls drive rendered output",
|
|
9342
|
+
componentType: "timeline",
|
|
9343
|
+
evidence: "timeline-output",
|
|
9344
|
+
expectedObservable:
|
|
9345
|
+
"Pause, scrub, and playback update visible renderer output.",
|
|
9346
|
+
fixture: "timeline playback fixture",
|
|
9347
|
+
id: "timeline.playback",
|
|
9348
|
+
kind: "runtime",
|
|
9349
|
+
target: "timeline.playback",
|
|
9350
|
+
timelineCoverage: "playback",
|
|
9351
|
+
timelinePlaybackCoverage: ["pause-resume", "scrub", "rendered-frame"],
|
|
9352
|
+
userAction: "Pause, scrub, and resume timeline playback.",
|
|
9353
|
+
},
|
|
6638
9354
|
]),
|
|
9355
|
+
).toContain(
|
|
9356
|
+
'timeline.playback timelineCoverage "playback" must declare timelinePlaybackCoverage for pause-resume, scrub, duration, loop, and rendered-frame. Duration coverage must prove renderer progress maps 0..state.timeline.durationSeconds, not a local fixed animation duration.',
|
|
9357
|
+
);
|
|
9358
|
+
});
|
|
9359
|
+
|
|
9360
|
+
it("requires timeline animation intent to declare loop duration provenance", () => {
|
|
9361
|
+
const schemaWithPlaybackTimeline = {
|
|
9362
|
+
...starterSchema,
|
|
9363
|
+
panels: {
|
|
9364
|
+
...starterSchema.panels,
|
|
9365
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
9366
|
+
},
|
|
9367
|
+
};
|
|
9368
|
+
|
|
9369
|
+
expect(
|
|
9370
|
+
validateToolcraftAcceptanceCoverage(
|
|
9371
|
+
schemaWithPlaybackTimeline,
|
|
9372
|
+
[playbackTimelineAcceptance],
|
|
9373
|
+
{
|
|
9374
|
+
animationIntent: { mode: "timeline-playback" },
|
|
9375
|
+
mode: "new-toolcraft-app",
|
|
9376
|
+
} as unknown as ToolcraftTransferMode,
|
|
9377
|
+
),
|
|
9378
|
+
).toContain(
|
|
9379
|
+
'starterTransferMode.animationIntent mode "timeline-playback" must declare loopDuration with source, seconds, and evidence. Do not let runtime/template fallback duration such as 8s stand in for product loop intent.',
|
|
9380
|
+
);
|
|
9381
|
+
});
|
|
9382
|
+
|
|
9383
|
+
it("requires playback timeline apps to declare matching playback animation intent", () => {
|
|
9384
|
+
const schemaWithPlaybackTimeline = {
|
|
9385
|
+
...starterSchema,
|
|
9386
|
+
panels: {
|
|
9387
|
+
...starterSchema.panels,
|
|
9388
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
9389
|
+
},
|
|
9390
|
+
};
|
|
9391
|
+
|
|
9392
|
+
expect(
|
|
9393
|
+
validateToolcraftAcceptanceCoverage(
|
|
9394
|
+
schemaWithPlaybackTimeline,
|
|
9395
|
+
[playbackTimelineAcceptance],
|
|
9396
|
+
{
|
|
9397
|
+
animationIntent: { mode: "none" },
|
|
9398
|
+
mode: "new-toolcraft-app",
|
|
9399
|
+
},
|
|
9400
|
+
),
|
|
9401
|
+
).toContain(
|
|
9402
|
+
'panels.timeline mode "playback" requires starterTransferMode.animationIntent mode "timeline-playback" with loopDuration provenance.',
|
|
9403
|
+
);
|
|
9404
|
+
});
|
|
9405
|
+
|
|
9406
|
+
it("requires keyframe timeline apps to declare matching keyframe animation intent", () => {
|
|
9407
|
+
const schemaWithKeyframesTimeline = {
|
|
9408
|
+
...starterSchema,
|
|
9409
|
+
panels: {
|
|
9410
|
+
...starterSchema.panels,
|
|
9411
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "keyframes" as const },
|
|
9412
|
+
},
|
|
9413
|
+
};
|
|
9414
|
+
|
|
9415
|
+
expect(
|
|
9416
|
+
validateToolcraftAcceptanceCoverage(
|
|
9417
|
+
schemaWithKeyframesTimeline,
|
|
9418
|
+
[playbackTimelineAcceptance],
|
|
9419
|
+
{
|
|
9420
|
+
animationIntent: { mode: "none" },
|
|
9421
|
+
mode: "new-toolcraft-app",
|
|
9422
|
+
},
|
|
9423
|
+
),
|
|
9424
|
+
).toContain(
|
|
9425
|
+
'panels.timeline mode "keyframes" requires starterTransferMode.animationIntent mode "timeline-keyframes" with loopDuration provenance.',
|
|
9426
|
+
);
|
|
9427
|
+
});
|
|
9428
|
+
|
|
9429
|
+
it("requires declared loop duration to match timeline default duration", () => {
|
|
9430
|
+
const schemaWithPlaybackTimeline = {
|
|
9431
|
+
...starterSchema,
|
|
9432
|
+
panels: {
|
|
9433
|
+
...starterSchema.panels,
|
|
9434
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
9435
|
+
},
|
|
9436
|
+
};
|
|
9437
|
+
|
|
9438
|
+
expect(
|
|
9439
|
+
validateToolcraftAcceptanceCoverage(
|
|
9440
|
+
schemaWithPlaybackTimeline,
|
|
9441
|
+
[playbackTimelineAcceptance],
|
|
9442
|
+
{
|
|
9443
|
+
animationIntent: {
|
|
9444
|
+
loopDuration: {
|
|
9445
|
+
evidence:
|
|
9446
|
+
"The product timing model derives a six second forward animation cycle from the authored baseline.",
|
|
9447
|
+
seconds: 6,
|
|
9448
|
+
source: "product-derived",
|
|
9449
|
+
},
|
|
9450
|
+
mode: "timeline-playback",
|
|
9451
|
+
},
|
|
9452
|
+
mode: "new-toolcraft-app",
|
|
9453
|
+
},
|
|
9454
|
+
),
|
|
9455
|
+
).toContain(
|
|
9456
|
+
"panels.timeline.defaultDurationSeconds (8) must match starterTransferMode.animationIntent.loopDuration.seconds (6).",
|
|
6639
9457
|
);
|
|
6640
9458
|
});
|
|
6641
9459
|
|
|
6642
|
-
it("
|
|
6643
|
-
const
|
|
9460
|
+
it("rejects runtime fallback evidence as a loop duration source", () => {
|
|
9461
|
+
const schemaWithPlaybackTimeline = {
|
|
6644
9462
|
...starterSchema,
|
|
6645
9463
|
panels: {
|
|
6646
9464
|
...starterSchema.panels,
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
type: "segmented",
|
|
6662
|
-
},
|
|
6663
|
-
depth: {
|
|
6664
|
-
defaultValue: 0.64,
|
|
6665
|
-
label: "Depth",
|
|
6666
|
-
max: 1,
|
|
6667
|
-
min: 0,
|
|
6668
|
-
orderRole: "strength" as const,
|
|
6669
|
-
target: "shader.depth",
|
|
6670
|
-
type: "slider",
|
|
6671
|
-
variant: "continuous",
|
|
6672
|
-
},
|
|
6673
|
-
},
|
|
6674
|
-
title: "Volume",
|
|
9465
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
9466
|
+
},
|
|
9467
|
+
};
|
|
9468
|
+
|
|
9469
|
+
expect(
|
|
9470
|
+
validateToolcraftAcceptanceCoverage(
|
|
9471
|
+
schemaWithPlaybackTimeline,
|
|
9472
|
+
[playbackTimelineAcceptance],
|
|
9473
|
+
{
|
|
9474
|
+
animationIntent: {
|
|
9475
|
+
loopDuration: {
|
|
9476
|
+
evidence: "Use the runtime default 8s fallback because Toolcraft starts there.",
|
|
9477
|
+
seconds: 8,
|
|
9478
|
+
source: "product-derived",
|
|
6675
9479
|
},
|
|
6676
|
-
|
|
6677
|
-
|
|
9480
|
+
mode: "timeline-playback",
|
|
9481
|
+
},
|
|
9482
|
+
mode: "new-toolcraft-app",
|
|
6678
9483
|
},
|
|
9484
|
+
),
|
|
9485
|
+
).toContain(
|
|
9486
|
+
"starterTransferMode.animationIntent.loopDuration.evidence must not cite the runtime/template fallback 8s default as the product loop source. Use reference timing, an explicit user request, or a product-derived timing rule.",
|
|
9487
|
+
);
|
|
9488
|
+
});
|
|
9489
|
+
|
|
9490
|
+
it("requires video reference study when a new app cites video reference evidence", () => {
|
|
9491
|
+
const schemaWithPlaybackTimeline = {
|
|
9492
|
+
...starterSchema,
|
|
9493
|
+
panels: {
|
|
9494
|
+
...starterSchema.panels,
|
|
9495
|
+
timeline: { defaultDurationSeconds: 5.8, enabled: true, mode: "playback" as const },
|
|
6679
9496
|
},
|
|
6680
9497
|
};
|
|
6681
9498
|
|
|
6682
|
-
expect(getToolcraftControlOrderTargets(schemaWithOrderedControls)).toEqual([
|
|
6683
|
-
"shader.blend",
|
|
6684
|
-
"shader.depth",
|
|
6685
|
-
]);
|
|
6686
9499
|
expect(
|
|
6687
|
-
validateToolcraftAcceptanceCoverage(
|
|
6688
|
-
|
|
9500
|
+
validateToolcraftAcceptanceCoverage(
|
|
9501
|
+
schemaWithPlaybackTimeline,
|
|
9502
|
+
[playbackTimelineAcceptance],
|
|
6689
9503
|
{
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
optionCoverage: "each-visible-item",
|
|
6701
|
-
target: "shader.blend",
|
|
6702
|
-
userAction: "Select each Blend option.",
|
|
9504
|
+
animationIntent: {
|
|
9505
|
+
loopDuration: {
|
|
9506
|
+
evidence:
|
|
9507
|
+
"The source reference video /fixtures/reference-motion/ref.mp4 was inspected for motion behavior.",
|
|
9508
|
+
seconds: 5.8,
|
|
9509
|
+
source: "reference",
|
|
9510
|
+
},
|
|
9511
|
+
mode: "timeline-playback",
|
|
9512
|
+
},
|
|
9513
|
+
mode: "new-toolcraft-app",
|
|
6703
9514
|
},
|
|
9515
|
+
),
|
|
9516
|
+
).toContain(
|
|
9517
|
+
"starterTransferMode cites a video reference, screen recording, GIF, extracted frames, or contact sheet; declare videoReferenceStudy with storyboard frames, frame-to-frame transition analysis, behavior decomposition, and acceptance mapping before implementation.",
|
|
9518
|
+
);
|
|
9519
|
+
});
|
|
9520
|
+
|
|
9521
|
+
it("does not require video reference study for ordinary video export intent", () => {
|
|
9522
|
+
const schemaWithPlaybackTimeline = defineToolcraft({
|
|
9523
|
+
canvas: {
|
|
9524
|
+
enabled: true,
|
|
9525
|
+
upload: true,
|
|
9526
|
+
},
|
|
9527
|
+
panels: {
|
|
9528
|
+
controls: {
|
|
9529
|
+
sections: [],
|
|
9530
|
+
title: "Controls",
|
|
9531
|
+
},
|
|
9532
|
+
timeline: { defaultDurationSeconds: 6, enabled: true, mode: "playback" as const },
|
|
9533
|
+
},
|
|
9534
|
+
toolbar: {
|
|
9535
|
+
history: true,
|
|
9536
|
+
radar: true,
|
|
9537
|
+
zoom: true,
|
|
9538
|
+
},
|
|
9539
|
+
});
|
|
9540
|
+
|
|
9541
|
+
expect(
|
|
9542
|
+
validateToolcraftAcceptanceCoverage(
|
|
9543
|
+
schemaWithPlaybackTimeline,
|
|
9544
|
+
[playbackTimelineAcceptance],
|
|
6704
9545
|
{
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
target: "shader.depth",
|
|
6716
|
-
userAction: "Drag the Depth slider.",
|
|
9546
|
+
animationIntent: {
|
|
9547
|
+
loopDuration: {
|
|
9548
|
+
evidence:
|
|
9549
|
+
"The product timing model derives a six second cycle for exported video output.",
|
|
9550
|
+
seconds: 6,
|
|
9551
|
+
source: "product-derived",
|
|
9552
|
+
},
|
|
9553
|
+
mode: "timeline-playback",
|
|
9554
|
+
},
|
|
9555
|
+
mode: "new-toolcraft-app",
|
|
6717
9556
|
},
|
|
6718
|
-
|
|
9557
|
+
),
|
|
6719
9558
|
).toEqual([]);
|
|
6720
9559
|
});
|
|
6721
9560
|
|
|
6722
|
-
it("
|
|
6723
|
-
const
|
|
6724
|
-
|
|
9561
|
+
it("accepts a new app video reference only with storyboard and transition acceptance mapping", () => {
|
|
9562
|
+
const schemaWithPlaybackTimeline = defineToolcraft({
|
|
9563
|
+
canvas: {
|
|
9564
|
+
enabled: true,
|
|
9565
|
+
upload: true,
|
|
9566
|
+
},
|
|
6725
9567
|
panels: {
|
|
6726
|
-
...starterSchema.panels,
|
|
6727
9568
|
controls: {
|
|
6728
|
-
sections: [
|
|
9569
|
+
sections: [],
|
|
9570
|
+
title: "Controls",
|
|
9571
|
+
},
|
|
9572
|
+
timeline: { defaultDurationSeconds: 5.8, enabled: true, mode: "playback" as const },
|
|
9573
|
+
},
|
|
9574
|
+
toolbar: {
|
|
9575
|
+
history: true,
|
|
9576
|
+
radar: true,
|
|
9577
|
+
zoom: true,
|
|
9578
|
+
},
|
|
9579
|
+
});
|
|
9580
|
+
|
|
9581
|
+
expect(
|
|
9582
|
+
validateToolcraftAcceptanceCoverage(
|
|
9583
|
+
schemaWithPlaybackTimeline,
|
|
9584
|
+
[playbackTimelineAcceptance, videoReferenceMotionAcceptance],
|
|
9585
|
+
{
|
|
9586
|
+
animationIntent: {
|
|
9587
|
+
loopDuration: {
|
|
9588
|
+
evidence:
|
|
9589
|
+
"The source reference timing comes from the inspected motion reference asset.",
|
|
9590
|
+
seconds: 5.8,
|
|
9591
|
+
source: "reference",
|
|
9592
|
+
},
|
|
9593
|
+
mode: "timeline-playback",
|
|
9594
|
+
},
|
|
9595
|
+
mode: "new-toolcraft-app",
|
|
9596
|
+
videoReferenceStudy: videoReferenceStudyEvidence,
|
|
9597
|
+
},
|
|
9598
|
+
),
|
|
9599
|
+
).toEqual([]);
|
|
9600
|
+
});
|
|
9601
|
+
|
|
9602
|
+
it("rejects incomplete video reference studies", () => {
|
|
9603
|
+
expect(
|
|
9604
|
+
validateToolcraftAcceptanceCoverage(starterSchema, [], {
|
|
9605
|
+
mode: "new-toolcraft-app",
|
|
9606
|
+
videoReferenceStudy: {
|
|
9607
|
+
acceptanceMapping: [
|
|
6729
9608
|
{
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
label: "FX Preset",
|
|
6734
|
-
options: [
|
|
6735
|
-
{ label: "Full Stack", value: "full-stack" },
|
|
6736
|
-
{ label: "RGB Split", value: "rgb-split" },
|
|
6737
|
-
{ label: "Shade", value: "shade" },
|
|
6738
|
-
{ label: "Lines", value: "lines" },
|
|
6739
|
-
{ label: "Off", value: "off" },
|
|
6740
|
-
],
|
|
6741
|
-
orderRole: "mode" as const,
|
|
6742
|
-
target: "shader.fxPreset",
|
|
6743
|
-
type: "segmented",
|
|
6744
|
-
},
|
|
6745
|
-
},
|
|
6746
|
-
title: "Style",
|
|
9609
|
+
acceptanceId: "missing.acceptance",
|
|
9610
|
+
behavior: "Motion behavior",
|
|
9611
|
+
frameIds: ["f001"],
|
|
6747
9612
|
},
|
|
6748
9613
|
],
|
|
6749
|
-
|
|
9614
|
+
behaviorDecomposition: "",
|
|
9615
|
+
extractionEvidence: "",
|
|
9616
|
+
referenceLocation: "/fixtures/reference-motion/ref.mp4",
|
|
9617
|
+
storyboard: [
|
|
9618
|
+
{
|
|
9619
|
+
behaviorObservation: "Only one frame was inspected.",
|
|
9620
|
+
frameId: "f001",
|
|
9621
|
+
frameSource: "frames/frame_001.png",
|
|
9622
|
+
timeSeconds: 0,
|
|
9623
|
+
visualObservation: "A single static state.",
|
|
9624
|
+
},
|
|
9625
|
+
],
|
|
9626
|
+
transitionAnalysis: [],
|
|
6750
9627
|
},
|
|
9628
|
+
}),
|
|
9629
|
+
).toEqual(
|
|
9630
|
+
expect.arrayContaining([
|
|
9631
|
+
"videoReferenceStudy.extractionEvidence must explain how frames were inspected or extracted before implementation.",
|
|
9632
|
+
"videoReferenceStudy.behaviorDecomposition must decompose the observed frame-to-frame changes into product behavior to preserve.",
|
|
9633
|
+
"videoReferenceStudy.storyboard must include at least four timecoded frames so the reference is studied as motion, not a single screenshot.",
|
|
9634
|
+
"videoReferenceStudy.transitionAnalysis must include at least three frame-to-frame deltas proving how behavior changes between sampled frames.",
|
|
9635
|
+
'videoReferenceStudy.acceptanceMapping "Motion behavior" points to missing acceptanceId "missing.acceptance".',
|
|
9636
|
+
]),
|
|
9637
|
+
);
|
|
9638
|
+
});
|
|
9639
|
+
|
|
9640
|
+
it("requires playback timeline coverage to prove seamless forward-only loop behavior follows edited duration", () => {
|
|
9641
|
+
const schemaWithPlaybackTimeline = {
|
|
9642
|
+
...starterSchema,
|
|
9643
|
+
panels: {
|
|
9644
|
+
...starterSchema.panels,
|
|
9645
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
6751
9646
|
},
|
|
6752
9647
|
};
|
|
6753
9648
|
|
|
6754
9649
|
expect(
|
|
6755
|
-
validateToolcraftAcceptanceCoverage(
|
|
9650
|
+
validateToolcraftAcceptanceCoverage(schemaWithPlaybackTimeline, [
|
|
6756
9651
|
{
|
|
6757
9652
|
automated: true,
|
|
6758
|
-
automatedTestName: "
|
|
9653
|
+
automatedTestName: "timeline duration edit drives renderer output",
|
|
6759
9654
|
browser: true,
|
|
6760
|
-
browserTestName: "browser:
|
|
6761
|
-
componentType: "
|
|
6762
|
-
evidence: "
|
|
6763
|
-
expectedObservable:
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
target: "
|
|
6769
|
-
|
|
9655
|
+
browserTestName: "browser: timeline duration edit drives renderer output",
|
|
9656
|
+
componentType: "timeline",
|
|
9657
|
+
evidence: "timeline-output",
|
|
9658
|
+
expectedObservable:
|
|
9659
|
+
"Editing timeline duration changes the playback range and renderer follows state.timeline.durationSeconds.",
|
|
9660
|
+
fixture: "timeline playback fixture",
|
|
9661
|
+
id: "timeline.playback",
|
|
9662
|
+
kind: "runtime",
|
|
9663
|
+
target: "timeline.playback",
|
|
9664
|
+
timelineCoverage: "playback",
|
|
9665
|
+
timelinePlaybackCoverage: [
|
|
9666
|
+
"pause-resume",
|
|
9667
|
+
"scrub",
|
|
9668
|
+
"duration",
|
|
9669
|
+
"loop",
|
|
9670
|
+
"rendered-frame",
|
|
9671
|
+
],
|
|
9672
|
+
userAction: "Edit timeline duration, scrub the range, pause, and resume playback.",
|
|
6770
9673
|
},
|
|
6771
9674
|
]),
|
|
6772
|
-
).
|
|
6773
|
-
|
|
6774
|
-
"Style / preset (shader.fxPreset) segmented controls must preserve cell padding: use at most 4 short options (max 9 characters per label and 24 total) or shorten labels first; if the compact names still exceed the budget, use a select dropdown instead.",
|
|
6775
|
-
]),
|
|
9675
|
+
).toContain(
|
|
9676
|
+
'timeline.playback timelinePlaybackCoverage "loop" must prove a seamless forward-only product loop: motion advances in one direction, avoids mirror/yoyo/ping-pong/reverse fallbacks, first and last frames stitch without a visible jump, and the same seam holds after changing timeline duration.',
|
|
6776
9677
|
);
|
|
6777
9678
|
});
|
|
6778
9679
|
|
|
6779
|
-
it("
|
|
9680
|
+
it("rejects incomplete seamless loop evidence that omits fallback direction checks", () => {
|
|
6780
9681
|
const schemaWithPlaybackTimeline = {
|
|
6781
9682
|
...starterSchema,
|
|
6782
9683
|
panels: {
|
|
6783
9684
|
...starterSchema.panels,
|
|
6784
|
-
timeline: { enabled: true, mode: "playback" as const },
|
|
9685
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
6785
9686
|
},
|
|
6786
9687
|
};
|
|
6787
9688
|
|
|
6788
|
-
expect(
|
|
6789
|
-
|
|
6790
|
-
|
|
9689
|
+
expect(
|
|
9690
|
+
validateToolcraftAcceptanceCoverage(schemaWithPlaybackTimeline, [
|
|
9691
|
+
{
|
|
9692
|
+
automated: true,
|
|
9693
|
+
automatedTestName: "timeline duration edit verifies a seamless forward-only loop",
|
|
9694
|
+
browser: true,
|
|
9695
|
+
browserTestName: "browser: timeline duration edit verifies loop seam",
|
|
9696
|
+
componentType: "timeline",
|
|
9697
|
+
evidence: "timeline-output",
|
|
9698
|
+
expectedObservable:
|
|
9699
|
+
"Editing timeline duration keeps a seamless forward-only loop and stitches first and last frames.",
|
|
9700
|
+
fixture: "timeline playback fixture",
|
|
9701
|
+
id: "timeline.playback",
|
|
9702
|
+
kind: "runtime",
|
|
9703
|
+
target: "timeline.playback",
|
|
9704
|
+
timelineCoverage: "playback",
|
|
9705
|
+
timelinePlaybackCoverage: [
|
|
9706
|
+
"pause-resume",
|
|
9707
|
+
"scrub",
|
|
9708
|
+
"duration",
|
|
9709
|
+
"loop",
|
|
9710
|
+
"rendered-frame",
|
|
9711
|
+
],
|
|
9712
|
+
userAction:
|
|
9713
|
+
"Edit timeline duration and verify first and last frames stitch with no mirror fallback.",
|
|
9714
|
+
},
|
|
6791
9715
|
]),
|
|
9716
|
+
).toContain(
|
|
9717
|
+
'timeline.playback timelinePlaybackCoverage "loop" must prove a seamless forward-only product loop: motion advances in one direction, avoids mirror/yoyo/ping-pong/reverse fallbacks, first and last frames stitch without a visible jump, and the same seam holds after changing timeline duration.',
|
|
6792
9718
|
);
|
|
6793
9719
|
});
|
|
6794
9720
|
|
|
6795
|
-
it("
|
|
9721
|
+
it("rejects generic seamless loop evidence that omits first-last frame stitching", () => {
|
|
6796
9722
|
const schemaWithPlaybackTimeline = {
|
|
6797
9723
|
...starterSchema,
|
|
6798
9724
|
panels: {
|
|
6799
9725
|
...starterSchema.panels,
|
|
6800
|
-
timeline: { enabled: true, mode: "playback" as const },
|
|
9726
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
6801
9727
|
},
|
|
6802
9728
|
};
|
|
6803
9729
|
|
|
@@ -6805,24 +9731,31 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6805
9731
|
validateToolcraftAcceptanceCoverage(schemaWithPlaybackTimeline, [
|
|
6806
9732
|
{
|
|
6807
9733
|
automated: true,
|
|
6808
|
-
automatedTestName: "timeline
|
|
9734
|
+
automatedTestName: "timeline duration edit verifies a generic loop",
|
|
6809
9735
|
browser: true,
|
|
6810
|
-
browserTestName: "browser: timeline
|
|
9736
|
+
browserTestName: "browser: timeline duration edit verifies a generic loop",
|
|
6811
9737
|
componentType: "timeline",
|
|
6812
9738
|
evidence: "timeline-output",
|
|
6813
9739
|
expectedObservable:
|
|
6814
|
-
"
|
|
9740
|
+
"Editing timeline duration keeps a seamless forward-only loop with no mirror, yoyo, ping-pong, or reverse fallback.",
|
|
6815
9741
|
fixture: "timeline playback fixture",
|
|
6816
9742
|
id: "timeline.playback",
|
|
6817
9743
|
kind: "runtime",
|
|
6818
9744
|
target: "timeline.playback",
|
|
6819
9745
|
timelineCoverage: "playback",
|
|
6820
|
-
timelinePlaybackCoverage: [
|
|
6821
|
-
|
|
9746
|
+
timelinePlaybackCoverage: [
|
|
9747
|
+
"pause-resume",
|
|
9748
|
+
"scrub",
|
|
9749
|
+
"duration",
|
|
9750
|
+
"loop",
|
|
9751
|
+
"rendered-frame",
|
|
9752
|
+
],
|
|
9753
|
+
userAction:
|
|
9754
|
+
"Edit timeline duration and verify the loop remains forward-only with no mirror, yoyo, ping-pong, or reverse fallback.",
|
|
6822
9755
|
},
|
|
6823
9756
|
]),
|
|
6824
9757
|
).toContain(
|
|
6825
|
-
'timeline.playback
|
|
9758
|
+
'timeline.playback timelinePlaybackCoverage "loop" must prove a seamless forward-only product loop: motion advances in one direction, avoids mirror/yoyo/ping-pong/reverse fallbacks, first and last frames stitch without a visible jump, and the same seam holds after changing timeline duration.',
|
|
6826
9759
|
);
|
|
6827
9760
|
});
|
|
6828
9761
|
|
|
@@ -6860,7 +9793,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6860
9793
|
],
|
|
6861
9794
|
title: "Controls",
|
|
6862
9795
|
},
|
|
6863
|
-
timeline: { enabled: true, mode: "keyframes" as const },
|
|
9796
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "keyframes" as const },
|
|
6864
9797
|
},
|
|
6865
9798
|
};
|
|
6866
9799
|
|
|
@@ -6965,7 +9898,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6965
9898
|
],
|
|
6966
9899
|
title: "Controls",
|
|
6967
9900
|
},
|
|
6968
|
-
timeline: { enabled: true, mode: "keyframes" as const },
|
|
9901
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "keyframes" as const },
|
|
6969
9902
|
},
|
|
6970
9903
|
};
|
|
6971
9904
|
|
|
@@ -7057,7 +9990,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
7057
9990
|
],
|
|
7058
9991
|
title: "Controls",
|
|
7059
9992
|
},
|
|
7060
|
-
timeline: { enabled: true, mode: "keyframes" as const },
|
|
9993
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "keyframes" as const },
|
|
7061
9994
|
},
|
|
7062
9995
|
};
|
|
7063
9996
|
|