@pixel-point/toolcraft 0.0.9 → 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 +14 -6
- package/src/generate.test.mjs +28 -0
- package/templates/runtime/contracts/component-contracts.test.ts +148 -34
- package/templates/runtime/contracts/component-contracts.ts +79 -38
- package/templates/runtime/contracts/decision-contracts.test.ts +33 -1
- package/templates/runtime/contracts/decision-contracts.ts +19 -6
- 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 +474 -27
- package/templates/runtime/react/controls-panel.tsx +71 -26
- 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 +225 -173
- package/templates/runtime/schema/define-toolcraft.ts +117 -247
- package/templates/runtime/schema/runtime-targets.ts +21 -0
- package/templates/runtime/schema/types.ts +41 -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 +657 -2
- package/templates/runtime/testing/performance.ts +789 -49
- package/templates/starter/AGENTS.md +22 -16
- package/templates/starter/docs/toolcraft/README.md +8 -4
- package/templates/starter/docs/toolcraft/acceptance-testing.md +38 -7
- package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +51 -20
- package/templates/starter/docs/toolcraft/component-rules.md +49 -37
- package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
- package/templates/starter/docs/toolcraft/performance.md +30 -10
- package/templates/starter/docs/toolcraft/schema-reference.md +134 -33
- 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/index.html +1 -0
- package/templates/starter/package.json +1 -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 +82 -16
- package/templates/starter/scripts/toolcraft-port.mjs +178 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +147 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +2573 -313
- package/templates/starter/src/app/starter-acceptance.ts +978 -81
- 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/controls/actions/actions-control.tsx +46 -3
- package/templates/ui/components/controls/color/palette-control.tsx +34 -4
- 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 +2 -0
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
- package/templates/ui/components/controls/select/select-control.tsx +4 -26
- package/templates/ui/components/controls/vector/vector-control.tsx +25 -4
- package/templates/ui/components/panel/panel-actions.tsx +1 -1
|
@@ -12,6 +12,12 @@ 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,
|
|
16
22
|
starterControlSectionInventory,
|
|
17
23
|
starterProductReadiness,
|
|
@@ -101,6 +107,7 @@ const requiredAgentWorklogDecisionTrailFields = [
|
|
|
101
107
|
"Task type",
|
|
102
108
|
"User-visible result",
|
|
103
109
|
"Source/reference checked",
|
|
110
|
+
"Reference inputs",
|
|
104
111
|
"Docs/contracts read",
|
|
105
112
|
"Contract rules applied",
|
|
106
113
|
"Decision",
|
|
@@ -111,6 +118,10 @@ const requiredAgentWorklogDecisionTrailFields = [
|
|
|
111
118
|
"Skipped checks",
|
|
112
119
|
"Risks",
|
|
113
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;
|
|
114
125
|
|
|
115
126
|
const playbackTimelineAcceptance: ToolcraftComponentAcceptance = {
|
|
116
127
|
automated: true,
|
|
@@ -134,9 +145,133 @@ const playbackTimelineAcceptance: ToolcraftComponentAcceptance = {
|
|
|
134
145
|
"rendered-frame",
|
|
135
146
|
],
|
|
136
147
|
userAction:
|
|
137
|
-
"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.",
|
|
138
149
|
};
|
|
139
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
|
+
|
|
140
275
|
function makeControlAcceptance(
|
|
141
276
|
target: string,
|
|
142
277
|
componentType: string,
|
|
@@ -157,7 +292,91 @@ function makeControlAcceptance(
|
|
|
157
292
|
};
|
|
158
293
|
}
|
|
159
294
|
|
|
160
|
-
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) {
|
|
161
380
|
return defineToolcraft({
|
|
162
381
|
canvas: { enabled: true },
|
|
163
382
|
panels: {
|
|
@@ -186,13 +405,13 @@ function createSettingsTransferThresholdSchema(settingsTransfer: false | "auto"
|
|
|
186
405
|
});
|
|
187
406
|
}
|
|
188
407
|
|
|
189
|
-
function
|
|
408
|
+
function createMandatorySetupAcceptance() {
|
|
190
409
|
return Array.from({ length: 12 }, (_, index) =>
|
|
191
410
|
makeControlAcceptance(`settings.control${index}`, "slider"),
|
|
192
411
|
);
|
|
193
412
|
}
|
|
194
413
|
|
|
195
|
-
function
|
|
414
|
+
function createMandatorySetupWithCanvasSizeSchema() {
|
|
196
415
|
return defineToolcraft({
|
|
197
416
|
canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 } },
|
|
198
417
|
panels: {
|
|
@@ -214,14 +433,14 @@ function createSettingsTransferBelowThresholdWithCanvasSizeSchema() {
|
|
|
214
433
|
title: "Transform",
|
|
215
434
|
},
|
|
216
435
|
],
|
|
217
|
-
title: "
|
|
436
|
+
title: "Runtime Setup Settings",
|
|
218
437
|
},
|
|
219
438
|
},
|
|
220
439
|
settingsTransfer: false,
|
|
221
440
|
});
|
|
222
441
|
}
|
|
223
442
|
|
|
224
|
-
function
|
|
443
|
+
function createMandatorySetupWithCanvasSizeAcceptance() {
|
|
225
444
|
return [
|
|
226
445
|
makeControlAcceptance("canvas.size.width", "text"),
|
|
227
446
|
makeControlAcceptance("canvas.size.height", "text"),
|
|
@@ -267,7 +486,7 @@ function stripJsComments(source: string): string {
|
|
|
267
486
|
}
|
|
268
487
|
|
|
269
488
|
function getMarkdownHeadingLevel(line: string): number | null {
|
|
270
|
-
const match = /^(#{1,6})\s+\S/.exec(line);
|
|
489
|
+
const match = /^(#{1,6})\s+\S/.exec(line.trim());
|
|
271
490
|
|
|
272
491
|
return match ? match[1].length : null;
|
|
273
492
|
}
|
|
@@ -333,6 +552,104 @@ function escapeRegExp(value: string): string {
|
|
|
333
552
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
334
553
|
}
|
|
335
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
|
+
|
|
336
653
|
function getAgentWorklogValidationErrors(source: string): string[] {
|
|
337
654
|
const errors: string[] = [];
|
|
338
655
|
|
|
@@ -395,10 +712,21 @@ function getAgentWorklogValidationErrors(source: string): string[] {
|
|
|
395
712
|
errors.push("agent-worklog.md Evidence must name reviewed files, references, or contract rules.");
|
|
396
713
|
}
|
|
397
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
|
+
|
|
398
724
|
if (!/\bpnpm\s+(verify|test|build|typecheck)|browser|Playwright|perf/i.test(verificationBody)) {
|
|
399
725
|
errors.push("agent-worklog.md Verification must list concrete test/build/browser/performance checks.");
|
|
400
726
|
}
|
|
401
727
|
|
|
728
|
+
errors.push(...getAgentWorklogVerificationGateErrors(source));
|
|
729
|
+
|
|
402
730
|
if (!/\b(Risk|None):\s*\S/i.test(risksBody)) {
|
|
403
731
|
errors.push('agent-worklog.md Risks must include either "Risk:" entries or "None:" with a reason.');
|
|
404
732
|
}
|
|
@@ -455,7 +783,7 @@ function sourceDefinesProductCanvasContent(): boolean {
|
|
|
455
783
|
function schemaHasProductSurface(): boolean {
|
|
456
784
|
return (
|
|
457
785
|
(starterSchema.panels.controls?.sections ?? []).some(
|
|
458
|
-
(section) => Object.keys(section.controls).length > 0,
|
|
786
|
+
(section) => section.title !== "Setup" && Object.keys(section.controls).length > 0,
|
|
459
787
|
) ||
|
|
460
788
|
starterSchema.panels.layers === true ||
|
|
461
789
|
starterSchema.panels.timeline?.enabled === true ||
|
|
@@ -677,12 +1005,93 @@ function sourceHasVideoDurationMetadataCoverage(source: string): boolean {
|
|
|
677
1005
|
);
|
|
678
1006
|
}
|
|
679
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
|
+
|
|
680
1025
|
function sourceHasCustomMovOrProResEncoder(source: string): boolean {
|
|
681
1026
|
return /\bVideoEncoder\b|\bffmpeg\b|\bFFmpeg\b|\bProRes\b|\bprores\b|\btranscoder\b/i.test(
|
|
682
1027
|
source,
|
|
683
1028
|
);
|
|
684
1029
|
}
|
|
685
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
|
+
|
|
686
1095
|
function sourceHasImageExportDimensionCoverage(source: string): boolean {
|
|
687
1096
|
const hasImageDecoder =
|
|
688
1097
|
/\bcreateImageBitmap\b/.test(source) ||
|
|
@@ -807,6 +1216,19 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
807
1216
|
expect(validateToolcraftAcceptanceCoverage(starterSchema, starterAcceptance)).toEqual([]);
|
|
808
1217
|
});
|
|
809
1218
|
|
|
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
|
+
|
|
810
1232
|
it("requires generated product apps to publish a control section inventory", () => {
|
|
811
1233
|
if (!schemaHasProductSurface()) {
|
|
812
1234
|
expect(starterControlSectionInventory).toEqual([]);
|
|
@@ -827,7 +1249,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
827
1249
|
).toEqual([]);
|
|
828
1250
|
});
|
|
829
1251
|
|
|
830
|
-
it("requires fileDrop acceptance to prove upload, clear, and
|
|
1252
|
+
it("requires fileDrop acceptance to prove upload, clear, reset, and image transforms", () => {
|
|
831
1253
|
const fileDropSchema = defineToolcraft({
|
|
832
1254
|
canvas: { enabled: true },
|
|
833
1255
|
panels: {
|
|
@@ -870,12 +1292,69 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
870
1292
|
]),
|
|
871
1293
|
).toEqual(
|
|
872
1294
|
expect.arrayContaining([
|
|
873
|
-
"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
|
+
},
|
|
874
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.",
|
|
875
1354
|
);
|
|
876
1355
|
});
|
|
877
1356
|
|
|
878
|
-
it("accepts fileDrop lifecycle coverage that includes reset", () => {
|
|
1357
|
+
it("accepts fileDrop lifecycle coverage that includes reset and image transforms", () => {
|
|
879
1358
|
const fileDropSchema = defineToolcraft({
|
|
880
1359
|
canvas: { enabled: true },
|
|
881
1360
|
panels: {
|
|
@@ -907,13 +1386,13 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
907
1386
|
componentType: "fileDrop",
|
|
908
1387
|
evidence: "media-lifecycle",
|
|
909
1388
|
expectedObservable:
|
|
910
|
-
"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.",
|
|
911
1390
|
fixture: "source image fixture",
|
|
912
1391
|
id: "media.source",
|
|
913
1392
|
kind: "control",
|
|
914
1393
|
target: "media.source",
|
|
915
1394
|
userAction:
|
|
916
|
-
"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.",
|
|
917
1396
|
},
|
|
918
1397
|
]);
|
|
919
1398
|
|
|
@@ -922,6 +1401,77 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
922
1401
|
).toEqual([]);
|
|
923
1402
|
});
|
|
924
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
|
+
|
|
925
1475
|
it("rejects fake video duration browser coverage that falls back to expected duration", () => {
|
|
926
1476
|
const fakeCoverage = `
|
|
927
1477
|
async function getVideoDuration(page, buffer, mimeType) {
|
|
@@ -1142,7 +1692,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1142
1692
|
outputActions: {
|
|
1143
1693
|
actions: [
|
|
1144
1694
|
{
|
|
1145
|
-
icon: "
|
|
1695
|
+
icon: "upload-simple",
|
|
1146
1696
|
label: "Export PNG",
|
|
1147
1697
|
value: "export.png",
|
|
1148
1698
|
},
|
|
@@ -1184,20 +1734,79 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1184
1734
|
);
|
|
1185
1735
|
});
|
|
1186
1736
|
|
|
1187
|
-
it("requires
|
|
1188
|
-
const
|
|
1189
|
-
canvas: {
|
|
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
|
+
},
|
|
1190
1744
|
panels: {
|
|
1191
1745
|
controls: {
|
|
1192
|
-
sections: [
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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 },
|
|
1799
|
+
panels: {
|
|
1800
|
+
controls: {
|
|
1801
|
+
sections: [
|
|
1802
|
+
{
|
|
1803
|
+
controls: {
|
|
1804
|
+
opacity: {
|
|
1805
|
+
defaultValue: 0.75,
|
|
1806
|
+
label: "Opacity",
|
|
1807
|
+
target: "appearance.opacity",
|
|
1808
|
+
type: "slider",
|
|
1809
|
+
},
|
|
1201
1810
|
},
|
|
1202
1811
|
title: "Appearance",
|
|
1203
1812
|
},
|
|
@@ -1305,67 +1914,29 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1305
1914
|
);
|
|
1306
1915
|
});
|
|
1307
1916
|
|
|
1308
|
-
it("
|
|
1309
|
-
const complexSchema =
|
|
1917
|
+
it("does not require app-authored settings transfer because setup controls are runtime-mandatory", () => {
|
|
1918
|
+
const complexSchema = createMandatorySetupSchema(false);
|
|
1310
1919
|
|
|
1311
1920
|
expect(
|
|
1312
1921
|
validateToolcraftAcceptanceCoverage(
|
|
1313
1922
|
complexSchema,
|
|
1314
|
-
|
|
1923
|
+
createMandatorySetupAcceptance(),
|
|
1315
1924
|
),
|
|
1316
|
-
).toEqual(
|
|
1317
|
-
expect.arrayContaining([
|
|
1318
|
-
expect.stringContaining(
|
|
1319
|
-
"settingsTransfer is required for this complex product app because settings-transfer eligibility was reached.",
|
|
1320
|
-
),
|
|
1321
|
-
]),
|
|
1322
|
-
);
|
|
1925
|
+
).toEqual([]);
|
|
1323
1926
|
});
|
|
1324
1927
|
|
|
1325
|
-
it("
|
|
1326
|
-
const
|
|
1928
|
+
it("accepts small schemas because settings transfer setup is runtime-mandatory", () => {
|
|
1929
|
+
const smallSchema = createMandatorySetupWithCanvasSizeSchema();
|
|
1327
1930
|
const errors = validateToolcraftAcceptanceCoverage(
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
);
|
|
1331
|
-
|
|
1332
|
-
expect(errors).not.toEqual(
|
|
1333
|
-
expect.arrayContaining([
|
|
1334
|
-
expect.stringContaining(
|
|
1335
|
-
"settingsTransfer is required for this complex product app because settings-transfer eligibility was reached.",
|
|
1336
|
-
),
|
|
1337
|
-
]),
|
|
1931
|
+
smallSchema,
|
|
1932
|
+
createMandatorySetupWithCanvasSizeAcceptance(),
|
|
1338
1933
|
);
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
it("allows explicit settings transfer opt-out only with runtime evidence", () => {
|
|
1342
|
-
const complexSchema = createSettingsTransferThresholdSchema(false);
|
|
1343
|
-
const acceptance: ToolcraftComponentAcceptance[] = [
|
|
1344
|
-
...createSettingsTransferThresholdAcceptance(),
|
|
1345
|
-
{
|
|
1346
|
-
automated: true,
|
|
1347
|
-
automatedTestName: "settings transfer intentionally omitted for session-only app",
|
|
1348
|
-
browser: true,
|
|
1349
|
-
browserTestName: "browser: settings transfer intentionally omitted",
|
|
1350
|
-
componentType: "settingsTransfer",
|
|
1351
|
-
evidence: "persistence-state",
|
|
1352
|
-
expectedObservable:
|
|
1353
|
-
"The complex app is session-only and intentionally does not expose portable settings.",
|
|
1354
|
-
fixture: "settings transfer opt-out fixture",
|
|
1355
|
-
id: "settings.transfer.optOut",
|
|
1356
|
-
kind: "runtime",
|
|
1357
|
-
settingsTransferCoverage: "opt-out",
|
|
1358
|
-
target: "runtime.settingsTransfer",
|
|
1359
|
-
userAction:
|
|
1360
|
-
"Open the session-only app, change temporary settings, and verify no portable settings workflow is required.",
|
|
1361
|
-
},
|
|
1362
|
-
];
|
|
1363
1934
|
|
|
1364
|
-
expect(
|
|
1935
|
+
expect(errors).toEqual([]);
|
|
1365
1936
|
});
|
|
1366
1937
|
|
|
1367
1938
|
it("passes complex schemas with auto settings transfer enabled", () => {
|
|
1368
|
-
const complexSchema =
|
|
1939
|
+
const complexSchema = createMandatorySetupSchema("auto");
|
|
1369
1940
|
const acceptance: ToolcraftComponentAcceptance[] = [
|
|
1370
1941
|
{
|
|
1371
1942
|
automated: true,
|
|
@@ -1383,12 +1954,63 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1383
1954
|
userAction:
|
|
1384
1955
|
"Change one complex setting, export settings, change it again, import the JSON, and observe the restored value.",
|
|
1385
1956
|
},
|
|
1386
|
-
...
|
|
1957
|
+
...createMandatorySetupAcceptance(),
|
|
1387
1958
|
];
|
|
1388
1959
|
|
|
1389
1960
|
expect(validateToolcraftAcceptanceCoverage(complexSchema, acceptance)).toEqual([]);
|
|
1390
1961
|
});
|
|
1391
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
|
+
|
|
1392
2014
|
it("requires compound controls to cover every semantic value part", () => {
|
|
1393
2015
|
const compoundSchema = defineToolcraft({
|
|
1394
2016
|
canvas: { enabled: true },
|
|
@@ -1502,7 +2124,6 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1502
2124
|
});
|
|
1503
2125
|
|
|
1504
2126
|
const acceptance = [
|
|
1505
|
-
"runtime.settingsTransfer",
|
|
1506
2127
|
"mesh.anchor",
|
|
1507
2128
|
"mesh.focus",
|
|
1508
2129
|
"mesh.gradient",
|
|
@@ -1518,9 +2139,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
1518
2139
|
browser: true,
|
|
1519
2140
|
browserTestName: `browser: ${target} changes output`,
|
|
1520
2141
|
componentType:
|
|
1521
|
-
target === "
|
|
1522
|
-
? "settingsTransfer"
|
|
1523
|
-
: target === "mesh.anchor"
|
|
2142
|
+
target === "mesh.anchor"
|
|
1524
2143
|
? "anchorGrid"
|
|
1525
2144
|
: target === "mesh.focus"
|
|
1526
2145
|
? "vector"
|
|
@@ -2536,6 +3155,11 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2536
3155
|
schemaHasProductSurface(),
|
|
2537
3156
|
"Product readiness requires product surface: controls, layers, timeline, canvasContent, or acceptance coverage.",
|
|
2538
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");
|
|
2539
3163
|
return;
|
|
2540
3164
|
}
|
|
2541
3165
|
|
|
@@ -2589,6 +3213,247 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2589
3213
|
expect(getAgentWorklogValidationErrors(worklogSource)).toEqual([]);
|
|
2590
3214
|
});
|
|
2591
3215
|
|
|
3216
|
+
it("requires product worklogs to record storyboard evidence for video references", () => {
|
|
3217
|
+
const videoWorklogWithoutStudy = `
|
|
3218
|
+
# Implementation Worklog
|
|
3219
|
+
|
|
3220
|
+
## Status
|
|
3221
|
+
|
|
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.
|
|
3242
|
+
|
|
3243
|
+
## Decisions
|
|
3244
|
+
|
|
3245
|
+
### Renderer
|
|
3246
|
+
- Decision: Use SVG.
|
|
3247
|
+
- Reason: Vector output.
|
|
3248
|
+
- Evidence: src/app/product-renderer.tsx.
|
|
3249
|
+
|
|
3250
|
+
### Timeline
|
|
3251
|
+
- Decision: Use playback.
|
|
3252
|
+
- Reason: Motion output.
|
|
3253
|
+
- Evidence: src/app/app-schema.ts.
|
|
3254
|
+
|
|
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
|
+
|
|
2592
3457
|
it("rejects stale or incomplete product worklogs", () => {
|
|
2593
3458
|
const staleWorklog = `
|
|
2594
3459
|
# Implementation Worklog
|
|
@@ -2700,7 +3565,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2700
3565
|
|
|
2701
3566
|
- Decision: Run viewport-stability and animation drag budgets.
|
|
2702
3567
|
- Reason: Animation and canvas movement are performance-sensitive.
|
|
2703
|
-
- Evidence:
|
|
3568
|
+
- Evidence: agent-browser performance checkpoint.
|
|
2704
3569
|
|
|
2705
3570
|
## Decision Trail
|
|
2706
3571
|
|
|
@@ -2710,13 +3575,14 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2710
3575
|
- Task type: Renderer, schema, timeline, export, and performance.
|
|
2711
3576
|
- User-visible result: The canvas renders the animated poster, the top timeline controls playback, and sticky footer actions export video and PNG.
|
|
2712
3577
|
- Source/reference checked: User prompt plus the generated product renderer requirements.
|
|
3578
|
+
- Reference inputs: None.
|
|
2713
3579
|
- Docs/contracts read: workflow.md, assembly-workflow.md, schema-reference.md, component-rules.md, renderer-technique.md, performance.md.
|
|
2714
3580
|
- Contract rules applied: runtime-shell-required, controls-layout-heuristics, timeline-enabled-behavior, output-export-required, performance-coverage-levels.
|
|
2715
3581
|
- Decision: Use SVG foreground with Canvas 2D background and Toolcraft playback timeline.
|
|
2716
3582
|
- Alternatives rejected: DOM-only output because dense animation can jank; app-local timeline because playback UI is runtime-owned.
|
|
2717
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.
|
|
2718
3584
|
- Files changed: src/app/app-schema.ts, src/app/app-performance.ts, src/app/product-renderer.tsx, e2e/app-controls.spec.ts.
|
|
2719
|
-
- Verification: pnpm verify:quick;
|
|
3585
|
+
- Verification: pnpm verify:quick; agent-browser performance checkpoint; browser timeline pause/resume and canvas drag stress.
|
|
2720
3586
|
- Skipped checks: None.
|
|
2721
3587
|
- Risks: None; browser and performance gates cover the touched surfaces.
|
|
2722
3588
|
|
|
@@ -2728,7 +3594,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2728
3594
|
## Verification
|
|
2729
3595
|
|
|
2730
3596
|
- Run: pnpm verify:quick
|
|
2731
|
-
- Run:
|
|
3597
|
+
- Run: agent-browser performance checkpoint
|
|
2732
3598
|
- Browser: timeline pause/resume and canvas drag stress.
|
|
2733
3599
|
|
|
2734
3600
|
## Risks
|
|
@@ -2739,8 +3605,8 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2739
3605
|
expect(getAgentWorklogValidationErrors(productWorklog)).toEqual([]);
|
|
2740
3606
|
});
|
|
2741
3607
|
|
|
2742
|
-
it("rejects product worklogs
|
|
2743
|
-
const
|
|
3608
|
+
it("rejects product worklogs that report incomplete required performance gates", () => {
|
|
3609
|
+
const incompletePerfWorklog = `
|
|
2744
3610
|
# Implementation Worklog
|
|
2745
3611
|
|
|
2746
3612
|
## Status
|
|
@@ -2751,33 +3617,507 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2751
3617
|
|
|
2752
3618
|
### Renderer
|
|
2753
3619
|
|
|
2754
|
-
- Decision: Use
|
|
2755
|
-
- Reason:
|
|
2756
|
-
- 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.
|
|
2757
3623
|
|
|
2758
3624
|
### Timeline
|
|
2759
3625
|
|
|
2760
|
-
- Decision: Use playback timeline.
|
|
2761
|
-
- Reason: The product
|
|
3626
|
+
- Decision: Use Toolcraft playback timeline.
|
|
3627
|
+
- Reason: The product is animated and exportable.
|
|
2762
3628
|
- Evidence: appSchema.panels.timeline.mode.
|
|
2763
3629
|
|
|
2764
3630
|
### Layers
|
|
2765
3631
|
|
|
2766
3632
|
- Decision: Do not enable layers.
|
|
2767
|
-
- Reason: The product
|
|
3633
|
+
- Reason: The product has one generated output.
|
|
2768
3634
|
- Evidence: appSchema.panels.layers is omitted.
|
|
2769
3635
|
|
|
2770
3636
|
### Controls
|
|
2771
3637
|
|
|
2772
3638
|
- Decision: Group controls by product entity.
|
|
2773
|
-
- Reason:
|
|
2774
|
-
- 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.
|
|
2775
3641
|
|
|
2776
3642
|
### Export
|
|
2777
3643
|
|
|
2778
3644
|
- Decision: Provide PNG and video export.
|
|
2779
|
-
- Reason: The
|
|
2780
|
-
- Evidence: panelActions.
|
|
3645
|
+
- Reason: The product is animated but still frames are useful.
|
|
3646
|
+
- Evidence: panelActions plus export handlers.
|
|
3647
|
+
|
|
3648
|
+
### Performance
|
|
3649
|
+
|
|
3650
|
+
- Decision: Declare performance scenarios for renderer and controls.
|
|
3651
|
+
- Reason: Canvas effects are performance-sensitive.
|
|
3652
|
+
- Evidence: src/app/app-performance.ts.
|
|
3653
|
+
|
|
3654
|
+
## Decision Trail
|
|
3655
|
+
|
|
3656
|
+
### Iteration 1 — Initial product build
|
|
3657
|
+
|
|
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.
|
|
3672
|
+
|
|
3673
|
+
## Evidence
|
|
3674
|
+
|
|
3675
|
+
- Source reviewed: src/app/app-schema.ts, src/app/app-performance.ts, e2e/app-controls.spec.ts.
|
|
3676
|
+
- Contract applied: performance-coverage-levels.
|
|
3677
|
+
|
|
3678
|
+
## Verification
|
|
3679
|
+
|
|
3680
|
+
- Run: pnpm test failed.
|
|
3681
|
+
- Run: pnpm verify:perf not complete.
|
|
3682
|
+
|
|
3683
|
+
## Risks
|
|
3684
|
+
|
|
3685
|
+
- Risk: Required checks are not complete.
|
|
3686
|
+
`;
|
|
3687
|
+
|
|
3688
|
+
expect(getAgentWorklogValidationErrors(incompletePerfWorklog)).toEqual(
|
|
3689
|
+
expect.arrayContaining([
|
|
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.",
|
|
3693
|
+
]),
|
|
3694
|
+
);
|
|
3695
|
+
});
|
|
3696
|
+
|
|
3697
|
+
it("requires first working product worklogs to record a browser performance checkpoint", () => {
|
|
3698
|
+
const missingPerfWorklog = `
|
|
3699
|
+
# Implementation Worklog
|
|
3700
|
+
|
|
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.",
|
|
3779
|
+
);
|
|
3780
|
+
});
|
|
3781
|
+
|
|
3782
|
+
it("recognizes supported browser performance checkpoint evidence line formats", () => {
|
|
3783
|
+
expect(
|
|
3784
|
+
isPassedAgentBrowserPerfRunLine(
|
|
3785
|
+
"- Browser: performance checkpoint runner agent-browser",
|
|
3786
|
+
),
|
|
3787
|
+
).toBe(true);
|
|
3788
|
+
expect(
|
|
3789
|
+
isPassedAgentBrowserPerfRunLine(
|
|
3790
|
+
"- Verification: agent-browser perf checkpoint",
|
|
3791
|
+
),
|
|
3792
|
+
).toBe(true);
|
|
3793
|
+
expect(
|
|
3794
|
+
isPassedPlaywrightFallbackPerfRunLine(
|
|
3795
|
+
"- Verification: playwright-fallback browser performance checkpoint",
|
|
3796
|
+
),
|
|
3797
|
+
).toBe(true);
|
|
3798
|
+
expect(isPassedPlaywrightFallbackPerfRunLine("- Run: pnpm verify:perf")).toBe(true);
|
|
3799
|
+
expect(
|
|
3800
|
+
isPassedPlaywrightFallbackPerfRunLine(
|
|
3801
|
+
"- Run: pnpm verify:perf not complete",
|
|
3802
|
+
),
|
|
3803
|
+
).toBe(false);
|
|
3804
|
+
expect(
|
|
3805
|
+
isPassedAgentBrowserPerfRunLine(
|
|
3806
|
+
"- Verification: agent-browser perf checkpoint planned after build",
|
|
3807
|
+
),
|
|
3808
|
+
).toBe(false);
|
|
3809
|
+
expect(
|
|
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.
|
|
2781
4121
|
|
|
2782
4122
|
### Performance
|
|
2783
4123
|
|
|
@@ -2815,6 +4155,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2815
4155
|
expect.arrayContaining([
|
|
2816
4156
|
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "User-visible result:".',
|
|
2817
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:".',
|
|
2818
4159
|
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "Contract rules applied:".',
|
|
2819
4160
|
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "Alternatives rejected:".',
|
|
2820
4161
|
'agent-worklog.md Decision Trail iteration "Iteration 1 — Product build" must include "State/output mapping:".',
|
|
@@ -2994,8 +4335,24 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
2994
4335
|
).toBeGreaterThanOrEqual(2);
|
|
2995
4336
|
expect(
|
|
2996
4337
|
productImplementationSource,
|
|
2997
|
-
"Video export must use
|
|
2998
|
-
).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);
|
|
2999
4356
|
expect(
|
|
3000
4357
|
productImplementationSource,
|
|
3001
4358
|
"Video export must use shouldIncludeToolcraftExportBackground so PNG transparency does not remove the video background.",
|
|
@@ -3074,6 +4431,10 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3074
4431
|
sourceHasVideoDurationMetadataCoverage(browserTestSources),
|
|
3075
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.",
|
|
3076
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);
|
|
3077
4438
|
if (hasMovOrProResFormat) {
|
|
3078
4439
|
expect(
|
|
3079
4440
|
sourceHasCustomMovOrProResEncoder(productImplementationSource),
|
|
@@ -3102,7 +4463,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3102
4463
|
value: "reset",
|
|
3103
4464
|
},
|
|
3104
4465
|
{
|
|
3105
|
-
icon: "
|
|
4466
|
+
icon: "upload-simple",
|
|
3106
4467
|
label: "Export PNG",
|
|
3107
4468
|
value: "export.png",
|
|
3108
4469
|
},
|
|
@@ -3157,7 +4518,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3157
4518
|
outputActions: {
|
|
3158
4519
|
actions: [
|
|
3159
4520
|
{
|
|
3160
|
-
icon: "
|
|
4521
|
+
icon: "upload-simple",
|
|
3161
4522
|
label: "Export PNG",
|
|
3162
4523
|
value: "export.png",
|
|
3163
4524
|
},
|
|
@@ -3226,7 +4587,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3226
4587
|
outputActions: {
|
|
3227
4588
|
actions: [
|
|
3228
4589
|
{
|
|
3229
|
-
icon: "
|
|
4590
|
+
icon: "upload-simple",
|
|
3230
4591
|
label: "Export PNG",
|
|
3231
4592
|
value: "export.png",
|
|
3232
4593
|
},
|
|
@@ -3292,7 +4653,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3292
4653
|
outputActions: {
|
|
3293
4654
|
actions: [
|
|
3294
4655
|
{
|
|
3295
|
-
icon: "
|
|
4656
|
+
icon: "upload-simple",
|
|
3296
4657
|
label: "Export PNG",
|
|
3297
4658
|
value: "export.png",
|
|
3298
4659
|
},
|
|
@@ -3359,7 +4720,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3359
4720
|
outputActions: {
|
|
3360
4721
|
actions: [
|
|
3361
4722
|
{
|
|
3362
|
-
icon: "
|
|
4723
|
+
icon: "upload-simple",
|
|
3363
4724
|
label: "Export PNG",
|
|
3364
4725
|
value: "export.png",
|
|
3365
4726
|
},
|
|
@@ -3422,7 +4783,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3422
4783
|
outputActions: {
|
|
3423
4784
|
actions: [
|
|
3424
4785
|
{
|
|
3425
|
-
icon: "
|
|
4786
|
+
icon: "upload-simple",
|
|
3426
4787
|
label: "Export PNG",
|
|
3427
4788
|
value: "export.png",
|
|
3428
4789
|
},
|
|
@@ -3487,7 +4848,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3487
4848
|
outputActions: {
|
|
3488
4849
|
actions: [
|
|
3489
4850
|
{
|
|
3490
|
-
icon: "
|
|
4851
|
+
icon: "upload-simple",
|
|
3491
4852
|
label: "Export PNG",
|
|
3492
4853
|
value: "export.png",
|
|
3493
4854
|
},
|
|
@@ -3569,12 +4930,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3569
4930
|
outputActions: {
|
|
3570
4931
|
actions: [
|
|
3571
4932
|
{
|
|
3572
|
-
icon: "
|
|
4933
|
+
icon: "upload-simple",
|
|
3573
4934
|
label: "Export Video",
|
|
3574
4935
|
value: "export.video",
|
|
3575
4936
|
},
|
|
3576
4937
|
{
|
|
3577
|
-
icon: "
|
|
4938
|
+
icon: "upload-simple",
|
|
3578
4939
|
label: "Export PNG",
|
|
3579
4940
|
value: "export.png",
|
|
3580
4941
|
},
|
|
@@ -3588,7 +4949,6 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3588
4949
|
],
|
|
3589
4950
|
title: "Controls",
|
|
3590
4951
|
},
|
|
3591
|
-
timeline: { enabled: true, mode: "playback" },
|
|
3592
4952
|
},
|
|
3593
4953
|
});
|
|
3594
4954
|
|
|
@@ -3643,12 +5003,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3643
5003
|
outputActions: {
|
|
3644
5004
|
actions: [
|
|
3645
5005
|
{
|
|
3646
|
-
icon: "
|
|
5006
|
+
icon: "upload-simple",
|
|
3647
5007
|
label: "Export Video",
|
|
3648
5008
|
value: "export.video",
|
|
3649
5009
|
},
|
|
3650
5010
|
{
|
|
3651
|
-
icon: "
|
|
5011
|
+
icon: "upload-simple",
|
|
3652
5012
|
label: "Export PNG",
|
|
3653
5013
|
value: "export.png",
|
|
3654
5014
|
},
|
|
@@ -3662,7 +5022,6 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3662
5022
|
],
|
|
3663
5023
|
title: "Controls",
|
|
3664
5024
|
},
|
|
3665
|
-
timeline: { enabled: true, mode: "playback" },
|
|
3666
5025
|
},
|
|
3667
5026
|
});
|
|
3668
5027
|
const imageFormatAcceptance = makeControlAcceptance("export.image.format", "select");
|
|
@@ -3714,7 +5073,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3714
5073
|
);
|
|
3715
5074
|
});
|
|
3716
5075
|
|
|
3717
|
-
it("
|
|
5076
|
+
it("rejects disabled product controls and requires visibleWhen for unavailable controls", () => {
|
|
3718
5077
|
const schemaWithDisabledDependency = defineToolcraft({
|
|
3719
5078
|
canvas: { enabled: true },
|
|
3720
5079
|
panels: {
|
|
@@ -3764,12 +5123,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3764
5123
|
"disabledWhen target distribution.mode does not match another schema control target",
|
|
3765
5124
|
),
|
|
3766
5125
|
expect.stringContaining(
|
|
3767
|
-
"uses disabledWhen
|
|
5126
|
+
"uses disabledWhen. Generated product panels should show only controls usable in the current state",
|
|
3768
5127
|
),
|
|
3769
5128
|
]),
|
|
3770
5129
|
);
|
|
3771
5130
|
|
|
3772
|
-
const
|
|
5131
|
+
const schemaWithBranchDisabledDependency = defineToolcraft({
|
|
3773
5132
|
canvas: { enabled: true },
|
|
3774
5133
|
panels: {
|
|
3775
5134
|
controls: {
|
|
@@ -3806,7 +5165,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3806
5165
|
},
|
|
3807
5166
|
},
|
|
3808
5167
|
});
|
|
3809
|
-
const
|
|
5168
|
+
const branchErrors = validateToolcraftAcceptanceCoverage(schemaWithBranchDisabledDependency, [
|
|
3810
5169
|
makeControlAcceptance("distribution.fillMode", "segmented"),
|
|
3811
5170
|
{
|
|
3812
5171
|
...makeControlAcceptance("distribution.fillAmount", "slider"),
|
|
@@ -3816,10 +5175,48 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3816
5175
|
},
|
|
3817
5176
|
]);
|
|
3818
5177
|
|
|
3819
|
-
expect(
|
|
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,
|
|
5196
|
+
label: "Fill level",
|
|
5197
|
+
max: 100,
|
|
5198
|
+
min: 0,
|
|
5199
|
+
target: "distribution.fillAmount",
|
|
5200
|
+
type: "slider",
|
|
5201
|
+
},
|
|
5202
|
+
},
|
|
5203
|
+
title: "Distribution",
|
|
5204
|
+
},
|
|
5205
|
+
],
|
|
5206
|
+
title: "Controls",
|
|
5207
|
+
},
|
|
5208
|
+
},
|
|
5209
|
+
});
|
|
5210
|
+
|
|
5211
|
+
expect(
|
|
5212
|
+
validateToolcraftAcceptanceCoverage(schemaWithDisabledControl, [
|
|
5213
|
+
makeControlAcceptance("distribution.fillAmount", "slider"),
|
|
5214
|
+
]),
|
|
5215
|
+
).toEqual(
|
|
3820
5216
|
expect.arrayContaining([
|
|
3821
|
-
expect.stringContaining(
|
|
3822
|
-
|
|
5217
|
+
expect.stringContaining(
|
|
5218
|
+
"sets disabled: true. Generated product panels should show only controls usable in the current state",
|
|
5219
|
+
),
|
|
3823
5220
|
]),
|
|
3824
5221
|
);
|
|
3825
5222
|
});
|
|
@@ -4654,6 +6051,189 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4654
6051
|
);
|
|
4655
6052
|
});
|
|
4656
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",
|
|
6168
|
+
},
|
|
6169
|
+
),
|
|
6170
|
+
).toEqual(
|
|
6171
|
+
expect.arrayContaining([
|
|
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.',
|
|
6174
|
+
]),
|
|
6175
|
+
);
|
|
6176
|
+
});
|
|
6177
|
+
|
|
6178
|
+
it("requires feature-level behavior evidence for reference inventory items", () => {
|
|
6179
|
+
expect(
|
|
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"),
|
|
6187
|
+
],
|
|
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.',
|
|
6234
|
+
);
|
|
6235
|
+
});
|
|
6236
|
+
|
|
4657
6237
|
it("rejects reference-runtime-clone apps that disable the Toolcraft canvas shell", () => {
|
|
4658
6238
|
const schemaWithoutCanvas = defineToolcraft({
|
|
4659
6239
|
canvas: {
|
|
@@ -4788,7 +6368,9 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4788
6368
|
{
|
|
4789
6369
|
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
4790
6370
|
mode: "reference-runtime-clone",
|
|
6371
|
+
referenceFeatureInventory: makeReferenceFeatureInventory(),
|
|
4791
6372
|
referenceName: "legacy badge wall",
|
|
6373
|
+
referenceStudy: referenceStudyEvidence,
|
|
4792
6374
|
referenceTimeline: { behaviorCoverage: [], mode: "none" },
|
|
4793
6375
|
sourceOfTruth: "reference-runtime",
|
|
4794
6376
|
},
|
|
@@ -4811,41 +6393,165 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4811
6393
|
sourceOfTruth: "reference-runtime",
|
|
4812
6394
|
}),
|
|
4813
6395
|
).toContain(
|
|
4814
|
-
'reference-runtime-clone transport behaviorCoverage "pause-resume" requires referenceTimeline mode "toolcraft-playback", "toolcraft-keyframes", or "custom-reference-timeline"; mode "none" is only for references with no user-facing transport behavior.',
|
|
6396
|
+
'reference-runtime-clone transport behaviorCoverage "pause-resume" requires referenceTimeline mode "toolcraft-playback", "toolcraft-keyframes", or "custom-reference-timeline"; mode "none" is only for references with no user-facing transport behavior.',
|
|
6397
|
+
);
|
|
6398
|
+
});
|
|
6399
|
+
|
|
6400
|
+
it("rejects reference clones that hide restart transport behind timeline none", () => {
|
|
6401
|
+
expect(
|
|
6402
|
+
validateToolcraftAcceptanceCoverage(starterSchema, starterAcceptance, {
|
|
6403
|
+
behaviorCoverage: [
|
|
6404
|
+
"canvas-sizing",
|
|
6405
|
+
"control-mapping",
|
|
6406
|
+
"renderer-state",
|
|
6407
|
+
"restart",
|
|
6408
|
+
"time-progress",
|
|
6409
|
+
],
|
|
6410
|
+
mode: "reference-runtime-clone",
|
|
6411
|
+
referenceName: "legacy restart animation",
|
|
6412
|
+
referenceTimeline: { behaviorCoverage: [], mode: "none" },
|
|
6413
|
+
sourceOfTruth: "reference-runtime",
|
|
6414
|
+
}),
|
|
6415
|
+
).toContain(
|
|
6416
|
+
'reference-runtime-clone transport behaviorCoverage "restart", "time-progress" requires referenceTimeline mode "toolcraft-playback", "toolcraft-keyframes", or "custom-reference-timeline"; mode "none" is only for references with no user-facing transport behavior.',
|
|
6417
|
+
);
|
|
6418
|
+
});
|
|
6419
|
+
|
|
6420
|
+
it("requires playback reference timelines to declare concrete behavior coverage", () => {
|
|
6421
|
+
expect(
|
|
6422
|
+
validateToolcraftAcceptanceCoverage(starterSchema, starterAcceptance, {
|
|
6423
|
+
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
6424
|
+
mode: "reference-runtime-clone",
|
|
6425
|
+
referenceName: "legacy playback animation",
|
|
6426
|
+
referenceTimeline: { behaviorCoverage: [], mode: "toolcraft-playback" },
|
|
6427
|
+
sourceOfTruth: "reference-runtime",
|
|
6428
|
+
}),
|
|
6429
|
+
).toContain(
|
|
6430
|
+
'referenceTimeline mode "toolcraft-playback" must list the concrete timeline transport behaviors in behaviorCoverage.',
|
|
6431
|
+
);
|
|
6432
|
+
});
|
|
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.',
|
|
4815
6469
|
);
|
|
4816
6470
|
});
|
|
4817
6471
|
|
|
4818
|
-
it("
|
|
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
|
+
|
|
4819
6481
|
expect(
|
|
4820
|
-
validateToolcraftAcceptanceCoverage(
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
6482
|
+
validateToolcraftAcceptanceCoverage(
|
|
6483
|
+
schemaWithPlaybackTimeline,
|
|
6484
|
+
[
|
|
6485
|
+
...starterAcceptance,
|
|
6486
|
+
{
|
|
6487
|
+
...playbackTimelineAcceptance,
|
|
6488
|
+
id: "reference.timeline.playback",
|
|
6489
|
+
referenceTimelineCoverage: "playback",
|
|
6490
|
+
},
|
|
4827
6491
|
],
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
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
|
+
),
|
|
4833
6514
|
).toContain(
|
|
4834
|
-
|
|
6515
|
+
"panels.timeline.defaultDurationSeconds (8) must match referenceTimeline.loopDuration.seconds (6).",
|
|
4835
6516
|
);
|
|
4836
6517
|
});
|
|
4837
6518
|
|
|
4838
|
-
it("requires
|
|
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
|
+
|
|
4839
6528
|
expect(
|
|
4840
|
-
validateToolcraftAcceptanceCoverage(
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
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
|
+
),
|
|
4847
6553
|
).toContain(
|
|
4848
|
-
'referenceTimeline mode "toolcraft-
|
|
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.',
|
|
4849
6555
|
);
|
|
4850
6556
|
});
|
|
4851
6557
|
|
|
@@ -4885,7 +6591,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
4885
6591
|
],
|
|
4886
6592
|
title: "Controls",
|
|
4887
6593
|
},
|
|
4888
|
-
timeline: { enabled: true, mode: "playback" as const },
|
|
6594
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
4889
6595
|
},
|
|
4890
6596
|
} as const;
|
|
4891
6597
|
|
|
@@ -5017,10 +6723,13 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5017
6723
|
});
|
|
5018
6724
|
|
|
5019
6725
|
it("accepts explicit autonomous animation intent for decorative self-running output", () => {
|
|
5020
|
-
const schemaWithAutonomousAnimation = {
|
|
5021
|
-
|
|
6726
|
+
const schemaWithAutonomousAnimation = defineToolcraft({
|
|
6727
|
+
canvas: {
|
|
6728
|
+
enabled: true,
|
|
6729
|
+
sizing: { mode: "editable-output" },
|
|
6730
|
+
upload: true,
|
|
6731
|
+
},
|
|
5022
6732
|
panels: {
|
|
5023
|
-
...starterSchema.panels,
|
|
5024
6733
|
controls: {
|
|
5025
6734
|
sections: [
|
|
5026
6735
|
{
|
|
@@ -5043,7 +6752,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5043
6752
|
},
|
|
5044
6753
|
timeline: undefined,
|
|
5045
6754
|
},
|
|
5046
|
-
|
|
6755
|
+
toolbar: {
|
|
6756
|
+
history: true,
|
|
6757
|
+
radar: true,
|
|
6758
|
+
zoom: true,
|
|
6759
|
+
},
|
|
6760
|
+
});
|
|
5047
6761
|
|
|
5048
6762
|
expect(
|
|
5049
6763
|
validateToolcraftAcceptanceCoverage(
|
|
@@ -5069,12 +6783,109 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5069
6783
|
).toEqual([]);
|
|
5070
6784
|
});
|
|
5071
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.",
|
|
6874
|
+
},
|
|
6875
|
+
mode: "new-toolcraft-app",
|
|
6876
|
+
},
|
|
6877
|
+
),
|
|
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
|
+
);
|
|
6881
|
+
});
|
|
6882
|
+
|
|
5072
6883
|
it("rejects timeline animation intent when the timeline mode does not match", () => {
|
|
5073
6884
|
const schemaWithPlaybackTimeline = {
|
|
5074
6885
|
...starterSchema,
|
|
5075
6886
|
panels: {
|
|
5076
6887
|
...starterSchema.panels,
|
|
5077
|
-
timeline: { enabled: true, mode: "playback" as const },
|
|
6888
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
5078
6889
|
},
|
|
5079
6890
|
};
|
|
5080
6891
|
|
|
@@ -5083,7 +6894,10 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5083
6894
|
schemaWithPlaybackTimeline,
|
|
5084
6895
|
[playbackTimelineAcceptance],
|
|
5085
6896
|
{
|
|
5086
|
-
animationIntent: {
|
|
6897
|
+
animationIntent: {
|
|
6898
|
+
loopDuration: productDerivedEightSecondLoop,
|
|
6899
|
+
mode: "timeline-keyframes",
|
|
6900
|
+
},
|
|
5087
6901
|
mode: "new-toolcraft-app",
|
|
5088
6902
|
},
|
|
5089
6903
|
),
|
|
@@ -5199,7 +7013,36 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5199
7013
|
{
|
|
5200
7014
|
behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
|
|
5201
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
|
+
]),
|
|
5202
7044
|
referenceName: "legacy state timeline",
|
|
7045
|
+
referenceStudy: referenceStudyEvidence,
|
|
5203
7046
|
referenceTimeline: {
|
|
5204
7047
|
behaviorCoverage: ["state-jump", "trim-range"],
|
|
5205
7048
|
mode: "custom-reference-timeline",
|
|
@@ -5451,7 +7294,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5451
7294
|
}
|
|
5452
7295
|
});
|
|
5453
7296
|
|
|
5454
|
-
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", () => {
|
|
5455
7298
|
const browserTestSources = readBrowserTestSources();
|
|
5456
7299
|
|
|
5457
7300
|
for (const entry of starterAcceptance) {
|
|
@@ -5461,7 +7304,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5461
7304
|
|
|
5462
7305
|
expect(
|
|
5463
7306
|
browserTestSources,
|
|
5464
|
-
`${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}".`,
|
|
5465
7308
|
).toContain(entry.browserTestName);
|
|
5466
7309
|
|
|
5467
7310
|
if (entry.timelineCoverage === "playback" && acceptanceCoversTimelineDurationEdit(entry)) {
|
|
@@ -5538,10 +7381,13 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5538
7381
|
});
|
|
5539
7382
|
|
|
5540
7383
|
it("accepts stepped continuous sliders without forcing the discrete visual variant", () => {
|
|
5541
|
-
const schemaWithAmbiguousSlider = {
|
|
5542
|
-
|
|
7384
|
+
const schemaWithAmbiguousSlider = defineToolcraft({
|
|
7385
|
+
canvas: {
|
|
7386
|
+
enabled: true,
|
|
7387
|
+
sizing: { mode: "editable-output" },
|
|
7388
|
+
upload: true,
|
|
7389
|
+
},
|
|
5543
7390
|
panels: {
|
|
5544
|
-
...starterSchema.panels,
|
|
5545
7391
|
controls: {
|
|
5546
7392
|
sections: [
|
|
5547
7393
|
{
|
|
@@ -5564,7 +7410,12 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5564
7410
|
},
|
|
5565
7411
|
timeline: undefined,
|
|
5566
7412
|
},
|
|
5567
|
-
|
|
7413
|
+
toolbar: {
|
|
7414
|
+
history: true,
|
|
7415
|
+
radar: true,
|
|
7416
|
+
zoom: true,
|
|
7417
|
+
},
|
|
7418
|
+
});
|
|
5568
7419
|
|
|
5569
7420
|
expect(
|
|
5570
7421
|
validateToolcraftAcceptanceCoverage(schemaWithAmbiguousSlider, [
|
|
@@ -6053,8 +7904,8 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6053
7904
|
controls: schema.panels.controls
|
|
6054
7905
|
? {
|
|
6055
7906
|
...schema.panels.controls,
|
|
6056
|
-
sections: schema.panels.controls.sections.map((section
|
|
6057
|
-
|
|
7907
|
+
sections: schema.panels.controls.sections.map((section) =>
|
|
7908
|
+
section.title === "Shader" ? { ...section, title: undefined } : section,
|
|
6058
7909
|
),
|
|
6059
7910
|
}
|
|
6060
7911
|
: schema.panels.controls,
|
|
@@ -6067,7 +7918,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6067
7918
|
]),
|
|
6068
7919
|
).toEqual(
|
|
6069
7920
|
expect.arrayContaining([
|
|
6070
|
-
"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.",
|
|
6071
7922
|
]),
|
|
6072
7923
|
);
|
|
6073
7924
|
});
|
|
@@ -6816,7 +8667,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6816
8667
|
]),
|
|
6817
8668
|
).toEqual(
|
|
6818
8669
|
expect.arrayContaining([
|
|
6819
|
-
'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
|
|
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.',
|
|
6820
8671
|
]),
|
|
6821
8672
|
);
|
|
6822
8673
|
});
|
|
@@ -6869,7 +8720,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
6869
8720
|
]),
|
|
6870
8721
|
).toEqual(
|
|
6871
8722
|
expect.arrayContaining([
|
|
6872
|
-
'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
|
|
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.',
|
|
6873
8724
|
]),
|
|
6874
8725
|
);
|
|
6875
8726
|
});
|
|
@@ -7221,49 +9072,131 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
7221
9072
|
],
|
|
7222
9073
|
title: "Controls",
|
|
7223
9074
|
},
|
|
7224
|
-
},
|
|
7225
|
-
});
|
|
7226
|
-
|
|
7227
|
-
expect(
|
|
7228
|
-
validateToolcraftAcceptanceCoverage(schemaWithUsefulColorLabels, [
|
|
7229
|
-
makeControlAcceptance("palette.accent1", "color"),
|
|
7230
|
-
makeControlAcceptance("palette.accent2", "color"),
|
|
7231
|
-
makeControlAcceptance("palette.spread", "slider"),
|
|
7232
|
-
makeControlAcceptance("object.fill", "color"),
|
|
7233
|
-
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
|
+
},
|
|
7234
9158
|
]),
|
|
7235
|
-
).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
|
+
);
|
|
7236
9164
|
});
|
|
7237
9165
|
|
|
7238
|
-
it("
|
|
7239
|
-
const
|
|
7240
|
-
|
|
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
|
+
},
|
|
7241
9173
|
panels: {
|
|
7242
|
-
...starterSchema.panels,
|
|
7243
9174
|
controls: {
|
|
7244
9175
|
sections: [
|
|
7245
9176
|
{
|
|
7246
9177
|
controls: {
|
|
9178
|
+
blend: {
|
|
9179
|
+
defaultValue: "liquid",
|
|
9180
|
+
label: "Blend",
|
|
9181
|
+
options: [
|
|
9182
|
+
{ label: "Silk", value: "silk" },
|
|
9183
|
+
{ label: "Liquid", value: "liquid" },
|
|
9184
|
+
{ label: "Crystal", value: "crystal" },
|
|
9185
|
+
],
|
|
9186
|
+
orderRole: "mode" as const,
|
|
9187
|
+
target: "shader.blend",
|
|
9188
|
+
type: "segmented",
|
|
9189
|
+
},
|
|
7247
9190
|
depth: {
|
|
7248
9191
|
defaultValue: 0.64,
|
|
7249
9192
|
label: "Depth",
|
|
7250
9193
|
max: 1,
|
|
7251
9194
|
min: 0,
|
|
9195
|
+
orderRole: "strength" as const,
|
|
7252
9196
|
target: "shader.depth",
|
|
7253
9197
|
type: "slider",
|
|
7254
9198
|
variant: "continuous",
|
|
7255
9199
|
},
|
|
7256
|
-
mode: {
|
|
7257
|
-
defaultValue: "liquid",
|
|
7258
|
-
label: "Mode",
|
|
7259
|
-
options: [
|
|
7260
|
-
{ label: "Silk", value: "silk" },
|
|
7261
|
-
{ label: "Liquid", value: "liquid" },
|
|
7262
|
-
{ label: "Crystal", value: "crystal" },
|
|
7263
|
-
],
|
|
7264
|
-
target: "shader.mode",
|
|
7265
|
-
type: "segmented",
|
|
7266
|
-
},
|
|
7267
9200
|
},
|
|
7268
9201
|
title: "Volume",
|
|
7269
9202
|
},
|
|
@@ -7271,10 +9204,34 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
7271
9204
|
title: "Shader",
|
|
7272
9205
|
},
|
|
7273
9206
|
},
|
|
7274
|
-
|
|
9207
|
+
toolbar: {
|
|
9208
|
+
history: true,
|
|
9209
|
+
radar: true,
|
|
9210
|
+
zoom: true,
|
|
9211
|
+
},
|
|
9212
|
+
});
|
|
7275
9213
|
|
|
9214
|
+
expect(getToolcraftControlOrderTargets(schemaWithOrderedControls)).toEqual([
|
|
9215
|
+
"shader.blend",
|
|
9216
|
+
"shader.depth",
|
|
9217
|
+
]);
|
|
7276
9218
|
expect(
|
|
7277
|
-
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
|
+
},
|
|
7278
9235
|
{
|
|
7279
9236
|
automated: true,
|
|
7280
9237
|
automatedTestName: "depth changes rendered output",
|
|
@@ -7289,188 +9246,484 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
7289
9246
|
target: "shader.depth",
|
|
7290
9247
|
userAction: "Drag the Depth slider.",
|
|
7291
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, [
|
|
7292
9287
|
{
|
|
7293
9288
|
automated: true,
|
|
7294
|
-
automatedTestName: "
|
|
9289
|
+
automatedTestName: "fx preset changes rendered output",
|
|
7295
9290
|
browser: true,
|
|
7296
|
-
browserTestName: "browser:
|
|
9291
|
+
browserTestName: "browser: fx preset selector changes rendered output",
|
|
7297
9292
|
componentType: "segmented",
|
|
7298
9293
|
evidence: "rendered-pixels",
|
|
7299
|
-
expectedObservable: "Changing
|
|
7300
|
-
fixture: "
|
|
7301
|
-
id: "shader.
|
|
9294
|
+
expectedObservable: "Changing FX Preset switches the shader preset.",
|
|
9295
|
+
fixture: "fx preset fixture",
|
|
9296
|
+
id: "shader.fxPreset",
|
|
7302
9297
|
kind: "control",
|
|
7303
9298
|
optionCoverage: "each-visible-item",
|
|
7304
|
-
target: "shader.
|
|
7305
|
-
userAction: "Select each
|
|
9299
|
+
target: "shader.fxPreset",
|
|
9300
|
+
userAction: "Select each FX Preset option.",
|
|
7306
9301
|
},
|
|
7307
9302
|
]),
|
|
7308
9303
|
).toEqual(
|
|
7309
9304
|
expect.arrayContaining([
|
|
7310
|
-
|
|
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
|
+
},
|
|
7311
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).",
|
|
7312
9457
|
);
|
|
7313
9458
|
});
|
|
7314
9459
|
|
|
7315
|
-
it("
|
|
7316
|
-
const
|
|
9460
|
+
it("rejects runtime fallback evidence as a loop duration source", () => {
|
|
9461
|
+
const schemaWithPlaybackTimeline = {
|
|
7317
9462
|
...starterSchema,
|
|
7318
9463
|
panels: {
|
|
7319
9464
|
...starterSchema.panels,
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
type: "segmented",
|
|
7335
|
-
},
|
|
7336
|
-
depth: {
|
|
7337
|
-
defaultValue: 0.64,
|
|
7338
|
-
label: "Depth",
|
|
7339
|
-
max: 1,
|
|
7340
|
-
min: 0,
|
|
7341
|
-
orderRole: "strength" as const,
|
|
7342
|
-
target: "shader.depth",
|
|
7343
|
-
type: "slider",
|
|
7344
|
-
variant: "continuous",
|
|
7345
|
-
},
|
|
7346
|
-
},
|
|
7347
|
-
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",
|
|
7348
9479
|
},
|
|
7349
|
-
|
|
7350
|
-
|
|
9480
|
+
mode: "timeline-playback",
|
|
9481
|
+
},
|
|
9482
|
+
mode: "new-toolcraft-app",
|
|
7351
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 },
|
|
7352
9496
|
},
|
|
7353
9497
|
};
|
|
7354
9498
|
|
|
7355
|
-
expect(getToolcraftControlOrderTargets(schemaWithOrderedControls)).toEqual([
|
|
7356
|
-
"shader.blend",
|
|
7357
|
-
"shader.depth",
|
|
7358
|
-
]);
|
|
7359
9499
|
expect(
|
|
7360
|
-
validateToolcraftAcceptanceCoverage(
|
|
7361
|
-
|
|
9500
|
+
validateToolcraftAcceptanceCoverage(
|
|
9501
|
+
schemaWithPlaybackTimeline,
|
|
9502
|
+
[playbackTimelineAcceptance],
|
|
7362
9503
|
{
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
optionCoverage: "each-visible-item",
|
|
7374
|
-
target: "shader.blend",
|
|
7375
|
-
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",
|
|
7376
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],
|
|
7377
9545
|
{
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
target: "shader.depth",
|
|
7389
|
-
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",
|
|
7390
9556
|
},
|
|
7391
|
-
|
|
9557
|
+
),
|
|
7392
9558
|
).toEqual([]);
|
|
7393
9559
|
});
|
|
7394
9560
|
|
|
7395
|
-
it("
|
|
7396
|
-
const
|
|
7397
|
-
|
|
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
|
+
},
|
|
7398
9567
|
panels: {
|
|
7399
|
-
...starterSchema.panels,
|
|
7400
9568
|
controls: {
|
|
7401
|
-
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: [
|
|
7402
9608
|
{
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
label: "FX Preset",
|
|
7407
|
-
options: [
|
|
7408
|
-
{ label: "Full Stack", value: "full-stack" },
|
|
7409
|
-
{ label: "RGB Split", value: "rgb-split" },
|
|
7410
|
-
{ label: "Shade", value: "shade" },
|
|
7411
|
-
{ label: "Lines", value: "lines" },
|
|
7412
|
-
{ label: "Off", value: "off" },
|
|
7413
|
-
],
|
|
7414
|
-
orderRole: "mode" as const,
|
|
7415
|
-
target: "shader.fxPreset",
|
|
7416
|
-
type: "segmented",
|
|
7417
|
-
},
|
|
7418
|
-
},
|
|
7419
|
-
title: "Style",
|
|
9609
|
+
acceptanceId: "missing.acceptance",
|
|
9610
|
+
behavior: "Motion behavior",
|
|
9611
|
+
frameIds: ["f001"],
|
|
7420
9612
|
},
|
|
7421
9613
|
],
|
|
7422
|
-
|
|
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: [],
|
|
7423
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 },
|
|
7424
9646
|
},
|
|
7425
9647
|
};
|
|
7426
9648
|
|
|
7427
9649
|
expect(
|
|
7428
|
-
validateToolcraftAcceptanceCoverage(
|
|
9650
|
+
validateToolcraftAcceptanceCoverage(schemaWithPlaybackTimeline, [
|
|
7429
9651
|
{
|
|
7430
9652
|
automated: true,
|
|
7431
|
-
automatedTestName: "
|
|
9653
|
+
automatedTestName: "timeline duration edit drives renderer output",
|
|
7432
9654
|
browser: true,
|
|
7433
|
-
browserTestName: "browser:
|
|
7434
|
-
componentType: "
|
|
7435
|
-
evidence: "
|
|
7436
|
-
expectedObservable:
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
target: "
|
|
7442
|
-
|
|
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.",
|
|
7443
9673
|
},
|
|
7444
9674
|
]),
|
|
7445
|
-
).
|
|
7446
|
-
|
|
7447
|
-
"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.",
|
|
7448
|
-
]),
|
|
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.',
|
|
7449
9677
|
);
|
|
7450
9678
|
});
|
|
7451
9679
|
|
|
7452
|
-
it("
|
|
9680
|
+
it("rejects incomplete seamless loop evidence that omits fallback direction checks", () => {
|
|
7453
9681
|
const schemaWithPlaybackTimeline = {
|
|
7454
9682
|
...starterSchema,
|
|
7455
9683
|
panels: {
|
|
7456
9684
|
...starterSchema.panels,
|
|
7457
|
-
timeline: { enabled: true, mode: "playback" as const },
|
|
9685
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
7458
9686
|
},
|
|
7459
9687
|
};
|
|
7460
9688
|
|
|
7461
|
-
expect(
|
|
7462
|
-
|
|
7463
|
-
|
|
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
|
+
},
|
|
7464
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.',
|
|
7465
9718
|
);
|
|
7466
9719
|
});
|
|
7467
9720
|
|
|
7468
|
-
it("
|
|
9721
|
+
it("rejects generic seamless loop evidence that omits first-last frame stitching", () => {
|
|
7469
9722
|
const schemaWithPlaybackTimeline = {
|
|
7470
9723
|
...starterSchema,
|
|
7471
9724
|
panels: {
|
|
7472
9725
|
...starterSchema.panels,
|
|
7473
|
-
timeline: { enabled: true, mode: "playback" as const },
|
|
9726
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "playback" as const },
|
|
7474
9727
|
},
|
|
7475
9728
|
};
|
|
7476
9729
|
|
|
@@ -7478,24 +9731,31 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
7478
9731
|
validateToolcraftAcceptanceCoverage(schemaWithPlaybackTimeline, [
|
|
7479
9732
|
{
|
|
7480
9733
|
automated: true,
|
|
7481
|
-
automatedTestName: "timeline
|
|
9734
|
+
automatedTestName: "timeline duration edit verifies a generic loop",
|
|
7482
9735
|
browser: true,
|
|
7483
|
-
browserTestName: "browser: timeline
|
|
9736
|
+
browserTestName: "browser: timeline duration edit verifies a generic loop",
|
|
7484
9737
|
componentType: "timeline",
|
|
7485
9738
|
evidence: "timeline-output",
|
|
7486
9739
|
expectedObservable:
|
|
7487
|
-
"
|
|
9740
|
+
"Editing timeline duration keeps a seamless forward-only loop with no mirror, yoyo, ping-pong, or reverse fallback.",
|
|
7488
9741
|
fixture: "timeline playback fixture",
|
|
7489
9742
|
id: "timeline.playback",
|
|
7490
9743
|
kind: "runtime",
|
|
7491
9744
|
target: "timeline.playback",
|
|
7492
9745
|
timelineCoverage: "playback",
|
|
7493
|
-
timelinePlaybackCoverage: [
|
|
7494
|
-
|
|
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.",
|
|
7495
9755
|
},
|
|
7496
9756
|
]),
|
|
7497
9757
|
).toContain(
|
|
7498
|
-
'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.',
|
|
7499
9759
|
);
|
|
7500
9760
|
});
|
|
7501
9761
|
|
|
@@ -7533,7 +9793,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
7533
9793
|
],
|
|
7534
9794
|
title: "Controls",
|
|
7535
9795
|
},
|
|
7536
|
-
timeline: { enabled: true, mode: "keyframes" as const },
|
|
9796
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "keyframes" as const },
|
|
7537
9797
|
},
|
|
7538
9798
|
};
|
|
7539
9799
|
|
|
@@ -7638,7 +9898,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
7638
9898
|
],
|
|
7639
9899
|
title: "Controls",
|
|
7640
9900
|
},
|
|
7641
|
-
timeline: { enabled: true, mode: "keyframes" as const },
|
|
9901
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "keyframes" as const },
|
|
7642
9902
|
},
|
|
7643
9903
|
};
|
|
7644
9904
|
|
|
@@ -7730,7 +9990,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
7730
9990
|
],
|
|
7731
9991
|
title: "Controls",
|
|
7732
9992
|
},
|
|
7733
|
-
timeline: { enabled: true, mode: "keyframes" as const },
|
|
9993
|
+
timeline: { defaultDurationSeconds: 8, enabled: true, mode: "keyframes" as const },
|
|
7734
9994
|
},
|
|
7735
9995
|
};
|
|
7736
9996
|
|