@pixel-point/toolcraft 0.0.8 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -9
- package/package.json +1 -1
- package/src/generate.mjs +42 -5
- package/src/generate.test.mjs +40 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +251 -47
- package/templates/runtime/contracts/component-contracts.ts +130 -57
- package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
- package/templates/runtime/contracts/decision-contracts.ts +22 -9
- package/templates/runtime/export/export.test.ts +63 -0
- package/templates/runtime/export/export.ts +55 -0
- package/templates/runtime/index.ts +1 -0
- package/templates/runtime/react/canvas-shell.test.tsx +58 -4
- package/templates/runtime/react/canvas-shell.tsx +31 -9
- package/templates/runtime/react/controls-panel.test.tsx +916 -108
- package/templates/runtime/react/controls-panel.tsx +136 -30
- package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
- package/templates/runtime/react/settings-transfer.test.ts +4 -0
- package/templates/runtime/react/settings-transfer.ts +6 -1
- package/templates/runtime/react/timeline-panel.test.tsx +14 -0
- package/templates/runtime/react/timeline-panel.tsx +44 -7
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
- package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
- package/templates/runtime/react/toolcraft-app.tsx +56 -37
- package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
- package/templates/runtime/schema/define-toolcraft.ts +140 -246
- package/templates/runtime/schema/runtime-targets.ts +21 -0
- package/templates/runtime/schema/types.ts +44 -0
- package/templates/runtime/state/create-template-state.test.ts +156 -0
- package/templates/runtime/state/create-template-state.ts +38 -8
- package/templates/runtime/state/media-defaults.ts +105 -0
- package/templates/runtime/state/persistence.test.ts +58 -0
- package/templates/runtime/state/persistence.ts +105 -1
- package/templates/runtime/state/reducer.test.ts +280 -4
- package/templates/runtime/state/reducer.ts +195 -9
- package/templates/runtime/state/timeline-loop.test.ts +71 -0
- package/templates/runtime/state/timeline-loop.ts +35 -0
- package/templates/runtime/state/types.ts +27 -0
- package/templates/runtime/testing/performance.test.ts +810 -21
- package/templates/runtime/testing/performance.ts +823 -53
- package/templates/starter/AGENTS.md +24 -18
- package/templates/starter/docs/toolcraft/README.md +8 -4
- package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
- package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
- package/templates/starter/docs/toolcraft/component-rules.md +75 -42
- package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
- package/templates/starter/docs/toolcraft/performance.md +37 -11
- package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
- package/templates/starter/docs/toolcraft/workflow.md +5 -2
- package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
- package/templates/starter/e2e/app-performance.spec.ts +55 -2
- package/templates/starter/e2e/performance-helpers.ts +45 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/index.html +1 -0
- package/templates/starter/package.json +3 -0
- package/templates/starter/playwright.config.ts +1 -1
- package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
- package/templates/starter/scripts/toolcraft-port.mjs +280 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
- package/templates/starter/src/app/starter-acceptance.ts +1453 -97
- package/templates/starter/src/app/starter-performance.test.ts +111 -7
- package/templates/starter/src/app/starter-performance.ts +5 -0
- package/templates/starter/src/app/starter-schema.test.ts +32 -7
- package/templates/starter/src/app/starter-schema.ts +6 -2
- package/templates/starter/vite.config.ts +58 -2
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/palette-control.tsx +34 -4
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
- package/templates/ui/components/controls/file-drop/index.ts +6 -1
- package/templates/ui/components/controls/index.ts +4 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
- package/templates/ui/components/controls/select/select-control.tsx +8 -25
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
- package/templates/ui/components/panel/panel-actions.tsx +1 -1
- package/templates/ui/components/panel/panel-section.tsx +29 -5
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getToolcraftControlKeyframeCapability,
|
|
3
|
-
getToolcraftSettingsTransferEligibility,
|
|
4
|
-
} from "@repo/toolcraft-runtime";
|
|
1
|
+
import { getToolcraftControlKeyframeCapability } from "@repo/toolcraft-runtime";
|
|
5
2
|
import type {
|
|
6
3
|
ToolcraftActionSchema,
|
|
7
4
|
ToolcraftControlConditionSchema,
|
|
@@ -58,7 +55,7 @@ export type ToolcraftTimelinePlaybackCoverage =
|
|
|
58
55
|
| "rendered-frame"
|
|
59
56
|
| "scrub";
|
|
60
57
|
|
|
61
|
-
export type ToolcraftCanvasSizingCoverage = "fixed-output-size";
|
|
58
|
+
export type ToolcraftCanvasSizingCoverage = "fixed-output-size" | "intrinsic-media-size";
|
|
62
59
|
|
|
63
60
|
export type ToolcraftPersistenceCoverage = "reload";
|
|
64
61
|
|
|
@@ -72,6 +69,17 @@ export type ToolcraftAutonomousAnimationCoverage =
|
|
|
72
69
|
| "no-scrub"
|
|
73
70
|
| "no-user-facing-transport";
|
|
74
71
|
|
|
72
|
+
export type ToolcraftTimelineLoopDurationSource =
|
|
73
|
+
| "product-derived"
|
|
74
|
+
| "reference"
|
|
75
|
+
| "user-request";
|
|
76
|
+
|
|
77
|
+
export type ToolcraftTimelineLoopDurationIntent = {
|
|
78
|
+
evidence: string;
|
|
79
|
+
seconds: number;
|
|
80
|
+
source: ToolcraftTimelineLoopDurationSource;
|
|
81
|
+
};
|
|
82
|
+
|
|
75
83
|
export type ToolcraftAnimationIntent =
|
|
76
84
|
| {
|
|
77
85
|
mode: "none";
|
|
@@ -82,9 +90,11 @@ export type ToolcraftAnimationIntent =
|
|
|
82
90
|
reason: string;
|
|
83
91
|
}
|
|
84
92
|
| {
|
|
93
|
+
loopDuration: ToolcraftTimelineLoopDurationIntent;
|
|
85
94
|
mode: "timeline-keyframes";
|
|
86
95
|
}
|
|
87
96
|
| {
|
|
97
|
+
loopDuration: ToolcraftTimelineLoopDurationIntent;
|
|
88
98
|
mode: "timeline-playback";
|
|
89
99
|
};
|
|
90
100
|
|
|
@@ -96,9 +106,71 @@ export type ToolcraftReferenceTimelineMode =
|
|
|
96
106
|
|
|
97
107
|
export type ToolcraftReferenceTimelineContract = {
|
|
98
108
|
behaviorCoverage: readonly ToolcraftReferenceTimelineCoverage[];
|
|
109
|
+
loopDuration?: ToolcraftTimelineLoopDurationIntent;
|
|
99
110
|
mode: ToolcraftReferenceTimelineMode;
|
|
100
111
|
};
|
|
101
112
|
|
|
113
|
+
export type ToolcraftReferenceStudyStatus =
|
|
114
|
+
| "ran-original"
|
|
115
|
+
| "restored-local"
|
|
116
|
+
| "source-inspection-only";
|
|
117
|
+
|
|
118
|
+
export type ToolcraftReferenceStudyEvidence = {
|
|
119
|
+
behaviorEvidence: string;
|
|
120
|
+
referenceLocation: string;
|
|
121
|
+
reproductionSteps: string;
|
|
122
|
+
sourceEvidence: string;
|
|
123
|
+
sourceOnlyReason?: string;
|
|
124
|
+
status: ToolcraftReferenceStudyStatus;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type ToolcraftVideoReferenceStoryboardFrame = {
|
|
128
|
+
behaviorObservation: string;
|
|
129
|
+
frameId: string;
|
|
130
|
+
frameSource: string;
|
|
131
|
+
timeSeconds: number;
|
|
132
|
+
visualObservation: string;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type ToolcraftVideoReferenceTransition = {
|
|
136
|
+
behaviorDelta: string;
|
|
137
|
+
fromFrameId: string;
|
|
138
|
+
id: string;
|
|
139
|
+
toFrameId: string;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type ToolcraftVideoReferenceAcceptanceMapping = {
|
|
143
|
+
acceptanceId: string;
|
|
144
|
+
behavior: string;
|
|
145
|
+
frameIds: readonly string[];
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export type ToolcraftVideoReferenceStudyEvidence = {
|
|
149
|
+
acceptanceMapping: readonly ToolcraftVideoReferenceAcceptanceMapping[];
|
|
150
|
+
behaviorDecomposition: string;
|
|
151
|
+
extractionEvidence: string;
|
|
152
|
+
referenceLocation: string;
|
|
153
|
+
storyboard: readonly ToolcraftVideoReferenceStoryboardFrame[];
|
|
154
|
+
transitionAnalysis: readonly ToolcraftVideoReferenceTransition[];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type ToolcraftReferenceFeatureStatus =
|
|
158
|
+
| "intentionally-changed"
|
|
159
|
+
| "ported"
|
|
160
|
+
| "toolcraft-native";
|
|
161
|
+
|
|
162
|
+
export type ToolcraftReferenceFeatureInventoryItem = {
|
|
163
|
+
acceptanceId: string;
|
|
164
|
+
behaviorEvidence: string;
|
|
165
|
+
featureName: string;
|
|
166
|
+
id: string;
|
|
167
|
+
referenceBehavior: string;
|
|
168
|
+
sourceEvidence: string;
|
|
169
|
+
status: ToolcraftReferenceFeatureStatus;
|
|
170
|
+
toolcraftMapping: string;
|
|
171
|
+
userApprovedChangeReason?: string;
|
|
172
|
+
};
|
|
173
|
+
|
|
102
174
|
export type ToolcraftLayerCoverage =
|
|
103
175
|
| "grouping"
|
|
104
176
|
| "media-lifecycle"
|
|
@@ -147,6 +219,7 @@ export type ToolcraftCustomControlCoverage =
|
|
|
147
219
|
const builtInToolcraftControlTypeValues = [
|
|
148
220
|
"actions",
|
|
149
221
|
"anchorGrid",
|
|
222
|
+
"aspectRatio",
|
|
150
223
|
"channelMixer",
|
|
151
224
|
"checkbox",
|
|
152
225
|
"code",
|
|
@@ -170,8 +243,8 @@ const builtInToolcraftControlTypeValues = [
|
|
|
170
243
|
"text",
|
|
171
244
|
"vector",
|
|
172
245
|
] as const;
|
|
173
|
-
const
|
|
174
|
-
/\b(
|
|
246
|
+
const insufficientFixedCanvasSizingReasonPattern =
|
|
247
|
+
/\b(no|without|missing|lacks?|does\s+not\s+(?:have|expose)|did\s+not\s+(?:have|expose)|has\s+no|had\s+no)\b[^.]{0,80}\b(?:size|dimension|canvas|output)\b[^.]{0,80}\b(?:editor|controls?|settings?|picker|input|ui)\b|\breference\s+app\b[^.]{0,120}\b(?:no|without|missing|lacks?|does\s+not\s+(?:have|expose)|did\s+not\s+(?:have|expose)|has\s+no|had\s+no)\b[^.]{0,80}\b(?:size|dimension|canvas|output)\b/i;
|
|
175
248
|
|
|
176
249
|
export type ToolcraftBuiltInControlType =
|
|
177
250
|
(typeof builtInToolcraftControlTypeValues)[number];
|
|
@@ -187,14 +260,18 @@ export type ToolcraftTransferMode =
|
|
|
187
260
|
| {
|
|
188
261
|
animationIntent?: ToolcraftAnimationIntent;
|
|
189
262
|
mode: "new-toolcraft-app";
|
|
263
|
+
videoReferenceStudy?: ToolcraftVideoReferenceStudyEvidence;
|
|
190
264
|
}
|
|
191
265
|
| {
|
|
192
266
|
animationIntent?: ToolcraftAnimationIntent;
|
|
193
267
|
behaviorCoverage: readonly ToolcraftReferenceCoverage[];
|
|
194
268
|
mode: "reference-runtime-clone";
|
|
269
|
+
referenceFeatureInventory?: readonly ToolcraftReferenceFeatureInventoryItem[];
|
|
195
270
|
referenceName: string;
|
|
271
|
+
referenceStudy?: ToolcraftReferenceStudyEvidence;
|
|
196
272
|
referenceTimeline: ToolcraftReferenceTimelineContract;
|
|
197
273
|
sourceOfTruth: "reference-runtime";
|
|
274
|
+
videoReferenceStudy?: ToolcraftVideoReferenceStudyEvidence;
|
|
198
275
|
};
|
|
199
276
|
|
|
200
277
|
export type ToolcraftProductReadiness =
|
|
@@ -264,6 +341,15 @@ export type ToolcraftControlOrderItem = {
|
|
|
264
341
|
type: string;
|
|
265
342
|
};
|
|
266
343
|
|
|
344
|
+
export type ToolcraftControlSectionInventoryEntry = {
|
|
345
|
+
entity?: string;
|
|
346
|
+
groupingReason: string;
|
|
347
|
+
splitReason?: string;
|
|
348
|
+
targets: readonly string[];
|
|
349
|
+
title: string;
|
|
350
|
+
workflowStage?: string;
|
|
351
|
+
};
|
|
352
|
+
|
|
267
353
|
export const starterTransferMode: ToolcraftTransferMode = {
|
|
268
354
|
animationIntent: { mode: "none" },
|
|
269
355
|
mode: "new-toolcraft-app",
|
|
@@ -277,10 +363,16 @@ export const starterProductReadiness: ToolcraftProductReadiness = {
|
|
|
277
363
|
|
|
278
364
|
export const starterAcceptance: readonly ToolcraftComponentAcceptance[] = [];
|
|
279
365
|
|
|
366
|
+
export const starterControlSectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = [];
|
|
367
|
+
|
|
280
368
|
function getActionValue(action: ToolcraftActionSchema | string): string {
|
|
281
369
|
return typeof action === "string" ? action : action.value;
|
|
282
370
|
}
|
|
283
371
|
|
|
372
|
+
function getActionLabelText(action: ToolcraftActionSchema | string): string {
|
|
373
|
+
return typeof action === "string" ? action : (action.label ?? action.value);
|
|
374
|
+
}
|
|
375
|
+
|
|
284
376
|
function getActionSearchText(action: ToolcraftActionSchema | string): string {
|
|
285
377
|
return typeof action === "string" ? action : `${action.label} ${action.value} ${action.command ?? ""}`;
|
|
286
378
|
}
|
|
@@ -289,6 +381,19 @@ function isCanvasSizeTarget(target: string): boolean {
|
|
|
289
381
|
return target === "canvas.size.width" || target === "canvas.size.height";
|
|
290
382
|
}
|
|
291
383
|
|
|
384
|
+
const runtimeSetupControlTargets = new Set([
|
|
385
|
+
"runtime.settingsTransfer",
|
|
386
|
+
"canvas.aspectRatio",
|
|
387
|
+
"canvas.renderScale",
|
|
388
|
+
"canvas.size.width",
|
|
389
|
+
"canvas.size.height",
|
|
390
|
+
"panels.timeline.extended",
|
|
391
|
+
]);
|
|
392
|
+
|
|
393
|
+
function isRuntimeSetupControlTarget(target: string): boolean {
|
|
394
|
+
return runtimeSetupControlTargets.has(target);
|
|
395
|
+
}
|
|
396
|
+
|
|
292
397
|
function isResetPanelAction(action: ToolcraftActionSchema | string): boolean {
|
|
293
398
|
return /\breset\b/i.test(getActionSearchText(action));
|
|
294
399
|
}
|
|
@@ -386,9 +491,70 @@ function getAcceptanceEvidenceText(entry: ToolcraftComponentAcceptance): string
|
|
|
386
491
|
].join(" ");
|
|
387
492
|
}
|
|
388
493
|
|
|
494
|
+
function getTimelineLoopDurationIntent(
|
|
495
|
+
animationIntent: ToolcraftAnimationIntent | undefined,
|
|
496
|
+
): ToolcraftTimelineLoopDurationIntent | undefined {
|
|
497
|
+
if (
|
|
498
|
+
animationIntent?.mode !== "timeline-playback" &&
|
|
499
|
+
animationIntent?.mode !== "timeline-keyframes"
|
|
500
|
+
) {
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return animationIntent.loopDuration;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function isValidTimelineLoopDurationSource(source: string): boolean {
|
|
508
|
+
return source === "reference" || source === "user-request" || source === "product-derived";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const runtimeDefaultLoopDurationEvidencePattern =
|
|
512
|
+
/\b(?:runtime|template|generic|fallback|default)\b.{0,40}\b(?:8\s*s|8\s*sec(?:ond)?s?|eight\s*sec(?:ond)?s?)\b|\b(?:8\s*s|8\s*sec(?:ond)?s?|eight\s*sec(?:ond)?s?)\b.{0,40}\b(?:runtime|template|generic|fallback|default)\b/i;
|
|
513
|
+
|
|
514
|
+
function hasSeamlessForwardLoopEvidence(text: string): boolean {
|
|
515
|
+
const hasDurationChange =
|
|
516
|
+
/\b(duration|range|state\.timeline\.durationSeconds|timeline)\b/i.test(text) &&
|
|
517
|
+
/\b(edit|change|changed|changing|commit|set|after)\w*\b/i.test(text);
|
|
518
|
+
const hasFirstLastFrameEvidence =
|
|
519
|
+
/\b(first[-\s/]*(?:and\s+)?last|last[-\s/]*(?:and\s+)?first|wrapped\s+first\s+frame)\b/i.test(
|
|
520
|
+
text,
|
|
521
|
+
) ||
|
|
522
|
+
(/\bfirst\s+frame\b/i.test(text) && /\blast\s+frame\b/i.test(text)) ||
|
|
523
|
+
(/\bstart\s+frame\b/i.test(text) && /\bend\s+frame\b/i.test(text));
|
|
524
|
+
const hasNoVisibleJumpEvidence =
|
|
525
|
+
/\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\bvisible\s+jump\b/i.test(
|
|
526
|
+
text,
|
|
527
|
+
);
|
|
528
|
+
const hasSeamEvidence =
|
|
529
|
+
hasFirstLastFrameEvidence &&
|
|
530
|
+
(/\b(stitch(?:es|ed|ing)?|seam(?:less)?)\b/i.test(text) || hasNoVisibleJumpEvidence);
|
|
531
|
+
const hasForwardDirectionEvidence =
|
|
532
|
+
/\b(forward[-\s]*only|one\s+direction|same\s+direction|advances?\s+in\s+one\s+direction|motion\s+advances?\s+forward|direction\s+does\s+not\s+reverse)\b/i.test(text);
|
|
533
|
+
const hasNoMirrorEvidence =
|
|
534
|
+
/\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\bmirror(?:ed|ing)?\b/i.test(text);
|
|
535
|
+
const hasNoYoyoEvidence =
|
|
536
|
+
/\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\byo-?yo\b/i.test(text);
|
|
537
|
+
const hasNoPingPongEvidence =
|
|
538
|
+
/\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\bping[-\s]*pong\b/i.test(text);
|
|
539
|
+
const hasNoReverseEvidence =
|
|
540
|
+
/\b(no|not|without|avoid(?:s|ing)?|absent|rejects?|disallow(?:s|ed)?|forbid(?:s|den)?)\b.{0,40}\breverse\b/i.test(text);
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
hasDurationChange &&
|
|
544
|
+
hasSeamEvidence &&
|
|
545
|
+
hasForwardDirectionEvidence &&
|
|
546
|
+
hasNoMirrorEvidence &&
|
|
547
|
+
hasNoYoyoEvidence &&
|
|
548
|
+
hasNoPingPongEvidence &&
|
|
549
|
+
hasNoReverseEvidence
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
389
553
|
function getFileDropLifecycleCoverageErrors(
|
|
390
554
|
label: string,
|
|
555
|
+
control: ToolcraftControlSchema,
|
|
391
556
|
entry: ToolcraftComponentAcceptance,
|
|
557
|
+
hasDefaultMediaAssets: boolean,
|
|
392
558
|
): string[] {
|
|
393
559
|
const errors: string[] = [];
|
|
394
560
|
const evidenceText = getAcceptanceEvidenceText(entry);
|
|
@@ -407,10 +573,59 @@ function getFileDropLifecycleCoverageErrors(
|
|
|
407
573
|
!/\b(reset|reset controls|section reset|global reset)\b/i.test(evidenceText)
|
|
408
574
|
) {
|
|
409
575
|
errors.push(
|
|
410
|
-
`${label} fileDrop acceptance must prove upload/import, clear/remove, and section or global reset remove source media.`,
|
|
576
|
+
`${label} 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.`,
|
|
411
577
|
);
|
|
412
578
|
}
|
|
413
579
|
|
|
580
|
+
if (hasDefaultMediaAssets) {
|
|
581
|
+
const provesDefaultRemoval =
|
|
582
|
+
/\b(default|predefined|preset|attached)\b.{0,60}\b(clear|remove|delete|trash)\b/i.test(
|
|
583
|
+
evidenceText,
|
|
584
|
+
) ||
|
|
585
|
+
/\b(clear|remove|delete|trash)\b.{0,60}\b(default|predefined|preset|attached)\b/i.test(
|
|
586
|
+
evidenceText,
|
|
587
|
+
);
|
|
588
|
+
const provesDefaultReset =
|
|
589
|
+
/\b(reset|section reset|global reset)\b.{0,80}\b(restore|restores|return|returns|recreate|recreates)\b.{0,80}\b(default|predefined|preset|attached)\b/i.test(
|
|
590
|
+
evidenceText,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
if (!provesDefaultRemoval || !provesDefaultReset) {
|
|
594
|
+
errors.push(
|
|
595
|
+
`${label} 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.`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (control.assetKind !== "file") {
|
|
601
|
+
const provesRotate = /\b(rotate|rotation|90°|90\s*degrees?)\b/i.test(evidenceText);
|
|
602
|
+
const provesFlip = /\b(flip|flipped)\b/i.test(evidenceText);
|
|
603
|
+
const provesTransformConsumption =
|
|
604
|
+
/\b(mediaAssets\[\]\.transform|runtime media transform|transform metadata|preview|renderer|rendered output|export)\b/i.test(
|
|
605
|
+
evidenceText,
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
if (!provesRotate || !provesFlip || !provesTransformConsumption) {
|
|
609
|
+
errors.push(
|
|
610
|
+
`${label} image fileDrop acceptance must prove rotate and flip actions update runtime media transform metadata and that preview, renderer, or export consumes the transform.`,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (control.multiple === true) {
|
|
616
|
+
const provesReorder = /\b(reorder|sort|sorting|drag|order)\b/i.test(evidenceText);
|
|
617
|
+
const provesOrderConsumption =
|
|
618
|
+
/\b(runtime media order|mediaAssets order|ordered media|preview|renderer|rendered output|export)\b/i.test(
|
|
619
|
+
evidenceText,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
if (!provesReorder || !provesOrderConsumption) {
|
|
623
|
+
errors.push(
|
|
624
|
+
`${label} multiple fileDrop acceptance must prove thumbnail/file reorder updates runtime media order and that preview, renderer, or export consumes that order.`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
414
629
|
return errors;
|
|
415
630
|
}
|
|
416
631
|
|
|
@@ -873,6 +1088,64 @@ function hasVisibleControlLabel(control: ToolcraftControlSchema): boolean {
|
|
|
873
1088
|
return typeof control.label === "string" && control.label.trim().length > 0;
|
|
874
1089
|
}
|
|
875
1090
|
|
|
1091
|
+
const shortSingleLineTextMaxLength = 80;
|
|
1092
|
+
const longTextControlEvidencePattern =
|
|
1093
|
+
/\b(multiline|multi-line|multi\s+line|long\s+(?:text|content|prompt|instructions?)|paragraph|body\s+(?:copy|text)|rich\s+text|textarea|instructions?|script|code|json|css|shader|template|markdown|csv|xml|yaml|html|svg|list|lines|dataset|structured|source\s+text)\b/i;
|
|
1094
|
+
const shortSingleLineTextIntentPattern =
|
|
1095
|
+
/\b(button|label|title|name|caption|headline|cta|badge|token|word|phrase|single-line|one-line|short\s+text)\b/i;
|
|
1096
|
+
|
|
1097
|
+
function getControlDefaultString(control: ToolcraftControlSchema): string | undefined {
|
|
1098
|
+
return typeof control.defaultValue === "string" ? control.defaultValue : undefined;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function getTextValueControlIntentText(
|
|
1102
|
+
controlId: string,
|
|
1103
|
+
control: ToolcraftControlSchema,
|
|
1104
|
+
): string {
|
|
1105
|
+
return [
|
|
1106
|
+
controlId,
|
|
1107
|
+
control.target,
|
|
1108
|
+
getControlLabelText(control),
|
|
1109
|
+
control.description ?? "",
|
|
1110
|
+
]
|
|
1111
|
+
.join(" ")
|
|
1112
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function getCodeTextareaControlError({
|
|
1116
|
+
control,
|
|
1117
|
+
controlId,
|
|
1118
|
+
label,
|
|
1119
|
+
}: {
|
|
1120
|
+
control: ToolcraftControlSchema;
|
|
1121
|
+
controlId: string;
|
|
1122
|
+
label: string;
|
|
1123
|
+
}): string | undefined {
|
|
1124
|
+
if (control.type !== "code") {
|
|
1125
|
+
return undefined;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const defaultValue = getControlDefaultString(control);
|
|
1129
|
+
const intentText = getTextValueControlIntentText(controlId, control);
|
|
1130
|
+
const hasLongTextEvidence =
|
|
1131
|
+
longTextControlEvidencePattern.test(intentText) ||
|
|
1132
|
+
(defaultValue !== undefined &&
|
|
1133
|
+
(defaultValue.length > shortSingleLineTextMaxLength ||
|
|
1134
|
+
/[\r\n]/.test(defaultValue)));
|
|
1135
|
+
const looksLikeShortSingleLineText =
|
|
1136
|
+
shortSingleLineTextIntentPattern.test(intentText) ||
|
|
1137
|
+
(defaultValue !== undefined &&
|
|
1138
|
+
defaultValue.trim().length > 0 &&
|
|
1139
|
+
defaultValue.trim().length <= shortSingleLineTextMaxLength &&
|
|
1140
|
+
!/[\r\n]/.test(defaultValue));
|
|
1141
|
+
|
|
1142
|
+
if (!looksLikeShortSingleLineText || hasLongTextEvidence) {
|
|
1143
|
+
return undefined;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
return `${label} uses CodeTextarea for short single-line text. Use type "text" for button labels, canvas labels, titles, captions, names, badges, tokens, and other short one-line content. Reserve type "code" for long, multiline, or structured values, and document that reason in description when the default is short.`;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
876
1149
|
const singleCurveSemanticPattern =
|
|
877
1150
|
/\b(acceleration|accel|bend|easing|ease|response|depth|mask|opacity|alpha|motion|velocity|threshold|falloff|remap|remapping)\b|speed\s+profile|mapping\s+curve|curve\s+mapping/i;
|
|
878
1151
|
const rgbCurveSemanticPattern =
|
|
@@ -931,6 +1204,33 @@ function getToggleControlLabelError(
|
|
|
931
1204
|
return undefined;
|
|
932
1205
|
}
|
|
933
1206
|
|
|
1207
|
+
function getSingleActionsControlLabelError(
|
|
1208
|
+
control: ToolcraftControlSchema,
|
|
1209
|
+
): string | undefined {
|
|
1210
|
+
if (control.type !== "actions") {
|
|
1211
|
+
return undefined;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const actions = getControlActions(control);
|
|
1215
|
+
|
|
1216
|
+
if (actions.length !== 1) {
|
|
1217
|
+
return undefined;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const label = getControlLabelText(control).trim();
|
|
1221
|
+
const actionLabel = getActionLabelText(actions[0] as ToolcraftActionSchema | string).trim();
|
|
1222
|
+
|
|
1223
|
+
if (
|
|
1224
|
+
label &&
|
|
1225
|
+
actionLabel &&
|
|
1226
|
+
normalizeToolcraftSemanticText(label) === normalizeToolcraftSemanticText(actionLabel)
|
|
1227
|
+
) {
|
|
1228
|
+
return `single Actions control label "${label}" duplicates its only button label "${actionLabel}". Keep the button as the command and use a short context label such as "Ink wash", "Palette action", or "Current layer".`;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return undefined;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
934
1234
|
const maxInlineSwitchLabelLength = 16;
|
|
935
1235
|
const maxInlineSwitchLabelWordCount = 2;
|
|
936
1236
|
|
|
@@ -1281,6 +1581,27 @@ const customReferenceTimelineCoverage = new Set<ToolcraftReferenceTimelineCovera
|
|
|
1281
1581
|
"trim-range",
|
|
1282
1582
|
]);
|
|
1283
1583
|
|
|
1584
|
+
const referenceFeatureStatusValues = new Set<ToolcraftReferenceFeatureStatus>([
|
|
1585
|
+
"intentionally-changed",
|
|
1586
|
+
"ported",
|
|
1587
|
+
"toolcraft-native",
|
|
1588
|
+
]);
|
|
1589
|
+
|
|
1590
|
+
const referenceStudyStatusValues = new Set<ToolcraftReferenceStudyStatus>([
|
|
1591
|
+
"ran-original",
|
|
1592
|
+
"restored-local",
|
|
1593
|
+
"source-inspection-only",
|
|
1594
|
+
]);
|
|
1595
|
+
|
|
1596
|
+
const explicitReferenceChangeReasonPattern =
|
|
1597
|
+
/\b(user|requested|explicit|approved|redesign|change request)\b/i;
|
|
1598
|
+
|
|
1599
|
+
const referenceStudySourceOnlyReasonPattern =
|
|
1600
|
+
/\b(cannot|can't|unable|unavailable|missing|broken|fails?|blocked|no\s+(?:server|deps?|dependency|access)|not\s+(?:runnable|available))\b/i;
|
|
1601
|
+
|
|
1602
|
+
const videoReferenceEvidencePattern =
|
|
1603
|
+
/\b(?:reference\s+(?:video|gif)|screen\s*recording|contact[-\s]*sheet|storyboard|frame[-\s]*by[-\s]*frame|extracted[-\s]*frames?|ffprobe)\b|(?:^|[\\/"'\s])[^\\/"'\s]+\.(?:mp4|mov|webm|gif)\b/i;
|
|
1604
|
+
|
|
1284
1605
|
const timelineTransportControlPattern =
|
|
1285
1606
|
/\b(play|pause|paused|resume|animate|restart)\b/i;
|
|
1286
1607
|
|
|
@@ -1311,6 +1632,184 @@ const requiredTimelinePlaybackCoverage = [
|
|
|
1311
1632
|
"rendered-frame",
|
|
1312
1633
|
] satisfies readonly ToolcraftTimelinePlaybackCoverage[];
|
|
1313
1634
|
|
|
1635
|
+
function getToolcraftTransferModeEvidenceText(
|
|
1636
|
+
transferMode: ToolcraftTransferMode,
|
|
1637
|
+
): string {
|
|
1638
|
+
return JSON.stringify(transferMode);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function validateToolcraftVideoReferenceStudy(
|
|
1642
|
+
study: ToolcraftVideoReferenceStudyEvidence,
|
|
1643
|
+
acceptance: readonly ToolcraftComponentAcceptance[],
|
|
1644
|
+
): string[] {
|
|
1645
|
+
const errors: string[] = [];
|
|
1646
|
+
const acceptanceById = new Map(acceptance.map((entry) => [entry.id, entry]));
|
|
1647
|
+
const frameIds = new Set<string>();
|
|
1648
|
+
|
|
1649
|
+
if (!study.referenceLocation.trim()) {
|
|
1650
|
+
errors.push(
|
|
1651
|
+
"videoReferenceStudy.referenceLocation must name the inspected video, GIF, screen recording, contact sheet, or extracted-frame folder.",
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (!study.extractionEvidence.trim()) {
|
|
1656
|
+
errors.push(
|
|
1657
|
+
"videoReferenceStudy.extractionEvidence must explain how frames were inspected or extracted before implementation.",
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
if (!study.behaviorDecomposition.trim()) {
|
|
1662
|
+
errors.push(
|
|
1663
|
+
"videoReferenceStudy.behaviorDecomposition must decompose the observed frame-to-frame changes into product behavior to preserve.",
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (study.storyboard.length < 4) {
|
|
1668
|
+
errors.push(
|
|
1669
|
+
"videoReferenceStudy.storyboard must include at least four timecoded frames so the reference is studied as motion, not a single screenshot.",
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
for (const [index, frame] of study.storyboard.entries()) {
|
|
1674
|
+
const frameLabel = frame.frameId.trim() || `#${index + 1}`;
|
|
1675
|
+
|
|
1676
|
+
if (!frame.frameId.trim()) {
|
|
1677
|
+
errors.push(`videoReferenceStudy.storyboard frame ${index + 1} must include a stable frameId.`);
|
|
1678
|
+
} else if (frameIds.has(frame.frameId)) {
|
|
1679
|
+
errors.push(
|
|
1680
|
+
`videoReferenceStudy.storyboard frameId "${frame.frameId}" is duplicated; each sampled frame needs a stable id.`,
|
|
1681
|
+
);
|
|
1682
|
+
} else {
|
|
1683
|
+
frameIds.add(frame.frameId);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
if (!Number.isFinite(frame.timeSeconds) || frame.timeSeconds < 0) {
|
|
1687
|
+
errors.push(
|
|
1688
|
+
`videoReferenceStudy.storyboard "${frameLabel}" must include a non-negative finite timeSeconds value.`,
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
if (!frame.frameSource.trim()) {
|
|
1693
|
+
errors.push(
|
|
1694
|
+
`videoReferenceStudy.storyboard "${frameLabel}" must cite the frame image, contact sheet cell, or source timecode inspected.`,
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (!frame.visualObservation.trim()) {
|
|
1699
|
+
errors.push(
|
|
1700
|
+
`videoReferenceStudy.storyboard "${frameLabel}" must describe the visible frame state.`,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
if (!frame.behaviorObservation.trim()) {
|
|
1705
|
+
errors.push(
|
|
1706
|
+
`videoReferenceStudy.storyboard "${frameLabel}" must describe the behavior inferred from that frame.`,
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
if (study.transitionAnalysis.length < 3) {
|
|
1712
|
+
errors.push(
|
|
1713
|
+
"videoReferenceStudy.transitionAnalysis must include at least three frame-to-frame deltas proving how behavior changes between sampled frames.",
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
for (const [index, transition] of study.transitionAnalysis.entries()) {
|
|
1718
|
+
const transitionLabel = transition.id.trim() || `#${index + 1}`;
|
|
1719
|
+
|
|
1720
|
+
if (!transition.id.trim()) {
|
|
1721
|
+
errors.push(
|
|
1722
|
+
`videoReferenceStudy.transitionAnalysis item ${index + 1} must include a stable id.`,
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
if (!frameIds.has(transition.fromFrameId)) {
|
|
1727
|
+
errors.push(
|
|
1728
|
+
`videoReferenceStudy.transitionAnalysis "${transitionLabel}" fromFrameId "${transition.fromFrameId}" must point to a storyboard frameId.`,
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
if (!frameIds.has(transition.toFrameId)) {
|
|
1733
|
+
errors.push(
|
|
1734
|
+
`videoReferenceStudy.transitionAnalysis "${transitionLabel}" toFrameId "${transition.toFrameId}" must point to a storyboard frameId.`,
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
if (!transition.behaviorDelta.trim()) {
|
|
1739
|
+
errors.push(
|
|
1740
|
+
`videoReferenceStudy.transitionAnalysis "${transitionLabel}" must describe the behavior delta between frames.`,
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
if (study.acceptanceMapping.length === 0) {
|
|
1746
|
+
errors.push(
|
|
1747
|
+
"videoReferenceStudy.acceptanceMapping must map observed video behaviors to acceptance rows.",
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
for (const [index, mapping] of study.acceptanceMapping.entries()) {
|
|
1752
|
+
const mappingLabel = mapping.behavior.trim() || `#${index + 1}`;
|
|
1753
|
+
|
|
1754
|
+
if (!mapping.behavior.trim()) {
|
|
1755
|
+
errors.push(
|
|
1756
|
+
`videoReferenceStudy.acceptanceMapping item ${index + 1} must name the observed behavior.`,
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
if (mapping.frameIds.length === 0) {
|
|
1761
|
+
errors.push(
|
|
1762
|
+
`videoReferenceStudy.acceptanceMapping "${mappingLabel}" must cite storyboard frameIds that prove the behavior.`,
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
for (const frameId of mapping.frameIds) {
|
|
1767
|
+
if (!frameIds.has(frameId)) {
|
|
1768
|
+
errors.push(
|
|
1769
|
+
`videoReferenceStudy.acceptanceMapping "${mappingLabel}" frameId "${frameId}" must point to a storyboard frameId.`,
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
const acceptanceId = mapping.acceptanceId.trim();
|
|
1775
|
+
const entry = acceptanceById.get(acceptanceId);
|
|
1776
|
+
|
|
1777
|
+
if (!acceptanceId) {
|
|
1778
|
+
errors.push(
|
|
1779
|
+
`videoReferenceStudy.acceptanceMapping "${mappingLabel}" must include acceptanceId for the test proving the copied behavior.`,
|
|
1780
|
+
);
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (!entry) {
|
|
1785
|
+
errors.push(
|
|
1786
|
+
`videoReferenceStudy.acceptanceMapping "${mappingLabel}" points to missing acceptanceId "${acceptanceId}".`,
|
|
1787
|
+
);
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
if (!entry.automated || !entry.automatedTestName.trim()) {
|
|
1792
|
+
errors.push(
|
|
1793
|
+
`${acceptanceId} must have automated coverage proving video reference behavior "${mappingLabel}".`,
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
if (!entry.browser || !entry.browserTestName.trim()) {
|
|
1798
|
+
errors.push(
|
|
1799
|
+
`${acceptanceId} must have browser coverage proving video reference behavior "${mappingLabel}".`,
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
if (!entry.expectedObservable.trim()) {
|
|
1804
|
+
errors.push(
|
|
1805
|
+
`${acceptanceId} must describe the observable result for video reference behavior "${mappingLabel}".`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
return errors;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1314
1813
|
function isModeSelectorControl(
|
|
1315
1814
|
controlId: string,
|
|
1316
1815
|
control: ToolcraftControlSchema,
|
|
@@ -1319,7 +1818,7 @@ function isModeSelectorControl(
|
|
|
1319
1818
|
return false;
|
|
1320
1819
|
}
|
|
1321
1820
|
|
|
1322
|
-
return /mode|type|filter|
|
|
1821
|
+
return /mode|type|filter|style|preset|variant/i.test(
|
|
1323
1822
|
`${controlId} ${control.target} ${getControlLabelText(control)}`,
|
|
1324
1823
|
);
|
|
1325
1824
|
}
|
|
@@ -1396,7 +1895,7 @@ function getToolcraftControlOrderErrors(schema: ResolvedToolcraftAppSchema): str
|
|
|
1396
1895
|
let previousItem: ToolcraftControlOrderItem | undefined;
|
|
1397
1896
|
|
|
1398
1897
|
for (const [controlId, control] of Object.entries(section.controls)) {
|
|
1399
|
-
if (control.type === "panelActions") {
|
|
1898
|
+
if (control.type === "panelActions" || isRuntimeSetupControlTarget(control.target)) {
|
|
1400
1899
|
continue;
|
|
1401
1900
|
}
|
|
1402
1901
|
|
|
@@ -1425,17 +1924,118 @@ function getToolcraftControlOrderErrors(schema: ResolvedToolcraftAppSchema): str
|
|
|
1425
1924
|
return errors;
|
|
1426
1925
|
}
|
|
1427
1926
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1927
|
+
function getToolcraftRuntimeSetupSectionErrors(
|
|
1928
|
+
schema: ResolvedToolcraftAppSchema,
|
|
1929
|
+
): string[] {
|
|
1930
|
+
const errors: string[] = [];
|
|
1931
|
+
const controlsPanel = schema.panels.controls;
|
|
1430
1932
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1933
|
+
if (!controlsPanel) {
|
|
1934
|
+
return [
|
|
1935
|
+
"Generated Toolcraft apps must define a controls panel so the mandatory runtime Setup section is visible.",
|
|
1936
|
+
];
|
|
1937
|
+
}
|
|
1433
1938
|
|
|
1434
|
-
const
|
|
1435
|
-
/^(appearance|look|looks|properties?|style|styles|values?|visuals?)$/i;
|
|
1939
|
+
const sections = schema.panels.controls?.sections ?? [];
|
|
1436
1940
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1941
|
+
if (sections.length === 0) {
|
|
1942
|
+
return [
|
|
1943
|
+
'Runtime Setup must be the first visible controls-panel section titled "Setup". Do not ship an empty controls panel.',
|
|
1944
|
+
];
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
const setupSection = sections[0];
|
|
1948
|
+
const setupTitle = setupSection?.title?.trim();
|
|
1949
|
+
const setupControls = Object.values(setupSection?.controls ?? {});
|
|
1950
|
+
const setupTargets = new Set(setupControls.map((control) => control.target));
|
|
1951
|
+
const hasSetupTarget = (target: string) => setupTargets.has(target);
|
|
1952
|
+
|
|
1953
|
+
if (setupTitle !== "Setup") {
|
|
1954
|
+
errors.push(
|
|
1955
|
+
'Runtime Setup must be the first visible controls-panel section titled "Setup". Do not move Export Settings, Import Settings, canvas sizing, Resolution scale, or Timeline into app-authored sections.',
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
if (
|
|
1960
|
+
!setupControls.some(
|
|
1961
|
+
(control) =>
|
|
1962
|
+
control.type === "settingsTransfer" &&
|
|
1963
|
+
control.target === "runtime.settingsTransfer",
|
|
1964
|
+
)
|
|
1965
|
+
) {
|
|
1966
|
+
errors.push(
|
|
1967
|
+
'Runtime Setup must include settingsTransfer at target "runtime.settingsTransfer" so Export Settings and Import Settings are always visible.',
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
if (schema.canvas.enabled && schema.canvas.sizing.mode === "editable-output") {
|
|
1972
|
+
const missingCanvasTargets = [
|
|
1973
|
+
"canvas.aspectRatio",
|
|
1974
|
+
"canvas.size.width",
|
|
1975
|
+
"canvas.size.height",
|
|
1976
|
+
].filter((target) => !hasSetupTarget(target));
|
|
1977
|
+
|
|
1978
|
+
if (missingCanvasTargets.length > 0) {
|
|
1979
|
+
errors.push(
|
|
1980
|
+
`Runtime Setup for editable-output canvas must include Aspect ratio, Canvas width, and Canvas height. Missing targets: ${missingCanvasTargets.join(", ")}.`,
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
if (schema.canvas.renderScale.enabled && !hasSetupTarget("canvas.renderScale")) {
|
|
1986
|
+
errors.push(
|
|
1987
|
+
'Runtime Setup must include Resolution scale at target "canvas.renderScale" whenever canvas.renderScale is enabled.',
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (schema.panels.timeline?.enabled) {
|
|
1992
|
+
if (!hasSetupTarget("panels.timeline.extended")) {
|
|
1993
|
+
errors.push(
|
|
1994
|
+
'Runtime Setup must include the Timeline switch at target "panels.timeline.extended" whenever panels.timeline is enabled.',
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
} else if (
|
|
1998
|
+
sections.some((section) =>
|
|
1999
|
+
Object.values(section.controls).some(
|
|
2000
|
+
(control) => control.target === "panels.timeline.extended",
|
|
2001
|
+
),
|
|
2002
|
+
)
|
|
2003
|
+
) {
|
|
2004
|
+
errors.push(
|
|
2005
|
+
'Runtime Setup must not include the Timeline switch unless panels.timeline is enabled.',
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
for (const [sectionIndex, section] of sections.entries()) {
|
|
2010
|
+
const isRuntimeSetupSection = sectionIndex === 0 && section.title?.trim() === "Setup";
|
|
2011
|
+
|
|
2012
|
+
for (const [controlId, control] of Object.entries(section.controls)) {
|
|
2013
|
+
if (!isRuntimeSetupControlTarget(control.target) || isRuntimeSetupSection) {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const sectionLabel = getToolcraftSectionLabel(section.title, sectionIndex);
|
|
2018
|
+
|
|
2019
|
+
errors.push(
|
|
2020
|
+
`${sectionLabel} / ${controlId} uses runtime Setup target "${control.target}". 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.`,
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
return errors;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
const genericControlSectionTitlePattern =
|
|
2029
|
+
/^(controls?|settings?|parameters?|options?|configuration|config|adjustments?)$/i;
|
|
2030
|
+
|
|
2031
|
+
const controlTypeSectionTitlePattern =
|
|
2032
|
+
/^(sliders?|colors?|colours?|inputs?|selects?|switches?|checkboxes?|toggles?|buttons?|actions?)$/i;
|
|
2033
|
+
|
|
2034
|
+
const weakControlLabelContextSectionTitlePattern =
|
|
2035
|
+
/^(appearance|look|looks|properties?|style|styles|values?|visuals?)$/i;
|
|
2036
|
+
|
|
2037
|
+
const broadControlSectionTitlePattern =
|
|
2038
|
+
/^(animation|export|flow|icon|logo|motion|output|scene|shape|shapes|text|typography|visual|visuals)$/i;
|
|
1439
2039
|
|
|
1440
2040
|
const genericControlLabelPattern =
|
|
1441
2041
|
/^(angle|amount|blur|brightness|color|contrast|count|density|depth|frequency|height|hue|intensity|offset|opacity|phase|position|quality|radius|rotation|saturation|scale|size|spacing|speed|strength|threshold|tint|width)$/i;
|
|
@@ -1445,7 +2045,7 @@ const maxHardControlsPerSection = 10;
|
|
|
1445
2045
|
|
|
1446
2046
|
const controlSemanticClusterPatterns: ReadonlyArray<readonly [string, RegExp]> = [
|
|
1447
2047
|
["input", /\b(upload|source|prompt|content|text|phrase|copy|message|file|media|image)\b/i],
|
|
1448
|
-
["mode", /\b(mode|type|preset|style|variant|
|
|
2048
|
+
["mode", /\b(mode|type|preset|style|variant|filter|layout|format|quality)\b/i],
|
|
1449
2049
|
["motion", /\b(animation|speed|velocity|accel|acceleration|correlation|duration|timing|loop|phase|fps|rate)\b/i],
|
|
1450
2050
|
["geometry", /\b(width|height|size|scale|position|offset|anchor|origin|target|radius|distance|spread|bend|curve|curves|path|shape|grid|gap)\b/i],
|
|
1451
2051
|
["density", /\b(fill|density|amount|count|ratio|word|words|letter|letters|particle|particles|layer|layers|island|islands)\b/i],
|
|
@@ -1683,6 +2283,74 @@ function getToolcraftControlSemanticCluster(
|
|
|
1683
2283
|
return inferToolcraftControlOrderRole(controlId, control);
|
|
1684
2284
|
}
|
|
1685
2285
|
|
|
2286
|
+
function normalizeToolcraftConditionValue(value: unknown): string | null {
|
|
2287
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
2288
|
+
const normalized = normalizeToolcraftSemanticText(String(value));
|
|
2289
|
+
return normalized || null;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function getToolcraftConditionBranchTexts(
|
|
2296
|
+
condition: ToolcraftControlConditionSchema,
|
|
2297
|
+
gateControl: ToolcraftControlSchema,
|
|
2298
|
+
): string[] {
|
|
2299
|
+
const rawValues =
|
|
2300
|
+
"equals" in condition
|
|
2301
|
+
? [condition.equals]
|
|
2302
|
+
: Array.isArray(condition.oneOf)
|
|
2303
|
+
? [...condition.oneOf]
|
|
2304
|
+
: [];
|
|
2305
|
+
const normalizedValues = rawValues
|
|
2306
|
+
.map(normalizeToolcraftConditionValue)
|
|
2307
|
+
.filter((value): value is string => Boolean(value));
|
|
2308
|
+
const optionLabels = (gateControl.options ?? [])
|
|
2309
|
+
.filter((option) =>
|
|
2310
|
+
normalizedValues.includes(normalizeToolcraftSemanticText(option.value)),
|
|
2311
|
+
)
|
|
2312
|
+
.map((option) => normalizeToolcraftSemanticText(option.label))
|
|
2313
|
+
.filter(Boolean);
|
|
2314
|
+
|
|
2315
|
+
return [...new Set([...normalizedValues, ...optionLabels])];
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
function sectionTitleLooksLikeDependencyBranch({
|
|
2319
|
+
condition,
|
|
2320
|
+
gateControl,
|
|
2321
|
+
sectionTitle,
|
|
2322
|
+
}: {
|
|
2323
|
+
condition: ToolcraftControlConditionSchema;
|
|
2324
|
+
gateControl: ToolcraftControlSchema;
|
|
2325
|
+
sectionTitle: string | undefined;
|
|
2326
|
+
}): boolean {
|
|
2327
|
+
const sectionText = normalizeToolcraftSemanticText(sectionTitle);
|
|
2328
|
+
|
|
2329
|
+
if (!sectionText) {
|
|
2330
|
+
return false;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
return getToolcraftConditionBranchTexts(condition, gateControl).some(
|
|
2334
|
+
(branchText) =>
|
|
2335
|
+
branchText.length > 2 &&
|
|
2336
|
+
(sectionText === branchText ||
|
|
2337
|
+
sectionText.includes(branchText) ||
|
|
2338
|
+
branchText.includes(sectionText)),
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
function isToolcraftProductSectionControl(control: ToolcraftControlSchema): boolean {
|
|
2343
|
+
return (
|
|
2344
|
+
control.type !== "panelActions" &&
|
|
2345
|
+
control.type !== "settingsTransfer" &&
|
|
2346
|
+
!isRuntimeSetupControlTarget(control.target)
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function isToolcraftVisibleAcceptanceControl(control: ToolcraftControlSchema): boolean {
|
|
2351
|
+
return control.type === "panelActions" || isToolcraftProductSectionControl(control);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
1686
2354
|
function getToolcraftGenericControlLabelError({
|
|
1687
2355
|
control,
|
|
1688
2356
|
controlId,
|
|
@@ -1887,8 +2555,187 @@ function getToolcraftColorBankLabelErrors({
|
|
|
1887
2555
|
return errors;
|
|
1888
2556
|
}
|
|
1889
2557
|
|
|
2558
|
+
function getToolcraftSectionInventoryByTitle(
|
|
2559
|
+
sectionInventory: readonly ToolcraftControlSectionInventoryEntry[],
|
|
2560
|
+
): Map<string, ToolcraftControlSectionInventoryEntry> {
|
|
2561
|
+
return new Map(
|
|
2562
|
+
sectionInventory.map((entry) => [entry.title.trim(), entry]),
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
function hasToolcraftInventorySplitEvidence({
|
|
2567
|
+
sectionInventoryByTitle,
|
|
2568
|
+
sections,
|
|
2569
|
+
}: {
|
|
2570
|
+
sectionInventoryByTitle: ReadonlyMap<string, ToolcraftControlSectionInventoryEntry>;
|
|
2571
|
+
sections: ReadonlySet<string>;
|
|
2572
|
+
}): boolean {
|
|
2573
|
+
const entries = [...sections].map((sectionLabel) =>
|
|
2574
|
+
sectionInventoryByTitle.get(sectionLabel),
|
|
2575
|
+
);
|
|
2576
|
+
|
|
2577
|
+
return (
|
|
2578
|
+
entries.length === sections.size &&
|
|
2579
|
+
entries.every(
|
|
2580
|
+
(entry) =>
|
|
2581
|
+
entry &&
|
|
2582
|
+
(entry.workflowStage?.trim().length ?? 0) > 0 &&
|
|
2583
|
+
(entry.splitReason?.trim().length ?? 0) >= 12,
|
|
2584
|
+
)
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
function getToolcraftControlSectionInventoryErrors(
|
|
2589
|
+
schema: ResolvedToolcraftAppSchema,
|
|
2590
|
+
sectionInventory: readonly ToolcraftControlSectionInventoryEntry[],
|
|
2591
|
+
): string[] {
|
|
2592
|
+
const errors: string[] = [];
|
|
2593
|
+
|
|
2594
|
+
if (sectionInventory.length === 0) {
|
|
2595
|
+
return errors;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
const schemaSections = (schema.panels.controls?.sections ?? []).flatMap(
|
|
2599
|
+
(section, sectionIndex) => {
|
|
2600
|
+
const targets = Object.values(section.controls)
|
|
2601
|
+
.filter(isToolcraftProductSectionControl)
|
|
2602
|
+
.map((control) => control.target);
|
|
2603
|
+
|
|
2604
|
+
if (targets.length === 0) {
|
|
2605
|
+
return [];
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
return [
|
|
2609
|
+
{
|
|
2610
|
+
label: getToolcraftSectionLabel(section.title, sectionIndex),
|
|
2611
|
+
targets,
|
|
2612
|
+
},
|
|
2613
|
+
];
|
|
2614
|
+
},
|
|
2615
|
+
);
|
|
2616
|
+
const schemaSectionTitles = new Set(schemaSections.map((section) => section.label));
|
|
2617
|
+
const schemaTargetToSection = new Map<string, string>();
|
|
2618
|
+
|
|
2619
|
+
for (const section of schemaSections) {
|
|
2620
|
+
for (const target of section.targets) {
|
|
2621
|
+
schemaTargetToSection.set(target, section.label);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
const inventoryTitleCounts = new Map<string, number>();
|
|
2626
|
+
const inventoryTargetToSection = new Map<string, string>();
|
|
2627
|
+
|
|
2628
|
+
for (const entry of sectionInventory) {
|
|
2629
|
+
const title = entry.title.trim();
|
|
2630
|
+
const entity = entry.entity?.trim() ?? "";
|
|
2631
|
+
const workflowStage = entry.workflowStage?.trim() ?? "";
|
|
2632
|
+
const groupingReason = entry.groupingReason.trim();
|
|
2633
|
+
const splitReason = entry.splitReason?.trim() ?? "";
|
|
2634
|
+
|
|
2635
|
+
if (!title) {
|
|
2636
|
+
errors.push(
|
|
2637
|
+
"Control Section Inventory contains an entry without a section title.",
|
|
2638
|
+
);
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
inventoryTitleCounts.set(title, (inventoryTitleCounts.get(title) ?? 0) + 1);
|
|
2643
|
+
|
|
2644
|
+
if (!schemaSectionTitles.has(title)) {
|
|
2645
|
+
errors.push(
|
|
2646
|
+
`Control Section Inventory references "${title}", but no rendered product controls section uses that title.`,
|
|
2647
|
+
);
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
if (!entity && !workflowStage) {
|
|
2651
|
+
errors.push(
|
|
2652
|
+
`Control Section Inventory entry "${title}" must declare entity or workflowStage so grouping is based on product meaning, not UI layout.`,
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
if (groupingReason.length < 12) {
|
|
2657
|
+
errors.push(
|
|
2658
|
+
`Control Section Inventory entry "${title}" must include a concrete groupingReason explaining why these controls belong together.`,
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
if (splitReason && splitReason.length < 12) {
|
|
2663
|
+
errors.push(
|
|
2664
|
+
`Control Section Inventory entry "${title}" splitReason is too vague. Explain the product workflow split or omit splitReason.`,
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
if (entry.targets.length === 0) {
|
|
2669
|
+
errors.push(
|
|
2670
|
+
`Control Section Inventory entry "${title}" must list the product control targets rendered in that section.`,
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
for (const target of entry.targets) {
|
|
2675
|
+
const schemaSection = schemaTargetToSection.get(target);
|
|
2676
|
+
|
|
2677
|
+
if (!schemaSection) {
|
|
2678
|
+
errors.push(
|
|
2679
|
+
`Control Section Inventory entry "${title}" lists target "${target}", but that target is not rendered by any product controls section.`,
|
|
2680
|
+
);
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
if (schemaSection !== title) {
|
|
2685
|
+
errors.push(
|
|
2686
|
+
`Control Section Inventory entry "${title}" lists target "${target}", but the schema renders it in "${schemaSection}". Update the schema grouping or the inventory.`,
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
const existingSection = inventoryTargetToSection.get(target);
|
|
2691
|
+
|
|
2692
|
+
if (existingSection && existingSection !== title) {
|
|
2693
|
+
errors.push(
|
|
2694
|
+
`Control Section Inventory lists target "${target}" in both "${existingSection}" and "${title}". Each product control target belongs to exactly one section inventory entry.`,
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
inventoryTargetToSection.set(target, title);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
for (const [title, count] of inventoryTitleCounts) {
|
|
2703
|
+
if (count > 1) {
|
|
2704
|
+
errors.push(
|
|
2705
|
+
`Control Section Inventory repeats section "${title}" ${count} times. Section inventory entries must be unique.`,
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
for (const section of schemaSections) {
|
|
2711
|
+
const entry = sectionInventory.find(
|
|
2712
|
+
(inventoryEntry) => inventoryEntry.title.trim() === section.label,
|
|
2713
|
+
);
|
|
2714
|
+
|
|
2715
|
+
if (!entry) {
|
|
2716
|
+
errors.push(
|
|
2717
|
+
`Control Section Inventory is missing product section "${section.label}". Add its entity/workflow stage, targets, and groupingReason.`,
|
|
2718
|
+
);
|
|
2719
|
+
continue;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
const inventoryTargets = new Set(entry.targets);
|
|
2723
|
+
|
|
2724
|
+
for (const target of section.targets) {
|
|
2725
|
+
if (!inventoryTargets.has(target)) {
|
|
2726
|
+
errors.push(
|
|
2727
|
+
`Control Section Inventory entry "${section.label}" is missing rendered target "${target}". The inventory must cover every product control in the section.`,
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
return errors;
|
|
2734
|
+
}
|
|
2735
|
+
|
|
1890
2736
|
function getToolcraftControlSectionGroupingErrors(
|
|
1891
2737
|
schema: ResolvedToolcraftAppSchema,
|
|
2738
|
+
sectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = [],
|
|
1892
2739
|
): string[] {
|
|
1893
2740
|
const errors: string[] = [];
|
|
1894
2741
|
const visibleControls: Array<{
|
|
@@ -1896,17 +2743,29 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1896
2743
|
controlId: string;
|
|
1897
2744
|
loosePrefix: string | null;
|
|
1898
2745
|
sectionLabel: string;
|
|
2746
|
+
sectionTitle: string | undefined;
|
|
1899
2747
|
}> = [];
|
|
1900
2748
|
const strictPrefixSections = new Map<string, Set<string>>();
|
|
1901
2749
|
const loosePrefixSections = new Map<string, Set<string>>();
|
|
1902
2750
|
const colorSectionLoosePrefixes = new Map<string, string>();
|
|
2751
|
+
const sectionInventoryByTitle = getToolcraftSectionInventoryByTitle(sectionInventory);
|
|
1903
2752
|
const sectionTitleCounts = new Map<string, { count: number; label: string }>();
|
|
2753
|
+
const controlsByTarget = new Map<
|
|
2754
|
+
string,
|
|
2755
|
+
{
|
|
2756
|
+
control: ToolcraftControlSchema;
|
|
2757
|
+
controlId: string;
|
|
2758
|
+
loosePrefix: string | null;
|
|
2759
|
+
sectionLabel: string;
|
|
2760
|
+
sectionTitle: string | undefined;
|
|
2761
|
+
}
|
|
2762
|
+
>();
|
|
1904
2763
|
|
|
1905
2764
|
for (const [sectionIndex, section] of (schema.panels.controls?.sections ?? []).entries()) {
|
|
1906
2765
|
const sectionTitle = section.title?.trim();
|
|
1907
2766
|
const sectionLabel = getToolcraftSectionLabel(sectionTitle, sectionIndex);
|
|
1908
|
-
const controls = Object.entries(section.controls).filter(
|
|
1909
|
-
(
|
|
2767
|
+
const controls = Object.entries(section.controls).filter(([, control]) =>
|
|
2768
|
+
isToolcraftProductSectionControl(control),
|
|
1910
2769
|
);
|
|
1911
2770
|
|
|
1912
2771
|
if (controls.length === 0) {
|
|
@@ -1945,11 +2804,8 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1945
2804
|
.map(([, control]) => getToolcraftLooseTargetPrefix(control.target))
|
|
1946
2805
|
.filter((prefix): prefix is string => Boolean(prefix)),
|
|
1947
2806
|
);
|
|
1948
|
-
const productControls = controls.filter(
|
|
1949
|
-
(
|
|
1950
|
-
control.type !== "settingsTransfer" &&
|
|
1951
|
-
control.target !== "canvas.size.width" &&
|
|
1952
|
-
control.target !== "canvas.size.height",
|
|
2807
|
+
const productControls = controls.filter(([, control]) =>
|
|
2808
|
+
isToolcraftProductSectionControl(control),
|
|
1953
2809
|
);
|
|
1954
2810
|
const semanticClusters = new Set(
|
|
1955
2811
|
productControls.map(([controlId, control]) =>
|
|
@@ -2015,6 +2871,14 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
2015
2871
|
controlId,
|
|
2016
2872
|
loosePrefix,
|
|
2017
2873
|
sectionLabel,
|
|
2874
|
+
sectionTitle,
|
|
2875
|
+
});
|
|
2876
|
+
controlsByTarget.set(control.target, {
|
|
2877
|
+
control,
|
|
2878
|
+
controlId,
|
|
2879
|
+
loosePrefix,
|
|
2880
|
+
sectionLabel,
|
|
2881
|
+
sectionTitle,
|
|
2018
2882
|
});
|
|
2019
2883
|
|
|
2020
2884
|
if (strictPrefix) {
|
|
@@ -2048,6 +2912,42 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
2048
2912
|
}
|
|
2049
2913
|
}
|
|
2050
2914
|
|
|
2915
|
+
for (const item of visibleControls) {
|
|
2916
|
+
for (const [conditionName, condition] of [
|
|
2917
|
+
["visibleWhen", item.control.visibleWhen],
|
|
2918
|
+
["disabledWhen", item.control.disabledWhen],
|
|
2919
|
+
] as const) {
|
|
2920
|
+
if (!condition || !item.loosePrefix) {
|
|
2921
|
+
continue;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
const gateControl = controlsByTarget.get(condition.target);
|
|
2925
|
+
|
|
2926
|
+
if (!gateControl || gateControl.sectionLabel === item.sectionLabel) {
|
|
2927
|
+
continue;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
const sharesTargetEntity =
|
|
2931
|
+
gateControl.loosePrefix !== null &&
|
|
2932
|
+
gateControl.loosePrefix === item.loosePrefix;
|
|
2933
|
+
const looksLikeBranchSection =
|
|
2934
|
+
conditionName === "visibleWhen" &&
|
|
2935
|
+
sectionTitleLooksLikeDependencyBranch({
|
|
2936
|
+
condition,
|
|
2937
|
+
gateControl: gateControl.control,
|
|
2938
|
+
sectionTitle: item.sectionTitle,
|
|
2939
|
+
});
|
|
2940
|
+
|
|
2941
|
+
if (!sharesTargetEntity && !looksLikeBranchSection) {
|
|
2942
|
+
continue;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
errors.push(
|
|
2946
|
+
`${item.sectionLabel} / ${item.controlId} is gated by ${conditionName} target "${condition.target}" in ${gateControl.sectionLabel}, 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.`,
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2051
2951
|
const fontPickerControls = visibleControls.filter(
|
|
2052
2952
|
(item) => item.control.type === "fontPicker" && item.loosePrefix,
|
|
2053
2953
|
);
|
|
@@ -2080,9 +2980,12 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
2080
2980
|
}
|
|
2081
2981
|
|
|
2082
2982
|
for (const [prefix, sections] of strictPrefixSections) {
|
|
2083
|
-
if (
|
|
2983
|
+
if (
|
|
2984
|
+
sections.size > 1 &&
|
|
2985
|
+
!hasToolcraftInventorySplitEvidence({ sectionInventoryByTitle, sections })
|
|
2986
|
+
) {
|
|
2084
2987
|
errors.push(
|
|
2085
|
-
`Controls for product entity "${prefix}" are split across sections: ${[...sections].join(", ")}. Keep controls for the same product entity in one semantic section unless the
|
|
2988
|
+
`Controls for product entity "${prefix}" are split across sections: ${[...sections].join(", ")}. Keep controls for the same product entity in one semantic section unless the Control Section Inventory declares workflowStage and splitReason for every split section.`,
|
|
2086
2989
|
);
|
|
2087
2990
|
}
|
|
2088
2991
|
}
|
|
@@ -2104,11 +3007,13 @@ export function collectToolcraftVisibleControls(
|
|
|
2104
3007
|
schema: ResolvedToolcraftAppSchema = starterSchema,
|
|
2105
3008
|
): ToolcraftVisibleControl[] {
|
|
2106
3009
|
return (schema.panels.controls?.sections ?? []).flatMap((section) =>
|
|
2107
|
-
Object.entries(section.controls)
|
|
2108
|
-
control
|
|
2109
|
-
controlId,
|
|
2110
|
-
|
|
2111
|
-
|
|
3010
|
+
Object.entries(section.controls)
|
|
3011
|
+
.filter(([, control]) => isToolcraftVisibleAcceptanceControl(control))
|
|
3012
|
+
.map(([controlId, control]) => ({
|
|
3013
|
+
control,
|
|
3014
|
+
controlId,
|
|
3015
|
+
sectionTitle: section.title,
|
|
3016
|
+
})),
|
|
2112
3017
|
);
|
|
2113
3018
|
}
|
|
2114
3019
|
|
|
@@ -2125,7 +3030,7 @@ export function getToolcraftControlOrder(
|
|
|
2125
3030
|
): ToolcraftControlOrderItem[] {
|
|
2126
3031
|
return (schema.panels.controls?.sections ?? []).flatMap((section) =>
|
|
2127
3032
|
Object.entries(section.controls)
|
|
2128
|
-
.filter(([, control]) => control
|
|
3033
|
+
.filter(([, control]) => isToolcraftProductSectionControl(control))
|
|
2129
3034
|
.map(([controlId, control]) => {
|
|
2130
3035
|
const role = inferToolcraftControlOrderRole(controlId, control);
|
|
2131
3036
|
|
|
@@ -2151,6 +3056,7 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2151
3056
|
schema: ResolvedToolcraftAppSchema = starterSchema,
|
|
2152
3057
|
acceptance: readonly ToolcraftComponentAcceptance[] = starterAcceptance,
|
|
2153
3058
|
transferMode: ToolcraftTransferMode = starterTransferMode,
|
|
3059
|
+
sectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = starterControlSectionInventory,
|
|
2154
3060
|
): string[] {
|
|
2155
3061
|
const errors: string[] = [];
|
|
2156
3062
|
const controls = collectToolcraftVisibleControls(schema);
|
|
@@ -2163,6 +3069,7 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2163
3069
|
const layersEnabled = Boolean(schema.panels.layers);
|
|
2164
3070
|
const controlTargets = new Set(controls.map(({ control }) => control.target));
|
|
2165
3071
|
const animationIntent = transferMode.animationIntent;
|
|
3072
|
+
const hasVideoExportAction = schemaHasVideoExportPanelAction(schema);
|
|
2166
3073
|
const animationControls = controls.filter(
|
|
2167
3074
|
(visibleControl) =>
|
|
2168
3075
|
visibleControl.control.type !== "panelActions" &&
|
|
@@ -2180,8 +3087,30 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2180
3087
|
"history.undo",
|
|
2181
3088
|
]);
|
|
2182
3089
|
|
|
3090
|
+
errors.push(...getToolcraftRuntimeSetupSectionErrors(schema));
|
|
2183
3091
|
errors.push(...getToolcraftControlOrderErrors(schema));
|
|
2184
|
-
errors.push(...getToolcraftControlSectionGroupingErrors(schema));
|
|
3092
|
+
errors.push(...getToolcraftControlSectionGroupingErrors(schema, sectionInventory));
|
|
3093
|
+
errors.push(...getToolcraftControlSectionInventoryErrors(schema, sectionInventory));
|
|
3094
|
+
|
|
3095
|
+
const transferModeEvidenceText = getToolcraftTransferModeEvidenceText(transferMode);
|
|
3096
|
+
|
|
3097
|
+
if (
|
|
3098
|
+
videoReferenceEvidencePattern.test(transferModeEvidenceText) &&
|
|
3099
|
+
!transferMode.videoReferenceStudy
|
|
3100
|
+
) {
|
|
3101
|
+
errors.push(
|
|
3102
|
+
"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.",
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
if (transferMode.videoReferenceStudy) {
|
|
3107
|
+
errors.push(
|
|
3108
|
+
...validateToolcraftVideoReferenceStudy(
|
|
3109
|
+
transferMode.videoReferenceStudy,
|
|
3110
|
+
acceptance,
|
|
3111
|
+
),
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
2185
3114
|
|
|
2186
3115
|
for (const [sectionIndex, section] of (schema.panels.controls?.sections ?? []).entries()) {
|
|
2187
3116
|
const sectionLabel = getToolcraftSectionLabel(section.title, sectionIndex);
|
|
@@ -2201,6 +3130,16 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2201
3130
|
);
|
|
2202
3131
|
}
|
|
2203
3132
|
|
|
3133
|
+
const segmentedIds = layoutGroup.controls.filter(
|
|
3134
|
+
(controlId) => section.controls[controlId]?.type === "segmented",
|
|
3135
|
+
);
|
|
3136
|
+
|
|
3137
|
+
if (segmentedIds.length > 0) {
|
|
3138
|
+
errors.push(
|
|
3139
|
+
`${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" includes segmented control ${segmentedIds.join(", ")}. Segmented is full-width and must not share a two-column or half-width row; use Select when a finite choice must fit beside another control.`,
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
|
|
2204
3143
|
const switchEntries = layoutGroup.controls
|
|
2205
3144
|
.map((controlId) => [controlId, section.controls[controlId]] as const)
|
|
2206
3145
|
.filter(
|
|
@@ -2242,11 +3181,21 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2242
3181
|
`${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" includes toggle label ${unsafeBooleanLabels.map(([controlId, control]) => `${controlId} "${getInlineSwitchLabelText(controlId, control)}"`).join(", ")} that is too long for a compact toggle-plus-parameter row. Keep the toggle label short, such as "Include" inside Background, or stack the controls.`,
|
|
2243
3182
|
);
|
|
2244
3183
|
}
|
|
3184
|
+
|
|
3185
|
+
const visibleParameterLabels = parameterEntries.filter(
|
|
3186
|
+
([, control]) => control.label !== false,
|
|
3187
|
+
);
|
|
3188
|
+
|
|
3189
|
+
if (visibleParameterLabels.length > 0) {
|
|
3190
|
+
errors.push(
|
|
3191
|
+
`${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" pairs a toggle with parameter labels ${visibleParameterLabels.map(([controlId, control]) => `${controlId} "${getControlLabelText(control)}"`).join(", ")}. In toggle-plus-parameter rows, the non-toggle parameter must use label false; if that label is needed, stack the controls instead.`,
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
2245
3194
|
}
|
|
2246
3195
|
}
|
|
2247
3196
|
|
|
2248
|
-
const sectionControls = Object.entries(section.controls).filter(
|
|
2249
|
-
(
|
|
3197
|
+
const sectionControls = Object.entries(section.controls).filter(([, control]) =>
|
|
3198
|
+
isToolcraftProductSectionControl(control),
|
|
2250
3199
|
);
|
|
2251
3200
|
|
|
2252
3201
|
for (let index = 0; index < sectionControls.length - 1; index += 1) {
|
|
@@ -2282,7 +3231,6 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2282
3231
|
const panelActionsSectionIndex = getFirstPanelActionsSectionIndex(schema);
|
|
2283
3232
|
const imageExportSectionIndex = getSchemaControlsSectionIndexByTitle(schema, "Image Export");
|
|
2284
3233
|
const videoExportSectionIndex = getSchemaControlsSectionIndexByTitle(schema, "Video Export");
|
|
2285
|
-
const hasVideoExportAction = schemaHasVideoExportPanelAction(schema);
|
|
2286
3234
|
const expectedOutputSettingsIndex =
|
|
2287
3235
|
imageExportSectionIndex >= 0 ? imageExportSectionIndex : videoExportSectionIndex;
|
|
2288
3236
|
const finalExportSettingsIndex = hasVideoExportAction
|
|
@@ -2474,6 +3422,12 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2474
3422
|
}
|
|
2475
3423
|
}
|
|
2476
3424
|
|
|
3425
|
+
if (hasVideoExportAction && !timelineMode) {
|
|
3426
|
+
errors.push(
|
|
3427
|
+
'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.',
|
|
3428
|
+
);
|
|
3429
|
+
}
|
|
3430
|
+
|
|
2477
3431
|
if (animationControls.length > 0 && !timelineMode && animationIntent?.mode !== "autonomous") {
|
|
2478
3432
|
errors.push(
|
|
2479
3433
|
[
|
|
@@ -2495,9 +3449,15 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2495
3449
|
);
|
|
2496
3450
|
}
|
|
2497
3451
|
|
|
2498
|
-
if (
|
|
3452
|
+
if (hasVideoExportAction) {
|
|
2499
3453
|
errors.push(
|
|
2500
|
-
'starterTransferMode.animationIntent mode "autonomous"
|
|
3454
|
+
'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.',
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
if (!animationIntent.reason.trim()) {
|
|
3459
|
+
errors.push(
|
|
3460
|
+
'starterTransferMode.animationIntent mode "autonomous" must include a reason explaining why the animation is decorative/self-running and does not need top timeline transport.',
|
|
2501
3461
|
);
|
|
2502
3462
|
}
|
|
2503
3463
|
|
|
@@ -2520,9 +3480,92 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2520
3480
|
);
|
|
2521
3481
|
}
|
|
2522
3482
|
|
|
3483
|
+
if (
|
|
3484
|
+
transferMode.mode === "new-toolcraft-app" &&
|
|
3485
|
+
timelineMode === "playback" &&
|
|
3486
|
+
animationIntent?.mode !== "timeline-playback"
|
|
3487
|
+
) {
|
|
3488
|
+
errors.push(
|
|
3489
|
+
'panels.timeline mode "playback" requires starterTransferMode.animationIntent mode "timeline-playback" with loopDuration provenance.',
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
if (
|
|
3494
|
+
transferMode.mode === "new-toolcraft-app" &&
|
|
3495
|
+
timelineMode === "keyframes" &&
|
|
3496
|
+
animationIntent?.mode !== "timeline-keyframes"
|
|
3497
|
+
) {
|
|
3498
|
+
errors.push(
|
|
3499
|
+
'panels.timeline mode "keyframes" requires starterTransferMode.animationIntent mode "timeline-keyframes" with loopDuration provenance.',
|
|
3500
|
+
);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
if (
|
|
3504
|
+
timelineMode &&
|
|
3505
|
+
(animationIntent?.mode === "timeline-playback" ||
|
|
3506
|
+
animationIntent?.mode === "timeline-keyframes")
|
|
3507
|
+
) {
|
|
3508
|
+
const timelineDefaultDurationSeconds = schema.panels.timeline?.defaultDurationSeconds;
|
|
3509
|
+
const loopDuration = getTimelineLoopDurationIntent(animationIntent);
|
|
3510
|
+
|
|
3511
|
+
if (!loopDuration) {
|
|
3512
|
+
errors.push(
|
|
3513
|
+
`starterTransferMode.animationIntent mode "${animationIntent.mode}" must declare loopDuration with source, seconds, and evidence. Do not let runtime/template fallback duration such as 8s stand in for product loop intent.`,
|
|
3514
|
+
);
|
|
3515
|
+
} else {
|
|
3516
|
+
if (
|
|
3517
|
+
!Number.isFinite(loopDuration.seconds) ||
|
|
3518
|
+
loopDuration.seconds <= 0
|
|
3519
|
+
) {
|
|
3520
|
+
errors.push(
|
|
3521
|
+
`starterTransferMode.animationIntent.loopDuration.seconds must be a positive finite duration; received ${String(loopDuration.seconds)}.`,
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
if (!isValidTimelineLoopDurationSource(loopDuration.source)) {
|
|
3526
|
+
errors.push(
|
|
3527
|
+
`starterTransferMode.animationIntent.loopDuration.source must be "reference", "user-request", or "product-derived"; received "${String(loopDuration.source)}". Runtime/template fallback is not a valid loop-duration source.`,
|
|
3528
|
+
);
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
if (!loopDuration.evidence.trim()) {
|
|
3532
|
+
errors.push(
|
|
3533
|
+
"starterTransferMode.animationIntent.loopDuration.evidence must explain where the initial loop duration came from, such as reference timing, an explicit user request, or a product-derived timing rule.",
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
if (runtimeDefaultLoopDurationEvidencePattern.test(loopDuration.evidence)) {
|
|
3538
|
+
errors.push(
|
|
3539
|
+
"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.",
|
|
3540
|
+
);
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
if (typeof timelineDefaultDurationSeconds !== "number") {
|
|
3544
|
+
errors.push(
|
|
3545
|
+
"Timeline playback/keyframe apps must set panels.timeline.defaultDurationSeconds to the declared loopDuration.seconds so the initial UI duration is not the runtime fallback.",
|
|
3546
|
+
);
|
|
3547
|
+
} else if (
|
|
3548
|
+
Number.isFinite(loopDuration.seconds) &&
|
|
3549
|
+
Math.abs(timelineDefaultDurationSeconds - loopDuration.seconds) > 0.001
|
|
3550
|
+
) {
|
|
3551
|
+
errors.push(
|
|
3552
|
+
`panels.timeline.defaultDurationSeconds (${timelineDefaultDurationSeconds}) must match starterTransferMode.animationIntent.loopDuration.seconds (${loopDuration.seconds}).`,
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
|
|
2523
3558
|
if (transferMode.mode === "reference-runtime-clone") {
|
|
2524
3559
|
const declaredReferenceCoverage = new Set(transferMode.behaviorCoverage);
|
|
2525
3560
|
const referenceTimeline = transferMode.referenceTimeline;
|
|
3561
|
+
const referenceStudy = transferMode.referenceStudy;
|
|
3562
|
+
const referenceFeatureInventory = transferMode.referenceFeatureInventory ?? [];
|
|
3563
|
+
const acceptanceById = new Map(acceptance.map((entry) => [entry.id, entry]));
|
|
3564
|
+
const referenceFeatureIds = new Set<string>();
|
|
3565
|
+
const referenceFeatureAcceptanceIds = new Set<string>();
|
|
3566
|
+
const referenceCoverageFromInventory = new Set<ToolcraftReferenceCoverage>();
|
|
3567
|
+
const referenceTimelineCoverageFromInventory =
|
|
3568
|
+
new Set<ToolcraftReferenceTimelineCoverage>();
|
|
2526
3569
|
|
|
2527
3570
|
if (!schema.assembly.surfaces.canvas.enabled) {
|
|
2528
3571
|
errors.push(
|
|
@@ -2542,6 +3585,180 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2542
3585
|
);
|
|
2543
3586
|
}
|
|
2544
3587
|
|
|
3588
|
+
if (!referenceStudy) {
|
|
3589
|
+
errors.push(
|
|
3590
|
+
"reference-runtime-clone transferMode must declare referenceStudy proving the reference was inspected and, when runnable or reconstructable, run or restored locally before implementation.",
|
|
3591
|
+
);
|
|
3592
|
+
} else {
|
|
3593
|
+
if (!referenceStudyStatusValues.has(referenceStudy.status)) {
|
|
3594
|
+
errors.push(
|
|
3595
|
+
'referenceStudy.status must be "ran-original", "restored-local", or "source-inspection-only".',
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
if (!referenceStudy.referenceLocation.trim()) {
|
|
3600
|
+
errors.push(
|
|
3601
|
+
"referenceStudy.referenceLocation must name the inspected reference folder, URL, artifact, or source location.",
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
if (!referenceStudy.sourceEvidence.trim()) {
|
|
3606
|
+
errors.push(
|
|
3607
|
+
"referenceStudy.sourceEvidence must summarize the source/runtime files, routes, schemas, or assets inspected.",
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
if (!referenceStudy.behaviorEvidence.trim()) {
|
|
3612
|
+
errors.push(
|
|
3613
|
+
"referenceStudy.behaviorEvidence must summarize the runtime/browser behavior checked from the original or restored reference.",
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
if (!referenceStudy.reproductionSteps.trim()) {
|
|
3618
|
+
errors.push(
|
|
3619
|
+
"referenceStudy.reproductionSteps must record how the reference was run, restored in the Toolcraft environment, or why source-only inspection was used.",
|
|
3620
|
+
);
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
if (referenceStudy.status === "source-inspection-only") {
|
|
3624
|
+
if (!referenceStudy.sourceOnlyReason?.trim()) {
|
|
3625
|
+
errors.push(
|
|
3626
|
+
'referenceStudy.status "source-inspection-only" requires sourceOnlyReason explaining why the reference could not be run or restored locally.',
|
|
3627
|
+
);
|
|
3628
|
+
} else if (
|
|
3629
|
+
!referenceStudySourceOnlyReasonPattern.test(referenceStudy.sourceOnlyReason)
|
|
3630
|
+
) {
|
|
3631
|
+
errors.push(
|
|
3632
|
+
'referenceStudy.sourceOnlyReason must state the concrete blocker that made running or restoring the reference unavailable.',
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
if (referenceFeatureInventory.length === 0) {
|
|
3639
|
+
errors.push(
|
|
3640
|
+
"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.",
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
for (const [index, feature] of referenceFeatureInventory.entries()) {
|
|
3645
|
+
const featureLabel = feature.id.trim() || `#${index + 1}`;
|
|
3646
|
+
|
|
3647
|
+
if (!feature.id.trim()) {
|
|
3648
|
+
errors.push(
|
|
3649
|
+
`referenceFeatureInventory item ${index + 1} must include a stable id.`,
|
|
3650
|
+
);
|
|
3651
|
+
} else if (referenceFeatureIds.has(feature.id)) {
|
|
3652
|
+
errors.push(
|
|
3653
|
+
`referenceFeatureInventory id "${feature.id}" is duplicated; each reference feature must be inventoried once.`,
|
|
3654
|
+
);
|
|
3655
|
+
} else {
|
|
3656
|
+
referenceFeatureIds.add(feature.id);
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
if (!feature.featureName.trim()) {
|
|
3660
|
+
errors.push(
|
|
3661
|
+
`referenceFeatureInventory "${featureLabel}" must include a short featureName.`,
|
|
3662
|
+
);
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
if (!feature.sourceEvidence.trim()) {
|
|
3666
|
+
errors.push(
|
|
3667
|
+
`referenceFeatureInventory "${featureLabel}" must include sourceEvidence from the inspected reference source, runtime, UI, or browser behavior.`,
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
if (!feature.behaviorEvidence.trim()) {
|
|
3672
|
+
errors.push(
|
|
3673
|
+
`referenceFeatureInventory "${featureLabel}" must include behaviorEvidence from the original, restored, or source-only reference study proving this feature was observed before Toolcraft mapping.`,
|
|
3674
|
+
);
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
if (!feature.referenceBehavior.trim()) {
|
|
3678
|
+
errors.push(
|
|
3679
|
+
`referenceFeatureInventory "${featureLabel}" must describe the referenceBehavior to preserve.`,
|
|
3680
|
+
);
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
if (!feature.toolcraftMapping.trim()) {
|
|
3684
|
+
errors.push(
|
|
3685
|
+
`referenceFeatureInventory "${featureLabel}" must describe the Toolcraft mapping for the preserved behavior.`,
|
|
3686
|
+
);
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
if (!referenceFeatureStatusValues.has(feature.status)) {
|
|
3690
|
+
errors.push(
|
|
3691
|
+
`referenceFeatureInventory "${featureLabel}" status must be "ported", "toolcraft-native", or "intentionally-changed".`,
|
|
3692
|
+
);
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
if (feature.status === "intentionally-changed") {
|
|
3696
|
+
if (!feature.userApprovedChangeReason?.trim()) {
|
|
3697
|
+
errors.push(
|
|
3698
|
+
`referenceFeatureInventory "${featureLabel}" status "intentionally-changed" requires userApprovedChangeReason.`,
|
|
3699
|
+
);
|
|
3700
|
+
} else if (
|
|
3701
|
+
!explicitReferenceChangeReasonPattern.test(feature.userApprovedChangeReason)
|
|
3702
|
+
) {
|
|
3703
|
+
errors.push(
|
|
3704
|
+
`referenceFeatureInventory "${featureLabel}" userApprovedChangeReason must cite explicit user approval or redesign/change-request evidence.`,
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
const acceptanceId = feature.acceptanceId.trim();
|
|
3710
|
+
|
|
3711
|
+
if (!acceptanceId) {
|
|
3712
|
+
errors.push(
|
|
3713
|
+
`referenceFeatureInventory "${featureLabel}" must include acceptanceId for the test proving the mapped reference behavior.`,
|
|
3714
|
+
);
|
|
3715
|
+
continue;
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
referenceFeatureAcceptanceIds.add(acceptanceId);
|
|
3719
|
+
|
|
3720
|
+
const entry = acceptanceById.get(acceptanceId);
|
|
3721
|
+
|
|
3722
|
+
if (!entry) {
|
|
3723
|
+
errors.push(
|
|
3724
|
+
`referenceFeatureInventory "${featureLabel}" points to missing acceptanceId "${acceptanceId}".`,
|
|
3725
|
+
);
|
|
3726
|
+
continue;
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
if (!entry.referenceCoverage && !entry.referenceTimelineCoverage) {
|
|
3730
|
+
errors.push(
|
|
3731
|
+
`referenceFeatureInventory "${featureLabel}" acceptanceId "${acceptanceId}" must point to an acceptance entry with referenceCoverage or referenceTimelineCoverage.`,
|
|
3732
|
+
);
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
if (entry.referenceCoverage) {
|
|
3736
|
+
referenceCoverageFromInventory.add(entry.referenceCoverage);
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
if (entry.referenceTimelineCoverage) {
|
|
3740
|
+
referenceTimelineCoverageFromInventory.add(entry.referenceTimelineCoverage);
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
if (!entry.automated || !entry.automatedTestName.trim()) {
|
|
3744
|
+
errors.push(
|
|
3745
|
+
`${acceptanceId} must have automated coverage proving inventoried reference feature "${featureLabel}".`,
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
if (!entry.browser || !entry.browserTestName.trim()) {
|
|
3750
|
+
errors.push(
|
|
3751
|
+
`${acceptanceId} must have browser coverage proving inventoried reference feature "${featureLabel}".`,
|
|
3752
|
+
);
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
if (!entry.expectedObservable.trim()) {
|
|
3756
|
+
errors.push(
|
|
3757
|
+
`${acceptanceId} must describe the observable result for inventoried reference feature "${featureLabel}".`,
|
|
3758
|
+
);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
|
|
2545
3762
|
for (const coverage of requiredReferenceCloneCoverage) {
|
|
2546
3763
|
if (!declaredReferenceCoverage.has(coverage)) {
|
|
2547
3764
|
errors.push(
|
|
@@ -2581,6 +3798,14 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2581
3798
|
}
|
|
2582
3799
|
}
|
|
2583
3800
|
|
|
3801
|
+
for (const coverage of declaredReferenceCoverage) {
|
|
3802
|
+
if (!referenceCoverageFromInventory.has(coverage)) {
|
|
3803
|
+
errors.push(
|
|
3804
|
+
`reference-runtime-clone behaviorCoverage "${coverage}" must be represented in referenceFeatureInventory by an item whose acceptanceId points to that referenceCoverage.`,
|
|
3805
|
+
);
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
|
|
2584
3809
|
if (!referenceTimeline) {
|
|
2585
3810
|
errors.push(
|
|
2586
3811
|
'reference-runtime-clone transferMode must declare referenceTimeline with mode "none", "toolcraft-playback", "toolcraft-keyframes", or "custom-reference-timeline".',
|
|
@@ -2594,6 +3819,14 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2594
3819
|
(coverage) => toolcraftReferenceTimelineCoverage.has(coverage),
|
|
2595
3820
|
);
|
|
2596
3821
|
|
|
3822
|
+
for (const coverage of declaredReferenceTimelineCoverage) {
|
|
3823
|
+
if (!referenceTimelineCoverageFromInventory.has(coverage)) {
|
|
3824
|
+
errors.push(
|
|
3825
|
+
`referenceTimeline behaviorCoverage "${coverage}" must be represented in referenceFeatureInventory by an item whose acceptanceId points to that referenceTimelineCoverage.`,
|
|
3826
|
+
);
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
|
|
2597
3830
|
if (referenceTimeline.mode === "none" && declaredReferenceTimelineCoverage.size > 0) {
|
|
2598
3831
|
errors.push(
|
|
2599
3832
|
'referenceTimeline mode "none" must not declare reference timeline behaviorCoverage.',
|
|
@@ -2631,6 +3864,57 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2631
3864
|
);
|
|
2632
3865
|
}
|
|
2633
3866
|
|
|
3867
|
+
if (
|
|
3868
|
+
referenceTimeline.mode === "toolcraft-playback" ||
|
|
3869
|
+
referenceTimeline.mode === "toolcraft-keyframes"
|
|
3870
|
+
) {
|
|
3871
|
+
const timelineDefaultDurationSeconds = schema.panels.timeline?.defaultDurationSeconds;
|
|
3872
|
+
const loopDuration = referenceTimeline.loopDuration;
|
|
3873
|
+
|
|
3874
|
+
if (!loopDuration) {
|
|
3875
|
+
errors.push(
|
|
3876
|
+
`referenceTimeline mode "${referenceTimeline.mode}" must declare loopDuration with source, seconds, and evidence. Do not let runtime/template fallback duration such as 8s stand in for reference loop intent.`,
|
|
3877
|
+
);
|
|
3878
|
+
} else {
|
|
3879
|
+
if (!Number.isFinite(loopDuration.seconds) || loopDuration.seconds <= 0) {
|
|
3880
|
+
errors.push(
|
|
3881
|
+
`referenceTimeline.loopDuration.seconds must be a positive finite duration; received ${String(loopDuration.seconds)}.`,
|
|
3882
|
+
);
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
if (!isValidTimelineLoopDurationSource(loopDuration.source)) {
|
|
3886
|
+
errors.push(
|
|
3887
|
+
`referenceTimeline.loopDuration.source must be "reference", "user-request", or "product-derived"; received "${String(loopDuration.source)}". Runtime/template fallback is not a valid loop-duration source.`,
|
|
3888
|
+
);
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
if (!loopDuration.evidence.trim()) {
|
|
3892
|
+
errors.push(
|
|
3893
|
+
"referenceTimeline.loopDuration.evidence must explain where the reference loop duration came from, such as measured reference timing, an explicit user request, or a product-derived timing rule.",
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
if (runtimeDefaultLoopDurationEvidencePattern.test(loopDuration.evidence)) {
|
|
3898
|
+
errors.push(
|
|
3899
|
+
"referenceTimeline.loopDuration.evidence must not cite the runtime/template fallback 8s default as the reference loop source. Use measured reference timing, an explicit user request, or a product-derived timing rule.",
|
|
3900
|
+
);
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
if (typeof timelineDefaultDurationSeconds !== "number") {
|
|
3904
|
+
errors.push(
|
|
3905
|
+
"Toolcraft reference playback/keyframe timelines must set panels.timeline.defaultDurationSeconds to referenceTimeline.loopDuration.seconds so the initial UI duration is not the runtime fallback.",
|
|
3906
|
+
);
|
|
3907
|
+
} else if (
|
|
3908
|
+
Number.isFinite(loopDuration.seconds) &&
|
|
3909
|
+
Math.abs(timelineDefaultDurationSeconds - loopDuration.seconds) > 0.001
|
|
3910
|
+
) {
|
|
3911
|
+
errors.push(
|
|
3912
|
+
`panels.timeline.defaultDurationSeconds (${timelineDefaultDurationSeconds}) must match referenceTimeline.loopDuration.seconds (${loopDuration.seconds}).`,
|
|
3913
|
+
);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
|
|
2634
3918
|
if (
|
|
2635
3919
|
referenceTimeline.mode === "toolcraft-playback" &&
|
|
2636
3920
|
declaredReferenceTimelineCoverage.has("keyframes")
|
|
@@ -2714,6 +3998,17 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2714
3998
|
}
|
|
2715
3999
|
}
|
|
2716
4000
|
}
|
|
4001
|
+
|
|
4002
|
+
for (const entry of acceptance) {
|
|
4003
|
+
if (
|
|
4004
|
+
(entry.referenceCoverage || entry.referenceTimelineCoverage) &&
|
|
4005
|
+
!referenceFeatureAcceptanceIds.has(entry.id)
|
|
4006
|
+
) {
|
|
4007
|
+
errors.push(
|
|
4008
|
+
`${entry.id} declares reference coverage but is not mapped from referenceFeatureInventory. Every reference acceptance row must correspond to an inventoried reference feature.`,
|
|
4009
|
+
);
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
2717
4012
|
} else {
|
|
2718
4013
|
for (const entry of acceptance) {
|
|
2719
4014
|
if (entry.referenceCoverage) {
|
|
@@ -2786,23 +4081,42 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2786
4081
|
errors.push(
|
|
2787
4082
|
`${playbackEntry.id} 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.`,
|
|
2788
4083
|
);
|
|
2789
|
-
} else
|
|
2790
|
-
const
|
|
4084
|
+
} else {
|
|
4085
|
+
const playbackEvidenceText = [
|
|
2791
4086
|
playbackEntry.automatedTestName,
|
|
2792
4087
|
playbackEntry.browserTestName,
|
|
2793
4088
|
playbackEntry.expectedObservable,
|
|
2794
4089
|
playbackEntry.userAction,
|
|
2795
4090
|
].join(" ");
|
|
2796
4091
|
|
|
2797
|
-
if (
|
|
4092
|
+
if (
|
|
4093
|
+
hasTimelinePlaybackCoveragePart(playbackEntry.timelinePlaybackCoverage, "duration") &&
|
|
4094
|
+
(!/\bduration\b/i.test(playbackEvidenceText) ||
|
|
4095
|
+
!/\b(edit|change|commit|enter|set)\w*\b/i.test(playbackEvidenceText))
|
|
4096
|
+
) {
|
|
2798
4097
|
errors.push(
|
|
2799
4098
|
`${playbackEntry.id} timelinePlaybackCoverage "duration" must describe editing/changing the timeline duration through the UI and proving the renderer follows state.timeline.durationSeconds.`,
|
|
2800
4099
|
);
|
|
2801
4100
|
}
|
|
4101
|
+
|
|
4102
|
+
if (
|
|
4103
|
+
hasTimelinePlaybackCoveragePart(playbackEntry.timelinePlaybackCoverage, "loop") &&
|
|
4104
|
+
!hasSeamlessForwardLoopEvidence(playbackEvidenceText)
|
|
4105
|
+
) {
|
|
4106
|
+
errors.push(
|
|
4107
|
+
`${playbackEntry.id} 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.`,
|
|
4108
|
+
);
|
|
4109
|
+
}
|
|
2802
4110
|
}
|
|
2803
4111
|
}
|
|
2804
4112
|
|
|
2805
4113
|
if (schema.canvas.sizing.mode === "fixed-output") {
|
|
4114
|
+
if (schemaHasPngExportPanelAction(schema) || schemaHasVideoExportPanelAction(schema)) {
|
|
4115
|
+
errors.push(
|
|
4116
|
+
'Product/output apps with export actions must use canvas.sizing mode "editable-output" so Aspect ratio, Canvas width, and Canvas height are always available. Put reference, fixed-format, or user-requested dimensions in canvas.size as the initial value instead of hiding size controls with "fixed-output".',
|
|
4117
|
+
);
|
|
4118
|
+
}
|
|
4119
|
+
|
|
2806
4120
|
const fixedCanvasSizingEntry = acceptance.find(
|
|
2807
4121
|
(entry) =>
|
|
2808
4122
|
entry.kind === "runtime" &&
|
|
@@ -2811,14 +4125,20 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2811
4125
|
|
|
2812
4126
|
if (!fixedCanvasSizingEntry) {
|
|
2813
4127
|
errors.push(
|
|
2814
|
-
'canvas.sizing mode "fixed-output" requires a runtime acceptance entry with canvasSizingCoverage "fixed-output-size" explaining why width and height are intentionally non-editable.
|
|
4128
|
+
'canvas.sizing mode "fixed-output" requires a runtime acceptance entry with canvasSizingCoverage "fixed-output-size" explaining why width and height are intentionally non-editable. Product/output apps must use "editable-output"; user-provided, reference, fixed-format, or base/default sizes belong in canvas.size as editable initial values.',
|
|
2815
4129
|
);
|
|
2816
4130
|
} else {
|
|
2817
4131
|
const evidenceText = getAcceptanceEvidenceText(fixedCanvasSizingEntry);
|
|
2818
4132
|
|
|
2819
4133
|
if (!/(fixed|locked|non-editable|not user-editable|must not edit|reference-defined|product-defined)/i.test(evidenceText)) {
|
|
2820
4134
|
errors.push(
|
|
2821
|
-
`${fixedCanvasSizingEntry.id} canvasSizingCoverage "fixed-output-size" must explain why the
|
|
4135
|
+
`${fixedCanvasSizingEntry.id} canvasSizingCoverage "fixed-output-size" must explain why the output dimensions are intentionally fixed for a non-product/internal fixture, not merely initialized from a default size.`,
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
if (insufficientFixedCanvasSizingReasonPattern.test(evidenceText)) {
|
|
4140
|
+
errors.push(
|
|
4141
|
+
`${fixedCanvasSizingEntry.id} canvasSizingCoverage "fixed-output-size" cannot be justified by the reference or previous app lacking a size editor. Exportable product apps must use editable-output; put reference, product-defined, or fixed-format dimensions in canvas.size as initial values instead of hiding size controls.`,
|
|
2822
4142
|
);
|
|
2823
4143
|
}
|
|
2824
4144
|
|
|
@@ -2836,84 +4156,83 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2836
4156
|
}
|
|
2837
4157
|
}
|
|
2838
4158
|
|
|
2839
|
-
if (
|
|
2840
|
-
|
|
4159
|
+
if (
|
|
4160
|
+
schema.canvas.enabled &&
|
|
4161
|
+
schema.canvas.upload &&
|
|
4162
|
+
schema.canvas.sizing.mode === "intrinsic-media"
|
|
4163
|
+
) {
|
|
4164
|
+
const intrinsicCanvasSizingEntry = acceptance.find(
|
|
2841
4165
|
(entry) =>
|
|
2842
4166
|
entry.kind === "runtime" &&
|
|
2843
|
-
entry.
|
|
4167
|
+
entry.canvasSizingCoverage === "intrinsic-media-size",
|
|
2844
4168
|
);
|
|
2845
4169
|
|
|
2846
|
-
if (!
|
|
4170
|
+
if (!intrinsicCanvasSizingEntry) {
|
|
2847
4171
|
errors.push(
|
|
2848
|
-
'
|
|
4172
|
+
'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.',
|
|
2849
4173
|
);
|
|
2850
4174
|
} else {
|
|
2851
|
-
const evidenceText = getAcceptanceEvidenceText(
|
|
4175
|
+
const evidenceText = getAcceptanceEvidenceText(intrinsicCanvasSizingEntry);
|
|
2852
4176
|
|
|
2853
|
-
if (
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
)
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
if (!persistenceEntry.browser || !persistenceEntry.browserTestName.trim()) {
|
|
4177
|
+
if (
|
|
4178
|
+
!/(media[- ]viewer|source[- ]native|natural dimension|natural size|intrinsic media|intrinsic size|media owns canvas|canvas\.size)/i.test(
|
|
4179
|
+
evidenceText,
|
|
4180
|
+
)
|
|
4181
|
+
) {
|
|
2860
4182
|
errors.push(
|
|
2861
|
-
`${
|
|
4183
|
+
`${intrinsicCanvasSizingEntry.id} canvasSizingCoverage "intrinsic-media-size" must explain why uploaded media natural dimensions intentionally own canvas.size instead of behaving as background/source material inside the current editable output canvas.`,
|
|
2862
4184
|
);
|
|
2863
4185
|
}
|
|
2864
4186
|
|
|
2865
|
-
if (!
|
|
4187
|
+
if (!intrinsicCanvasSizingEntry.automated || !intrinsicCanvasSizingEntry.automatedTestName.trim()) {
|
|
2866
4188
|
errors.push(
|
|
2867
|
-
`${
|
|
4189
|
+
`${intrinsicCanvasSizingEntry.id} must have automated coverage proving intrinsic media sizing.`,
|
|
2868
4190
|
);
|
|
2869
4191
|
}
|
|
2870
4192
|
|
|
2871
|
-
if (
|
|
4193
|
+
if (!intrinsicCanvasSizingEntry.browser || !intrinsicCanvasSizingEntry.browserTestName.trim()) {
|
|
2872
4194
|
errors.push(
|
|
2873
|
-
`${
|
|
4195
|
+
`${intrinsicCanvasSizingEntry.id} must have browser coverage proving intrinsic media sizing.`,
|
|
2874
4196
|
);
|
|
2875
4197
|
}
|
|
2876
4198
|
}
|
|
2877
4199
|
}
|
|
2878
4200
|
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
});
|
|
2882
|
-
|
|
2883
|
-
if (settingsTransferEligibility.eligible && !schema.settingsTransfer.enabled) {
|
|
2884
|
-
const settingsTransferOptOutEntry = acceptance.find(
|
|
4201
|
+
if (schema.persistence.storage === "localStorage") {
|
|
4202
|
+
const persistenceEntry = acceptance.find(
|
|
2885
4203
|
(entry) =>
|
|
2886
4204
|
entry.kind === "runtime" &&
|
|
2887
|
-
entry.
|
|
2888
|
-
entry.settingsTransferCoverage === "opt-out",
|
|
4205
|
+
entry.persistenceCoverage === "reload",
|
|
2889
4206
|
);
|
|
2890
4207
|
|
|
2891
|
-
if (!
|
|
4208
|
+
if (!persistenceEntry) {
|
|
2892
4209
|
errors.push(
|
|
2893
|
-
|
|
2894
|
-
"settingsTransfer is required for this complex product app because settings-transfer eligibility was reached.",
|
|
2895
|
-
`Eligibility: ${settingsTransferEligibility.controlCount} product controls, ${settingsTransferEligibility.sectionCount} product sections, weighted score ${settingsTransferEligibility.score}, reasons ${settingsTransferEligibility.reasons.join(", ")}.`,
|
|
2896
|
-
'Use schema settingsTransfer: "auto" or true, or add a runtime acceptance entry with settingsTransferCoverage "opt-out" explaining why the app is ephemeral, temporary, one-off, not portable, or session-only.',
|
|
2897
|
-
].join(" "),
|
|
4210
|
+
'persistence.storage "localStorage" requires a runtime acceptance entry with persistenceCoverage "reload" proving user-edited persisted state restores after a real browser reload. Settings import/export is not a substitute for persistence.',
|
|
2898
4211
|
);
|
|
2899
4212
|
} else {
|
|
2900
|
-
const evidenceText = getAcceptanceEvidenceText(
|
|
4213
|
+
const evidenceText = getAcceptanceEvidenceText(persistenceEntry);
|
|
4214
|
+
|
|
4215
|
+
if (!persistenceEntry.automated || !persistenceEntry.automatedTestName.trim()) {
|
|
4216
|
+
errors.push(
|
|
4217
|
+
`${persistenceEntry.id} must have automated coverage proving persistence reload behavior.`,
|
|
4218
|
+
);
|
|
4219
|
+
}
|
|
2901
4220
|
|
|
2902
|
-
if (!
|
|
4221
|
+
if (!persistenceEntry.browser || !persistenceEntry.browserTestName.trim()) {
|
|
2903
4222
|
errors.push(
|
|
2904
|
-
`${
|
|
4223
|
+
`${persistenceEntry.id} must have browser coverage proving persistence reload behavior.`,
|
|
2905
4224
|
);
|
|
2906
4225
|
}
|
|
2907
4226
|
|
|
2908
|
-
if (!
|
|
4227
|
+
if (!persistenceEntry.expectedObservable.trim()) {
|
|
2909
4228
|
errors.push(
|
|
2910
|
-
`${
|
|
4229
|
+
`${persistenceEntry.id} must describe the persisted state observable after reload.`,
|
|
2911
4230
|
);
|
|
2912
4231
|
}
|
|
2913
4232
|
|
|
2914
|
-
if (
|
|
4233
|
+
if (!/\b(reload|refresh|reopen|page\.reload)\b/i.test(evidenceText)) {
|
|
2915
4234
|
errors.push(
|
|
2916
|
-
`${
|
|
4235
|
+
`${persistenceEntry.id} persistenceCoverage "reload" must describe changing a user-facing setting, reloading the browser page, and observing the restored value/output.`,
|
|
2917
4236
|
);
|
|
2918
4237
|
}
|
|
2919
4238
|
}
|
|
@@ -2938,11 +4257,25 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2938
4257
|
const isCustomControl = isCustomToolcraftControl(control);
|
|
2939
4258
|
const isSelectedLayerTarget = control.target.startsWith("selectedLayer.");
|
|
2940
4259
|
const toggleLabelError = getToggleControlLabelError(control, sectionTitle);
|
|
4260
|
+
const singleActionsLabelError = getSingleActionsControlLabelError(control);
|
|
4261
|
+
const codeTextareaError = getCodeTextareaControlError({
|
|
4262
|
+
control,
|
|
4263
|
+
controlId,
|
|
4264
|
+
label,
|
|
4265
|
+
});
|
|
2941
4266
|
|
|
2942
4267
|
if (toggleLabelError) {
|
|
2943
4268
|
errors.push(`${label} ${toggleLabelError}`);
|
|
2944
4269
|
}
|
|
2945
4270
|
|
|
4271
|
+
if (singleActionsLabelError) {
|
|
4272
|
+
errors.push(`${label} ${singleActionsLabelError}`);
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
if (codeTextareaError) {
|
|
4276
|
+
errors.push(codeTextareaError);
|
|
4277
|
+
}
|
|
4278
|
+
|
|
2946
4279
|
if (
|
|
2947
4280
|
control.type === "rangeSlider" &&
|
|
2948
4281
|
Array.isArray(control.defaultValue) &&
|
|
@@ -3014,6 +4347,18 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
3014
4347
|
);
|
|
3015
4348
|
}
|
|
3016
4349
|
|
|
4350
|
+
if (control.disabled === true) {
|
|
4351
|
+
errors.push(
|
|
4352
|
+
`${label} sets disabled: true. Generated product panels should show only controls usable in the current state; hide unavailable product controls with visibleWhen instead of rendering disabled controls.`,
|
|
4353
|
+
);
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
if (control.disabledWhen) {
|
|
4357
|
+
errors.push(
|
|
4358
|
+
`${label} uses disabledWhen. Generated product panels should show only controls usable in the current state; hide unavailable product controls with visibleWhen instead of rendering disabled controls.`,
|
|
4359
|
+
);
|
|
4360
|
+
}
|
|
4361
|
+
|
|
3017
4362
|
if (!entry) {
|
|
3018
4363
|
errors.push(`${label} is missing an acceptance entry.`);
|
|
3019
4364
|
continue;
|
|
@@ -3046,7 +4391,18 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
3046
4391
|
}
|
|
3047
4392
|
|
|
3048
4393
|
if (control.type === "fileDrop") {
|
|
3049
|
-
|
|
4394
|
+
const hasDefaultMediaAssets = schema.media.defaultAssets.some(
|
|
4395
|
+
(asset) => asset.sourceTarget === control.target,
|
|
4396
|
+
);
|
|
4397
|
+
|
|
4398
|
+
errors.push(
|
|
4399
|
+
...getFileDropLifecycleCoverageErrors(
|
|
4400
|
+
label,
|
|
4401
|
+
control,
|
|
4402
|
+
entry,
|
|
4403
|
+
hasDefaultMediaAssets,
|
|
4404
|
+
),
|
|
4405
|
+
);
|
|
3050
4406
|
}
|
|
3051
4407
|
|
|
3052
4408
|
if (
|
|
@@ -3065,17 +4421,6 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
3065
4421
|
errors.push(...getBuiltInFitCheckErrors(label, entry, control));
|
|
3066
4422
|
}
|
|
3067
4423
|
|
|
3068
|
-
if (
|
|
3069
|
-
control.disabledWhen &&
|
|
3070
|
-
!/\b(disabled|unavailable|inactive|not editable|not meaningful|no effect|without effect)\b/i.test(
|
|
3071
|
-
getAcceptanceEvidenceText(entry),
|
|
3072
|
-
)
|
|
3073
|
-
) {
|
|
3074
|
-
errors.push(
|
|
3075
|
-
`${label} uses disabledWhen and acceptance must prove the control becomes disabled/unavailable when ${control.disabledWhen.target} reaches the disabling value.`,
|
|
3076
|
-
);
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
4424
|
if (
|
|
3080
4425
|
control.visibleWhen &&
|
|
3081
4426
|
!/\b(visible|shown|show|appears|hidden|hide|hides|not visible|disappear|unavailable)\b/i.test(
|
|
@@ -3143,6 +4488,12 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
3143
4488
|
if (isSliderLikeControl(control)) {
|
|
3144
4489
|
const expectedMarkerCount = getStepMarkerCount(control);
|
|
3145
4490
|
|
|
4491
|
+
if (control.unit === "x") {
|
|
4492
|
+
errors.push(
|
|
4493
|
+
`${label} uses unit "x", but Toolcraft slider values do not use x suffixes. Omit unit for scale, multiplier, intensity, opacity, strength, depth, and shader amount values unless a real measurement unit applies.`,
|
|
4494
|
+
);
|
|
4495
|
+
}
|
|
4496
|
+
|
|
3146
4497
|
if (
|
|
3147
4498
|
control.variant === "discrete" &&
|
|
3148
4499
|
expectedMarkerCount &&
|
|
@@ -3204,7 +4555,12 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
3204
4555
|
}
|
|
3205
4556
|
|
|
3206
4557
|
for (const entry of acceptance) {
|
|
3207
|
-
if (
|
|
4558
|
+
if (
|
|
4559
|
+
entry.kind === "control" &&
|
|
4560
|
+
entry.target &&
|
|
4561
|
+
!controlTargets.has(entry.target) &&
|
|
4562
|
+
!isRuntimeSetupControlTarget(entry.target)
|
|
4563
|
+
) {
|
|
3208
4564
|
errors.push(`${entry.id} points to missing control target ${entry.target}.`);
|
|
3209
4565
|
}
|
|
3210
4566
|
|