@gakim-digital/dexter-bridge 0.11.13 → 0.11.15
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 +9 -12
- package/package.json +3 -2
- package/src/agent.js +31 -9
- package/src/cli.js +85 -17
- package/src/config.js +1 -3
- package/src/framerAgentTools.js +348 -776
- package/src/framerBehaviorContract.js +124 -1326
- package/src/outcomeWorkspace.js +24 -0
- package/src/protocol.js +6 -6
|
@@ -1,118 +1,12 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'appear',
|
|
8
|
-
'key',
|
|
9
|
-
];
|
|
3
|
+
// Dexter does not model Framer's interaction features. The model decides how
|
|
4
|
+
// to build a behavior from Framer's own documentation; this module only checks
|
|
5
|
+
// that the properties the model says carry the behavior exist on the live
|
|
6
|
+
// nodes it names.
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
'inline',
|
|
14
|
-
'floating',
|
|
15
|
-
'modal',
|
|
16
|
-
'navigation',
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
export const BEHAVIOR_LAYOUT_EFFECTS = [
|
|
20
|
-
'none',
|
|
21
|
-
'reflow',
|
|
22
|
-
'overlay',
|
|
23
|
-
'navigation',
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
export const BEHAVIOR_MECHANISMS = [
|
|
27
|
-
'native_link',
|
|
28
|
-
'native_event',
|
|
29
|
-
'native_effect',
|
|
30
|
-
'native_overlay',
|
|
31
|
-
'native_component_variants',
|
|
32
|
-
'existing_component_control',
|
|
33
|
-
'code_component',
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const MECHANISM_CAPABILITIES = {
|
|
37
|
-
native_link: {
|
|
38
|
-
activations: new Set(['tap', 'key']),
|
|
39
|
-
presentations: new Set(['navigation']),
|
|
40
|
-
layoutEffects: new Set(['navigation']),
|
|
41
|
-
stateChange: false,
|
|
42
|
-
reversible: false,
|
|
43
|
-
repeatable: true,
|
|
44
|
-
},
|
|
45
|
-
native_event: {
|
|
46
|
-
activations: new Set(['tap', 'hover', 'appear', 'key']),
|
|
47
|
-
presentations: new Set(['none', 'navigation']),
|
|
48
|
-
layoutEffects: new Set(['none', 'navigation']),
|
|
49
|
-
stateChange: false,
|
|
50
|
-
reversible: false,
|
|
51
|
-
repeatable: true,
|
|
52
|
-
},
|
|
53
|
-
native_effect: {
|
|
54
|
-
activations: new Set(['tap', 'hover', 'appear']),
|
|
55
|
-
presentations: new Set(['none']),
|
|
56
|
-
layoutEffects: new Set(['none']),
|
|
57
|
-
stateChange: false,
|
|
58
|
-
reversible: true,
|
|
59
|
-
repeatable: true,
|
|
60
|
-
},
|
|
61
|
-
native_overlay: {
|
|
62
|
-
activations: new Set(['tap', 'hover', 'appear', 'key']),
|
|
63
|
-
presentations: new Set(['floating', 'modal']),
|
|
64
|
-
layoutEffects: new Set(['overlay']),
|
|
65
|
-
stateChange: true,
|
|
66
|
-
reversible: true,
|
|
67
|
-
repeatable: true,
|
|
68
|
-
},
|
|
69
|
-
native_component_variants: {
|
|
70
|
-
activations: new Set(['tap', 'hover', 'appear', 'key']),
|
|
71
|
-
presentations: new Set(['none', 'inline']),
|
|
72
|
-
layoutEffects: new Set(['none', 'reflow']),
|
|
73
|
-
stateChange: true,
|
|
74
|
-
reversible: true,
|
|
75
|
-
repeatable: true,
|
|
76
|
-
},
|
|
77
|
-
existing_component_control: {
|
|
78
|
-
activations: new Set(BEHAVIOR_ACTIVATIONS),
|
|
79
|
-
presentations: new Set(BEHAVIOR_PRESENTATIONS),
|
|
80
|
-
layoutEffects: new Set(BEHAVIOR_LAYOUT_EFFECTS),
|
|
81
|
-
stateChange: true,
|
|
82
|
-
reversible: true,
|
|
83
|
-
repeatable: true,
|
|
84
|
-
},
|
|
85
|
-
code_component: {
|
|
86
|
-
activations: new Set(BEHAVIOR_ACTIVATIONS),
|
|
87
|
-
presentations: new Set(BEHAVIOR_PRESENTATIONS),
|
|
88
|
-
layoutEffects: new Set(BEHAVIOR_LAYOUT_EFFECTS),
|
|
89
|
-
stateChange: true,
|
|
90
|
-
reversible: true,
|
|
91
|
-
repeatable: true,
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const FRAME_EVENT_NODE_TYPES = new Set([
|
|
96
|
-
'FrameNode',
|
|
97
|
-
'RichTextNode',
|
|
98
|
-
'IconNode',
|
|
99
|
-
'ComponentInstanceNode',
|
|
100
|
-
]);
|
|
101
|
-
|
|
102
|
-
const FRAME_EFFECT_NODE_TYPES = new Set([
|
|
103
|
-
'ComponentInstanceNode',
|
|
104
|
-
'FrameNode',
|
|
105
|
-
'IconNode',
|
|
106
|
-
'RelativeOverlayNode',
|
|
107
|
-
'RichTextNode',
|
|
108
|
-
'ShaderNode',
|
|
109
|
-
]);
|
|
110
|
-
|
|
111
|
-
const NATIVE_EFFECT_BY_ACTIVATION = {
|
|
112
|
-
tap: 'tapEffect',
|
|
113
|
-
hover: 'hoverEffect',
|
|
114
|
-
appear: 'appearEffect',
|
|
115
|
-
};
|
|
8
|
+
const MAX_SEARCH_DEPTH = 6;
|
|
9
|
+
const MAX_OBSERVED_CHARS = 1000;
|
|
116
10
|
|
|
117
11
|
function isRecord(value) {
|
|
118
12
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
@@ -132,1291 +26,195 @@ function uniqueStrings(values, maximum = 50) {
|
|
|
132
26
|
)).slice(0, maximum);
|
|
133
27
|
}
|
|
134
28
|
|
|
135
|
-
function integer(value, fallback = 1, minimum = 1, maximum = 50) {
|
|
136
|
-
const parsed = Number(value);
|
|
137
|
-
return Number.isFinite(parsed)
|
|
138
|
-
? Math.min(maximum, Math.max(minimum, Math.floor(parsed)))
|
|
139
|
-
: fallback;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
29
|
function contractError(code, message) {
|
|
143
30
|
return Object.assign(new Error(message), { code, statusCode: 400 });
|
|
144
31
|
}
|
|
145
32
|
|
|
146
|
-
export function behaviorSignature(behavior) {
|
|
147
|
-
return crypto
|
|
148
|
-
.createHash('sha256')
|
|
149
|
-
.update(JSON.stringify({
|
|
150
|
-
activation: behavior.activation,
|
|
151
|
-
presentation: behavior.presentation,
|
|
152
|
-
stateChange: behavior.stateChange,
|
|
153
|
-
layoutEffect: behavior.layoutEffect,
|
|
154
|
-
reversible: behavior.reversible,
|
|
155
|
-
repeatable: behavior.repeatable,
|
|
156
|
-
expectedTargets: behavior.expectedTargets,
|
|
157
|
-
initialState: behavior.initialState,
|
|
158
|
-
activatedState: behavior.activatedState,
|
|
159
|
-
expectedDestinations: behavior.expectedDestinations,
|
|
160
|
-
}))
|
|
161
|
-
.digest('hex');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
33
|
function normalizeBehavior(rawBehavior, index) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
`Behavior ${index + 1} must be an object.`,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
const id = text(rawBehavior.id, 120);
|
|
172
|
-
const description = text(rawBehavior.description, 500);
|
|
173
|
-
const triggerDescription = text(rawBehavior.triggerDescription, 300);
|
|
174
|
-
const activation = text(rawBehavior.activation, 40);
|
|
175
|
-
const presentation = text(rawBehavior.presentation, 40);
|
|
176
|
-
const layoutEffect = text(rawBehavior.layoutEffect, 40);
|
|
177
|
-
const initialState = text(rawBehavior.initialState, 300);
|
|
178
|
-
const activatedState = text(rawBehavior.activatedState, 300);
|
|
179
|
-
if (
|
|
180
|
-
!id
|
|
181
|
-
|| description.length < 5
|
|
182
|
-
|| triggerDescription.length < 3
|
|
183
|
-
|| !BEHAVIOR_ACTIVATIONS.includes(activation)
|
|
184
|
-
|| !BEHAVIOR_PRESENTATIONS.includes(presentation)
|
|
185
|
-
|| !BEHAVIOR_LAYOUT_EFFECTS.includes(layoutEffect)
|
|
186
|
-
|| !initialState
|
|
187
|
-
|| !activatedState
|
|
188
|
-
) {
|
|
34
|
+
const id = text(rawBehavior?.id, 120);
|
|
35
|
+
const description = text(rawBehavior?.description, 500);
|
|
36
|
+
if (!isRecord(rawBehavior) || !id || description.length < 5) {
|
|
189
37
|
throw contractError(
|
|
190
38
|
'FRAMER_BEHAVIOR_CONTRACT_INVALID',
|
|
191
|
-
`Behavior ${index + 1}
|
|
39
|
+
`Behavior ${index + 1} needs an id and a description of what the user should experience.`,
|
|
192
40
|
);
|
|
193
41
|
}
|
|
194
|
-
|
|
42
|
+
return {
|
|
195
43
|
id,
|
|
196
44
|
description,
|
|
197
|
-
|
|
198
|
-
activation,
|
|
199
|
-
presentation,
|
|
200
|
-
stateChange: rawBehavior.stateChange === true,
|
|
201
|
-
layoutEffect,
|
|
202
|
-
reversible: rawBehavior.reversible === true,
|
|
203
|
-
repeatable: rawBehavior.repeatable === true,
|
|
204
|
-
expectedTargets: integer(rawBehavior.expectedTargets),
|
|
205
|
-
expectedContent: uniqueStrings(rawBehavior.expectedContent, 20),
|
|
206
|
-
expectedDestinations: uniqueStrings(rawBehavior.expectedDestinations, 20),
|
|
207
|
-
initialState,
|
|
208
|
-
activatedState,
|
|
209
|
-
};
|
|
210
|
-
if (
|
|
211
|
-
(behavior.presentation === 'navigation'
|
|
212
|
-
|| behavior.layoutEffect === 'navigation')
|
|
213
|
-
&& behavior.expectedDestinations.length === 0
|
|
214
|
-
) {
|
|
215
|
-
throw contractError(
|
|
216
|
-
'FRAMER_BEHAVIOR_CONTRACT_INVALID',
|
|
217
|
-
`Behavior ${index + 1} must declare expectedDestinations for navigation.`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
return {
|
|
221
|
-
...behavior,
|
|
222
|
-
signature: behaviorSignature(behavior),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function normalizeMechanism(rawMechanism, index) {
|
|
227
|
-
if (!isRecord(rawMechanism)) {
|
|
228
|
-
throw contractError(
|
|
229
|
-
'FRAMER_BEHAVIOR_MECHANISM_INVALID',
|
|
230
|
-
`Mechanism ${index + 1} must be an object.`,
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
const behaviorId = text(rawMechanism.behaviorId, 120);
|
|
234
|
-
const mechanism = text(rawMechanism.mechanism, 80);
|
|
235
|
-
const rationale = text(rawMechanism.rationale, 800);
|
|
236
|
-
if (
|
|
237
|
-
!behaviorId
|
|
238
|
-
|| !BEHAVIOR_MECHANISMS.includes(mechanism)
|
|
239
|
-
|| rationale.length < 10
|
|
240
|
-
) {
|
|
241
|
-
throw contractError(
|
|
242
|
-
'FRAMER_BEHAVIOR_MECHANISM_INVALID',
|
|
243
|
-
`Mechanism ${index + 1} must identify a behavior, a supported mechanism, and a concrete rationale.`,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
return {
|
|
247
|
-
behaviorId,
|
|
248
|
-
mechanism,
|
|
249
|
-
rationale,
|
|
250
|
-
representativeFirst: rawMechanism.representativeFirst !== false,
|
|
251
|
-
targetNodeIds: uniqueStrings(rawMechanism.targetNodeIds, 50),
|
|
252
|
-
representativeTargetNodeId: text(
|
|
253
|
-
rawMechanism.representativeTargetNodeId,
|
|
254
|
-
200,
|
|
255
|
-
),
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function normalizeRejectedMechanisms(values) {
|
|
260
|
-
return (Array.isArray(values) ? values : [])
|
|
261
|
-
.slice(0, 30)
|
|
262
|
-
.flatMap((raw, index) => {
|
|
263
|
-
if (!isRecord(raw)) return [];
|
|
264
|
-
const behaviorId = text(raw.behaviorId, 120);
|
|
265
|
-
const behaviorSignatureValue = text(raw.behaviorSignature, 128);
|
|
266
|
-
const mechanism = text(raw.mechanism, 80);
|
|
267
|
-
const reason = text(raw.reason, 800);
|
|
268
|
-
if (
|
|
269
|
-
(!behaviorId && !behaviorSignatureValue)
|
|
270
|
-
|| !BEHAVIOR_MECHANISMS.includes(mechanism)
|
|
271
|
-
|| reason.length < 5
|
|
272
|
-
) {
|
|
273
|
-
throw contractError(
|
|
274
|
-
'FRAMER_REJECTED_MECHANISM_INVALID',
|
|
275
|
-
`Rejected mechanism ${index + 1} must identify a behavior or signature, a supported mechanism, and a reason.`,
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
return [{
|
|
279
|
-
...(behaviorId ? { behaviorId } : {}),
|
|
280
|
-
...(behaviorSignatureValue
|
|
281
|
-
? { behaviorSignature: behaviorSignatureValue }
|
|
282
|
-
: {}),
|
|
283
|
-
mechanism,
|
|
284
|
-
reason,
|
|
285
|
-
source:
|
|
286
|
-
raw.source === 'user_reported_failure'
|
|
287
|
-
|| raw.source === 'verification_failure'
|
|
288
|
-
? raw.source
|
|
289
|
-
: 'verification_failure',
|
|
290
|
-
}];
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export function mechanismCompatibility(behavior, mechanism) {
|
|
295
|
-
const capabilities = MECHANISM_CAPABILITIES[mechanism];
|
|
296
|
-
if (!capabilities) {
|
|
297
|
-
return {
|
|
298
|
-
compatible: false,
|
|
299
|
-
failures: [`Unknown behavior mechanism ${mechanism}.`],
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
const failures = [];
|
|
303
|
-
if (!capabilities.activations.has(behavior.activation)) {
|
|
304
|
-
failures.push(`${mechanism} does not support ${behavior.activation} activation`);
|
|
305
|
-
}
|
|
306
|
-
if (!capabilities.presentations.has(behavior.presentation)) {
|
|
307
|
-
failures.push(`${mechanism} does not support ${behavior.presentation} presentation`);
|
|
308
|
-
}
|
|
309
|
-
if (!capabilities.layoutEffects.has(behavior.layoutEffect)) {
|
|
310
|
-
failures.push(`${mechanism} does not support ${behavior.layoutEffect} layout behavior`);
|
|
311
|
-
}
|
|
312
|
-
if (behavior.stateChange && !capabilities.stateChange) {
|
|
313
|
-
failures.push(`${mechanism} cannot provide state changes`);
|
|
314
|
-
}
|
|
315
|
-
if (behavior.reversible && !capabilities.reversible) {
|
|
316
|
-
failures.push(`${mechanism} cannot provide reversible behavior`);
|
|
317
|
-
}
|
|
318
|
-
if (behavior.repeatable && !capabilities.repeatable) {
|
|
319
|
-
failures.push(`${mechanism} cannot be repeated across targets`);
|
|
320
|
-
}
|
|
321
|
-
return {
|
|
322
|
-
compatible: failures.length === 0,
|
|
323
|
-
failures,
|
|
45
|
+
targetNodeIds: uniqueStrings(rawBehavior.targetNodeIds, 50),
|
|
324
46
|
};
|
|
325
47
|
}
|
|
326
48
|
|
|
327
|
-
export function normalizeBehaviorPlan(rawArguments
|
|
328
|
-
const behaviors = (Array.isArray(rawArguments
|
|
49
|
+
export function normalizeBehaviorPlan(rawArguments) {
|
|
50
|
+
const behaviors = (Array.isArray(rawArguments?.behaviors)
|
|
329
51
|
? rawArguments.behaviors
|
|
330
52
|
: []).slice(0, 20).map(normalizeBehavior);
|
|
331
|
-
const
|
|
332
|
-
? rawArguments.mechanisms
|
|
333
|
-
: []).slice(0, 20).map(normalizeMechanism);
|
|
334
|
-
const rejectedMechanisms = [
|
|
335
|
-
...normalizeRejectedMechanisms(inheritedRejected),
|
|
336
|
-
...normalizeRejectedMechanisms(rawArguments.rejectedMechanisms),
|
|
337
|
-
];
|
|
338
|
-
const behaviorIds = new Set();
|
|
53
|
+
const ids = new Set();
|
|
339
54
|
for (const behavior of behaviors) {
|
|
340
|
-
if (
|
|
55
|
+
if (ids.has(behavior.id)) {
|
|
341
56
|
throw contractError(
|
|
342
57
|
'FRAMER_BEHAVIOR_CONTRACT_INVALID',
|
|
343
58
|
`Behavior IDs must be unique; "${behavior.id}" is duplicated.`,
|
|
344
59
|
);
|
|
345
60
|
}
|
|
346
|
-
|
|
61
|
+
ids.add(behavior.id);
|
|
347
62
|
}
|
|
348
|
-
|
|
349
|
-
for (const decision of mechanisms) {
|
|
350
|
-
if (!behaviorIds.has(decision.behaviorId)) {
|
|
351
|
-
throw contractError(
|
|
352
|
-
'FRAMER_BEHAVIOR_MECHANISM_INVALID',
|
|
353
|
-
`Mechanism references unknown behavior "${decision.behaviorId}".`,
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
if (mechanismByBehavior.has(decision.behaviorId)) {
|
|
357
|
-
throw contractError(
|
|
358
|
-
'FRAMER_BEHAVIOR_MECHANISM_INVALID',
|
|
359
|
-
`Behavior "${decision.behaviorId}" has more than one selected mechanism.`,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
const behavior = behaviors.find((item) => item.id === decision.behaviorId);
|
|
363
|
-
if (
|
|
364
|
-
behavior?.expectedTargets > 1
|
|
365
|
-
&& decision.representativeFirst
|
|
366
|
-
&& (
|
|
367
|
-
decision.targetNodeIds.length !== behavior.expectedTargets
|
|
368
|
-
|| !decision.representativeTargetNodeId
|
|
369
|
-
|| !decision.targetNodeIds.includes(decision.representativeTargetNodeId)
|
|
370
|
-
)
|
|
371
|
-
) {
|
|
372
|
-
throw contractError(
|
|
373
|
-
'FRAMER_BEHAVIOR_REPRESENTATIVE_TARGETS_REQUIRED',
|
|
374
|
-
`Repeated behavior "${decision.behaviorId}" must declare all targetNodeIds and one representativeTargetNodeId before mutation.`,
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
mechanismByBehavior.set(decision.behaviorId, decision);
|
|
378
|
-
}
|
|
379
|
-
const compatibility = behaviors.map((behavior) => {
|
|
380
|
-
const decision = mechanismByBehavior.get(behavior.id);
|
|
381
|
-
if (!decision) {
|
|
382
|
-
throw contractError(
|
|
383
|
-
'FRAMER_BEHAVIOR_MECHANISM_REQUIRED',
|
|
384
|
-
`Behavior "${behavior.id}" requires one explicit implementation mechanism.`,
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
const rejected = rejectedMechanisms.find((item) =>
|
|
388
|
-
item.mechanism === decision.mechanism
|
|
389
|
-
&& (
|
|
390
|
-
item.behaviorId === behavior.id
|
|
391
|
-
|| item.behaviorSignature === behavior.signature
|
|
392
|
-
));
|
|
393
|
-
if (rejected) {
|
|
394
|
-
throw contractError(
|
|
395
|
-
'FRAMER_BEHAVIOR_MECHANISM_REJECTED',
|
|
396
|
-
`${decision.mechanism} was already rejected for behavior "${behavior.id}": ${rejected.reason}`,
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
const result = mechanismCompatibility(behavior, decision.mechanism);
|
|
400
|
-
if (!result.compatible) {
|
|
401
|
-
throw contractError(
|
|
402
|
-
'FRAMER_BEHAVIOR_MECHANISM_INCOMPATIBLE',
|
|
403
|
-
`The selected mechanism for behavior "${behavior.id}" is incompatible: ${result.failures.join('; ')}.`,
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
return {
|
|
407
|
-
behaviorId: behavior.id,
|
|
408
|
-
behaviorSignature: behavior.signature,
|
|
409
|
-
mechanism: decision.mechanism,
|
|
410
|
-
compatible: true,
|
|
411
|
-
requiredCapabilities: behaviorCapabilities(behavior),
|
|
412
|
-
};
|
|
413
|
-
});
|
|
414
|
-
return {
|
|
415
|
-
behaviors,
|
|
416
|
-
mechanisms,
|
|
417
|
-
rejectedMechanisms,
|
|
418
|
-
compatibility,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
export function behaviorCapabilities(behavior) {
|
|
423
|
-
return [
|
|
424
|
-
`activation:${behavior.activation}`,
|
|
425
|
-
`presentation:${behavior.presentation}`,
|
|
426
|
-
`layout:${behavior.layoutEffect}`,
|
|
427
|
-
...(behavior.stateChange ? ['state-change'] : []),
|
|
428
|
-
...(behavior.reversible ? ['reversible'] : []),
|
|
429
|
-
...(behavior.repeatable ? ['repeatable'] : []),
|
|
430
|
-
];
|
|
63
|
+
return { behaviors };
|
|
431
64
|
}
|
|
432
65
|
|
|
433
66
|
export function mergeBehaviorPlans(current, next) {
|
|
434
67
|
if (!current) return next;
|
|
435
68
|
const byId = new Map(current.behaviors.map((item) => [item.id, item]));
|
|
436
69
|
next.behaviors.forEach((item) => byId.set(item.id, item));
|
|
437
|
-
|
|
438
|
-
current.mechanisms.map((item) => [item.behaviorId, item]),
|
|
439
|
-
);
|
|
440
|
-
next.mechanisms.forEach((item) => mechanisms.set(item.behaviorId, item));
|
|
441
|
-
const merged = {
|
|
442
|
-
behaviors: [...byId.values()],
|
|
443
|
-
mechanisms: [...mechanisms.values()],
|
|
444
|
-
rejectedMechanisms: [
|
|
445
|
-
...current.rejectedMechanisms,
|
|
446
|
-
...next.rejectedMechanisms,
|
|
447
|
-
],
|
|
448
|
-
};
|
|
449
|
-
return normalizeBehaviorPlan(merged);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function normalizedEvidenceText(value) {
|
|
453
|
-
try {
|
|
454
|
-
return JSON.stringify(value).toLowerCase();
|
|
455
|
-
} catch {
|
|
456
|
-
return '';
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function eventEvidenceValues(value, event) {
|
|
461
|
-
const matches = [];
|
|
462
|
-
const expected = event.toLowerCase();
|
|
463
|
-
const visit = (current) => {
|
|
464
|
-
if (Array.isArray(current)) {
|
|
465
|
-
current.forEach(visit);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (!isRecord(current)) return;
|
|
469
|
-
for (const [key, nested] of Object.entries(current)) {
|
|
470
|
-
const normalizedKey = key.toLowerCase();
|
|
471
|
-
if (
|
|
472
|
-
normalizedKey === expected
|
|
473
|
-
|| normalizedKey.startsWith(`${expected}.`)
|
|
474
|
-
) {
|
|
475
|
-
matches.push(nested);
|
|
476
|
-
}
|
|
477
|
-
visit(nested);
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
|
-
visit(value);
|
|
481
|
-
return matches;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function snapshotFor(snapshots, id) {
|
|
485
|
-
if (!id) return null;
|
|
486
|
-
if (snapshots instanceof Map) return snapshots.get(id) || null;
|
|
487
|
-
return snapshots?.[id] || null;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function nodeFromSnapshot(snapshot) {
|
|
491
|
-
return isRecord(snapshot?.node) ? snapshot.node : snapshot;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function nodeType(snapshot) {
|
|
495
|
-
const node = nodeFromSnapshot(snapshot);
|
|
496
|
-
return text(node?.type || node?.nodeType, 80);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function parentId(snapshot) {
|
|
500
|
-
const node = nodeFromSnapshot(snapshot);
|
|
501
|
-
return text(
|
|
502
|
-
snapshot?.parentId
|
|
503
|
-
|| node?.parentId
|
|
504
|
-
|| node?.parent?.id,
|
|
505
|
-
200,
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function ancestorIds(snapshot) {
|
|
510
|
-
const node = nodeFromSnapshot(snapshot);
|
|
511
|
-
return uniqueStrings([
|
|
512
|
-
...(Array.isArray(snapshot?.ancestorIds) ? snapshot.ancestorIds : []),
|
|
513
|
-
...(Array.isArray(node?.ancestorIds) ? node.ancestorIds : []),
|
|
514
|
-
...(Array.isArray(node?.ancestors)
|
|
515
|
-
? node.ancestors.map((ancestor) =>
|
|
516
|
-
typeof ancestor === 'string' ? ancestor : ancestor?.id)
|
|
517
|
-
: []),
|
|
518
|
-
], 100);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function belongsTo(snapshot, expectedAncestorId) {
|
|
522
|
-
if (!snapshot || !expectedAncestorId) return false;
|
|
523
|
-
return (
|
|
524
|
-
parentId(snapshot) === expectedAncestorId
|
|
525
|
-
|| ancestorIds(snapshot).includes(expectedAncestorId)
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function referencesComponent(snapshot, componentNodeId) {
|
|
530
|
-
const node = nodeFromSnapshot(snapshot);
|
|
531
|
-
if (!node || !componentNodeId) return false;
|
|
532
|
-
const component = isRecord(node.component) ? node.component : {};
|
|
533
|
-
const references = [
|
|
534
|
-
typeof node.component === 'string' ? node.component : '',
|
|
535
|
-
node.componentId,
|
|
536
|
-
node.componentIdentifier,
|
|
537
|
-
component.id,
|
|
538
|
-
component.nodeId,
|
|
539
|
-
component.componentId,
|
|
540
|
-
component.componentIdentifier,
|
|
541
|
-
];
|
|
542
|
-
const normalizedReferences = new Set(
|
|
543
|
-
references.flatMap((value) => {
|
|
544
|
-
const reference = text(value, 500);
|
|
545
|
-
if (!reference) return [];
|
|
546
|
-
const withoutCodePrefix = reference
|
|
547
|
-
.replace(/^codefile\//i, '')
|
|
548
|
-
.replace(/^code:/i, '');
|
|
549
|
-
const withoutExport = withoutCodePrefix.split(':')[0];
|
|
550
|
-
return [
|
|
551
|
-
reference,
|
|
552
|
-
withoutCodePrefix,
|
|
553
|
-
withoutExport,
|
|
554
|
-
withoutExport.split('/').pop(),
|
|
555
|
-
].filter(Boolean);
|
|
556
|
-
}),
|
|
557
|
-
);
|
|
558
|
-
return normalizedReferences.has(componentNodeId);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function nodeVisible(snapshot) {
|
|
562
|
-
const node = nodeFromSnapshot(snapshot);
|
|
563
|
-
if (!node) return null;
|
|
564
|
-
const visible = Object.prototype.hasOwnProperty.call(node, 'visible')
|
|
565
|
-
? node.visible
|
|
566
|
-
: isRecord(node.attributes)
|
|
567
|
-
? node.attributes.visible
|
|
568
|
-
: undefined;
|
|
569
|
-
const hidden = Object.prototype.hasOwnProperty.call(node, 'hidden')
|
|
570
|
-
? node.hidden
|
|
571
|
-
: isRecord(node.attributes)
|
|
572
|
-
? node.attributes.hidden
|
|
573
|
-
: undefined;
|
|
574
|
-
if (visible === false || hidden === true) return false;
|
|
575
|
-
if (visible === true || hidden === false) return true;
|
|
576
|
-
return null;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function eventText(snapshot, event) {
|
|
580
|
-
const node = nodeFromSnapshot(snapshot);
|
|
581
|
-
if (!node) return '';
|
|
582
|
-
const values = eventEvidenceValues(node, event);
|
|
583
|
-
return values.length > 0 ? normalizedEvidenceText(values) : '';
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function directNodeProperty(snapshot, property) {
|
|
587
|
-
const node = nodeFromSnapshot(snapshot);
|
|
588
|
-
if (!node) return undefined;
|
|
589
|
-
if (Object.prototype.hasOwnProperty.call(node, property)) {
|
|
590
|
-
return node[property];
|
|
591
|
-
}
|
|
592
|
-
if (
|
|
593
|
-
isRecord(node.attributes)
|
|
594
|
-
&& Object.prototype.hasOwnProperty.call(node.attributes, property)
|
|
595
|
-
) {
|
|
596
|
-
return node.attributes[property];
|
|
597
|
-
}
|
|
598
|
-
return undefined;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function expectedEvents(activation) {
|
|
602
|
-
if (activation === 'tap') return ['onTap', 'onTapStart'];
|
|
603
|
-
if (activation === 'hover') return ['onMouseEnter', 'onMouseLeave'];
|
|
604
|
-
if (activation === 'appear') return ['onAppear'];
|
|
605
|
-
if (activation === 'key') return ['onKeyDown'];
|
|
606
|
-
return [];
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function normalizeEvidence(raw, behaviorById) {
|
|
610
|
-
if (!isRecord(raw)) {
|
|
611
|
-
throw contractError(
|
|
612
|
-
'FRAMER_BEHAVIOR_EVIDENCE_INVALID',
|
|
613
|
-
'Interaction evidence must be an object.',
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
const behaviorId = text(raw.behaviorId, 120);
|
|
617
|
-
const behavior = behaviorById.get(behaviorId);
|
|
618
|
-
if (!behavior) {
|
|
619
|
-
throw contractError(
|
|
620
|
-
'FRAMER_BEHAVIOR_EVIDENCE_INVALID',
|
|
621
|
-
`Interaction evidence references unknown behavior "${behaviorId}".`,
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
const expectedDestination = text(raw.expectedDestination, 500);
|
|
625
|
-
if (
|
|
626
|
-
expectedDestination
|
|
627
|
-
&& !behavior.expectedDestinations.some((planned) =>
|
|
628
|
-
destinationCandidates(planned).some((candidate) =>
|
|
629
|
-
destinationCandidates(expectedDestination).includes(candidate)))
|
|
630
|
-
) {
|
|
631
|
-
throw contractError(
|
|
632
|
-
'FRAMER_BEHAVIOR_EVIDENCE_INVALID',
|
|
633
|
-
`Interaction evidence for "${behaviorId}" must use a destination declared in the behavior plan.`,
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
return {
|
|
637
|
-
behaviorId,
|
|
638
|
-
description: text(raw.description, 300),
|
|
639
|
-
triggerNodeId: text(raw.triggerNodeId, 200),
|
|
640
|
-
event: text(raw.event, 80),
|
|
641
|
-
expectedDestination,
|
|
642
|
-
targetNodeId: text(raw.targetNodeId, 200),
|
|
643
|
-
componentNodeId: text(raw.componentNodeId, 200),
|
|
644
|
-
instanceNodeIds: uniqueStrings(raw.instanceNodeIds, 50),
|
|
645
|
-
openVariantNodeId: text(raw.openVariantNodeId, 200),
|
|
646
|
-
closedVariantNodeId: text(raw.closedVariantNodeId, 200),
|
|
647
|
-
openAnswerNodeId: text(raw.openAnswerNodeId, 200),
|
|
648
|
-
closedAnswerNodeId: text(raw.closedAnswerNodeId, 200),
|
|
649
|
-
semanticAssessment: isRecord(raw.semanticAssessment)
|
|
650
|
-
&& ['verified', 'contradicted', 'unknown']
|
|
651
|
-
.includes(raw.semanticAssessment.verdict)
|
|
652
|
-
&& text(raw.semanticAssessment.rationale, 1200).length >= 10
|
|
653
|
-
? {
|
|
654
|
-
verdict: raw.semanticAssessment.verdict,
|
|
655
|
-
rationale: text(raw.semanticAssessment.rationale, 1200),
|
|
656
|
-
evidenceNodeIds: uniqueStrings(
|
|
657
|
-
raw.semanticAssessment.evidenceNodeIds,
|
|
658
|
-
50,
|
|
659
|
-
),
|
|
660
|
-
}
|
|
661
|
-
: null,
|
|
662
|
-
};
|
|
70
|
+
return { behaviors: [...byId.values()] };
|
|
663
71
|
}
|
|
664
72
|
|
|
665
73
|
export function normalizeBehaviorEvidence(rawChecks, plan) {
|
|
666
|
-
const
|
|
667
|
-
(plan?.behaviors || []).map((behavior) => [behavior.id, behavior]),
|
|
668
|
-
);
|
|
74
|
+
const behaviorIds = new Set((plan?.behaviors || []).map((item) => item.id));
|
|
669
75
|
const checks = (Array.isArray(rawChecks) ? rawChecks : []).slice(0, 50);
|
|
670
76
|
if (!checks.length) {
|
|
671
77
|
throw contractError(
|
|
672
78
|
'FRAMER_BEHAVIOR_EVIDENCE_REQUIRED',
|
|
673
|
-
'
|
|
79
|
+
'Name at least one node and the Framer property that carries a planned behavior.',
|
|
674
80
|
);
|
|
675
81
|
}
|
|
676
|
-
return checks.map((raw) =>
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
check.openVariantNodeId,
|
|
685
|
-
check.closedVariantNodeId,
|
|
686
|
-
check.openAnswerNodeId,
|
|
687
|
-
check.closedAnswerNodeId,
|
|
688
|
-
...check.instanceNodeIds,
|
|
689
|
-
]), 200);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function evidenceState() {
|
|
693
|
-
return {
|
|
694
|
-
contradictions: [],
|
|
695
|
-
unknowns: [],
|
|
696
|
-
observations: [],
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function contradict(state, message) {
|
|
701
|
-
state.contradictions.push(message);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function unknown(state, message) {
|
|
705
|
-
state.unknowns.push(message);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function observe(state, kind, detail = {}) {
|
|
709
|
-
state.observations.push({ kind, ...detail });
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function verifyTrigger(check, behavior, trigger, state) {
|
|
713
|
-
if (!check.triggerNodeId || !trigger) {
|
|
714
|
-
unknown(state, 'trigger node was not found');
|
|
715
|
-
return '';
|
|
716
|
-
}
|
|
717
|
-
observe(state, 'node_found', {
|
|
718
|
-
nodeId: check.triggerNodeId,
|
|
719
|
-
nodeType: nodeType(trigger),
|
|
720
|
-
});
|
|
721
|
-
const allowedEvents = expectedEvents(behavior.activation);
|
|
722
|
-
if (
|
|
723
|
-
allowedEvents.length > 0
|
|
724
|
-
&& !allowedEvents.includes(check.event)
|
|
725
|
-
) {
|
|
726
|
-
contradict(
|
|
727
|
-
state,
|
|
728
|
-
`${check.event || 'the supplied event'} does not match ${behavior.activation} activation`,
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
if (!check.event) {
|
|
732
|
-
unknown(state, 'the trigger event was not supplied');
|
|
733
|
-
return '';
|
|
734
|
-
}
|
|
735
|
-
if (!FRAME_EVENT_NODE_TYPES.has(nodeType(trigger))) {
|
|
736
|
-
contradict(
|
|
737
|
-
state,
|
|
738
|
-
`${nodeType(trigger) || 'the trigger node'} does not support verified frame events`,
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
const evidence = eventText(trigger, check.event);
|
|
742
|
-
if (!evidence) {
|
|
743
|
-
unknown(state, `${check.event} was not visible in the serialized trigger`);
|
|
744
|
-
} else {
|
|
745
|
-
observe(state, 'event_binding', {
|
|
746
|
-
nodeId: check.triggerNodeId,
|
|
747
|
-
event: check.event,
|
|
748
|
-
value: evidence.slice(0, 1000),
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
return evidence;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function verifyExpectedContent(behavior, snapshots, nodeIds, state) {
|
|
755
|
-
const combined = normalizedEvidenceText(
|
|
756
|
-
nodeIds.map((id) => nodeFromSnapshot(snapshotFor(snapshots, id))),
|
|
757
|
-
);
|
|
758
|
-
for (const expected of behavior.expectedContent) {
|
|
759
|
-
if (!combined.includes(expected.toLowerCase())) {
|
|
760
|
-
unknown(state, `expected content was not visible in the evidence: ${expected}`);
|
|
761
|
-
} else {
|
|
762
|
-
observe(state, 'expected_content', { value: expected });
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function verifyOverlay(check, behavior, snapshots, state) {
|
|
768
|
-
const trigger = snapshotFor(snapshots, check.triggerNodeId);
|
|
769
|
-
const target = snapshotFor(snapshots, check.targetNodeId);
|
|
770
|
-
const triggerEvent = verifyTrigger(check, behavior, trigger, state);
|
|
771
|
-
if (!check.targetNodeId || !target) {
|
|
772
|
-
unknown(state, 'overlay target node was not found');
|
|
773
|
-
return;
|
|
774
|
-
}
|
|
775
|
-
observe(state, 'node_found', {
|
|
776
|
-
nodeId: check.targetNodeId,
|
|
777
|
-
nodeType: nodeType(target),
|
|
778
|
-
});
|
|
779
|
-
if (triggerEvent && !triggerEvent.includes('show_overlay')) {
|
|
780
|
-
contradict(state, `${check.event} does not contain SHOW_OVERLAY`);
|
|
781
|
-
}
|
|
782
|
-
if (
|
|
783
|
-
triggerEvent
|
|
784
|
-
&& !triggerEvent.includes(check.targetNodeId.toLowerCase())
|
|
785
|
-
) {
|
|
786
|
-
contradict(state, `${check.event} does not reference target ${check.targetNodeId}`);
|
|
787
|
-
}
|
|
788
|
-
const expectedType =
|
|
789
|
-
behavior.presentation === 'modal'
|
|
790
|
-
? 'FixedOverlayNode'
|
|
791
|
-
: 'RelativeOverlayNode';
|
|
792
|
-
if (nodeType(target) !== expectedType) {
|
|
793
|
-
contradict(state, `the behavior requires a ${expectedType}`);
|
|
794
|
-
}
|
|
795
|
-
const actualParentId = parentId(target);
|
|
796
|
-
if (!actualParentId) {
|
|
797
|
-
unknown(state, 'the overlay parent was not visible in the evidence');
|
|
798
|
-
} else if (actualParentId !== check.triggerNodeId) {
|
|
799
|
-
contradict(state, 'the overlay is not parented to its actual trigger');
|
|
800
|
-
}
|
|
801
|
-
verifyExpectedContent(behavior, snapshots, [check.targetNodeId], state);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function verifyComponentInstances(
|
|
805
|
-
check,
|
|
806
|
-
behavior,
|
|
807
|
-
snapshots,
|
|
808
|
-
state,
|
|
809
|
-
context,
|
|
810
|
-
{ requireDefinition = true } = {},
|
|
811
|
-
) {
|
|
812
|
-
const component = snapshotFor(snapshots, check.componentNodeId);
|
|
813
|
-
if (requireDefinition && !component) {
|
|
814
|
-
unknown(state, 'component definition was not found');
|
|
815
|
-
} else if (
|
|
816
|
-
requireDefinition
|
|
817
|
-
&& nodeType(component) !== 'ComponentNode'
|
|
818
|
-
) {
|
|
819
|
-
contradict(state, 'the declared component definition is not a ComponentNode');
|
|
820
|
-
}
|
|
821
|
-
const instances = check.instanceNodeIds.map((id) => ({
|
|
822
|
-
id,
|
|
823
|
-
snapshot: snapshotFor(snapshots, id),
|
|
824
|
-
}));
|
|
825
|
-
for (const instance of instances) {
|
|
826
|
-
if (!instance.snapshot) {
|
|
827
|
-
unknown(state, `component instance ${instance.id} was not found`);
|
|
828
|
-
} else if (nodeType(instance.snapshot) !== 'ComponentInstanceNode') {
|
|
829
|
-
contradict(state, `${instance.id} is not a ComponentInstanceNode`);
|
|
830
|
-
} else if (!referencesComponent(instance.snapshot, check.componentNodeId)) {
|
|
831
|
-
contradict(
|
|
832
|
-
state,
|
|
833
|
-
`component instance ${instance.id} does not reference ${check.componentNodeId}`,
|
|
82
|
+
return checks.map((raw, index) => {
|
|
83
|
+
const behaviorId = text(raw?.behaviorId, 120);
|
|
84
|
+
const nodeId = text(raw?.nodeId, 200);
|
|
85
|
+
const property = text(raw?.property, 120);
|
|
86
|
+
if (!isRecord(raw) || !behaviorIds.has(behaviorId) || !nodeId || !property) {
|
|
87
|
+
throw contractError(
|
|
88
|
+
'FRAMER_BEHAVIOR_EVIDENCE_INVALID',
|
|
89
|
+
`Check ${index + 1} must reference a planned behavior (${[...behaviorIds].join(', ') || 'none planned'}), a node ID, and the Framer property to inspect.`,
|
|
834
90
|
);
|
|
835
|
-
} else {
|
|
836
|
-
observe(state, 'component_instance', {
|
|
837
|
-
nodeId: instance.id,
|
|
838
|
-
componentNodeId: check.componentNodeId,
|
|
839
|
-
});
|
|
840
91
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
parentId(instance.snapshot),
|
|
849
|
-
...(Array.isArray(instance.snapshot.ancestorIds)
|
|
850
|
-
? instance.snapshot.ancestorIds
|
|
851
|
-
: []),
|
|
852
|
-
].filter(Boolean));
|
|
853
|
-
if (!authorizedContainerIds.some((id) => ancestry.has(id))) {
|
|
854
|
-
contradict(
|
|
855
|
-
state,
|
|
856
|
-
`component instance ${instance.id} is outside the authorized write scope`,
|
|
857
|
-
);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
92
|
+
return {
|
|
93
|
+
behaviorId,
|
|
94
|
+
nodeId,
|
|
95
|
+
property,
|
|
96
|
+
contains: text(raw.contains, 500),
|
|
97
|
+
};
|
|
98
|
+
});
|
|
861
99
|
}
|
|
862
100
|
|
|
863
|
-
function
|
|
864
|
-
check,
|
|
865
|
-
behavior,
|
|
866
|
-
snapshots,
|
|
867
|
-
state,
|
|
868
|
-
context,
|
|
869
|
-
) {
|
|
870
|
-
const trigger = snapshotFor(snapshots, check.triggerNodeId);
|
|
871
|
-
const component = snapshotFor(snapshots, check.componentNodeId);
|
|
872
|
-
const openVariant = snapshotFor(snapshots, check.openVariantNodeId);
|
|
873
|
-
const closedVariant = snapshotFor(snapshots, check.closedVariantNodeId);
|
|
874
|
-
const openAnswer = snapshotFor(snapshots, check.openAnswerNodeId);
|
|
875
|
-
const closedAnswer = snapshotFor(snapshots, check.closedAnswerNodeId);
|
|
876
|
-
const triggerEvent = verifyTrigger(check, behavior, trigger, state);
|
|
877
|
-
verifyComponentInstances(check, behavior, snapshots, state, context);
|
|
878
|
-
if (component && trigger && !belongsTo(trigger, check.componentNodeId)) {
|
|
879
|
-
contradict(state, 'the trigger is not inside the component definition');
|
|
880
|
-
}
|
|
881
|
-
if (
|
|
882
|
-
!openVariant
|
|
883
|
-
|| !closedVariant
|
|
884
|
-
) {
|
|
885
|
-
unknown(state, 'open and closed variants were not both found');
|
|
886
|
-
} else if (check.openVariantNodeId === check.closedVariantNodeId) {
|
|
887
|
-
contradict(state, 'open and closed variants must be distinct');
|
|
888
|
-
} else {
|
|
889
|
-
if (!belongsTo(openVariant, check.componentNodeId)) {
|
|
890
|
-
contradict(state, 'the open variant does not belong to the component');
|
|
891
|
-
}
|
|
892
|
-
if (!belongsTo(closedVariant, check.componentNodeId)) {
|
|
893
|
-
contradict(state, 'the closed variant does not belong to the component');
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
if (triggerEvent && !triggerEvent.includes('set_variant')) {
|
|
897
|
-
contradict(state, `${check.event} does not contain SET_VARIANT`);
|
|
898
|
-
}
|
|
899
|
-
if (
|
|
900
|
-
triggerEvent
|
|
901
|
-
&& !triggerEvent.includes('cycle')
|
|
902
|
-
&& !triggerEvent.includes(check.openVariantNodeId.toLowerCase())
|
|
903
|
-
&& !triggerEvent.includes(check.closedVariantNodeId.toLowerCase())
|
|
904
|
-
) {
|
|
905
|
-
contradict(state, 'SET_VARIANT does not reference cycle or a declared variant');
|
|
906
|
-
}
|
|
907
|
-
if (!openAnswer || !closedAnswer) {
|
|
908
|
-
unknown(state, 'open and closed answer-state nodes were not found');
|
|
909
|
-
} else {
|
|
910
|
-
if (!belongsTo(openAnswer, check.openVariantNodeId)) {
|
|
911
|
-
contradict(state, 'the visible answer does not belong to the open variant');
|
|
912
|
-
}
|
|
913
|
-
if (!belongsTo(closedAnswer, check.closedVariantNodeId)) {
|
|
914
|
-
contradict(state, 'the hidden answer does not belong to the closed variant');
|
|
915
|
-
}
|
|
916
|
-
const openVisibility = nodeVisible(openAnswer);
|
|
917
|
-
const closedVisibility = nodeVisible(closedAnswer);
|
|
918
|
-
observe(state, 'visibility', {
|
|
919
|
-
nodeId: check.openAnswerNodeId,
|
|
920
|
-
value:
|
|
921
|
-
openVisibility === null
|
|
922
|
-
? 'unknown'
|
|
923
|
-
: openVisibility
|
|
924
|
-
? 'visible'
|
|
925
|
-
: 'hidden',
|
|
926
|
-
});
|
|
927
|
-
observe(state, 'visibility', {
|
|
928
|
-
nodeId: check.closedAnswerNodeId,
|
|
929
|
-
value:
|
|
930
|
-
closedVisibility === null
|
|
931
|
-
? 'unknown'
|
|
932
|
-
: closedVisibility
|
|
933
|
-
? 'visible'
|
|
934
|
-
: 'hidden',
|
|
935
|
-
});
|
|
936
|
-
if (openVisibility === false) {
|
|
937
|
-
contradict(state, 'the answer is hidden in the open variant');
|
|
938
|
-
} else if (openVisibility === null) {
|
|
939
|
-
unknown(state, 'the open answer visibility was not exposed');
|
|
940
|
-
}
|
|
941
|
-
if (closedVisibility === true) {
|
|
942
|
-
contradict(state, 'the answer is not hidden in the closed variant');
|
|
943
|
-
} else if (closedVisibility === null) {
|
|
944
|
-
unknown(state, 'the closed answer visibility was not exposed');
|
|
945
|
-
}
|
|
946
|
-
}
|
|
101
|
+
export function evidenceNodeIds(checks) {
|
|
102
|
+
return uniqueStrings(checks.map((check) => check.nodeId), 200);
|
|
947
103
|
}
|
|
948
104
|
|
|
949
|
-
function
|
|
950
|
-
|
|
951
|
-
if (
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
try {
|
|
955
|
-
const parsed = new URL(candidate, 'https://dexter.local');
|
|
956
|
-
candidates.add(
|
|
957
|
-
`${parsed.pathname}${parsed.search}${parsed.hash}`
|
|
958
|
-
.toLowerCase()
|
|
959
|
-
.replace(/\/+$/, '') || '/',
|
|
960
|
-
);
|
|
961
|
-
} catch {
|
|
962
|
-
// Non-URL control values are ignored unless the raw value matches.
|
|
963
|
-
}
|
|
964
|
-
return [...candidates];
|
|
105
|
+
function hasContent(value) {
|
|
106
|
+
if (value === undefined || value === null || value === '') return false;
|
|
107
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
108
|
+
if (isRecord(value)) return Object.keys(value).length > 0;
|
|
109
|
+
return true;
|
|
965
110
|
}
|
|
966
111
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
112
|
+
// Finds the property on the node itself. Child nodes are skipped so a
|
|
113
|
+
// descendant cannot satisfy a claim about its parent.
|
|
114
|
+
function propertyValues(node, property) {
|
|
115
|
+
const values = [];
|
|
116
|
+
const visit = (current, depth) => {
|
|
117
|
+
if (depth > MAX_SEARCH_DEPTH || values.length >= 10) return;
|
|
970
118
|
if (Array.isArray(current)) {
|
|
971
|
-
current.forEach((
|
|
119
|
+
current.forEach((item) => visit(item, depth + 1));
|
|
972
120
|
return;
|
|
973
121
|
}
|
|
974
122
|
if (!isRecord(current)) return;
|
|
975
123
|
for (const [key, nested] of Object.entries(current)) {
|
|
976
|
-
|
|
977
|
-
if (
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
facts.push({
|
|
982
|
-
path: nextPath.join('.'),
|
|
983
|
-
value: nested,
|
|
984
|
-
candidates: destinationCandidates(nested),
|
|
985
|
-
});
|
|
124
|
+
if (key === 'children') continue;
|
|
125
|
+
if (key === property) {
|
|
126
|
+
if (hasContent(nested)) values.push(nested);
|
|
127
|
+
} else {
|
|
128
|
+
visit(nested, depth + 1);
|
|
986
129
|
}
|
|
987
|
-
visit(nested, nextPath);
|
|
988
130
|
}
|
|
989
131
|
};
|
|
990
|
-
visit(
|
|
991
|
-
return
|
|
132
|
+
visit(node, 0);
|
|
133
|
+
return values;
|
|
992
134
|
}
|
|
993
135
|
|
|
994
|
-
function
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
? [check.expectedDestination]
|
|
1000
|
-
: behavior.expectedDestinations;
|
|
1001
|
-
const expected = new Set(expectedValues.flatMap(destinationCandidates));
|
|
1002
|
-
if (targetIds.length === 0) {
|
|
1003
|
-
unknown(state, 'no navigation target node was supplied');
|
|
1004
|
-
return;
|
|
136
|
+
function serialized(value) {
|
|
137
|
+
try {
|
|
138
|
+
return JSON.stringify(value);
|
|
139
|
+
} catch {
|
|
140
|
+
return '';
|
|
1005
141
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function verifyBehaviorChecks({ checks, nodes }) {
|
|
145
|
+
return checks.map((check) => {
|
|
146
|
+
const node = nodes instanceof Map ? nodes.get(check.nodeId) : nodes?.[check.nodeId];
|
|
147
|
+
if (!node) {
|
|
148
|
+
return { ...check, verdict: 'unknown', reason: `node ${check.nodeId} was not found` };
|
|
1011
149
|
}
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
observe(state, 'component_link_control_available', {
|
|
1020
|
-
nodeId,
|
|
1021
|
-
sources: controlFacts.map((fact) => fact.path).slice(0, 20),
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
unknown(
|
|
1025
|
-
state,
|
|
1026
|
-
`no native link or component-control destination was visible for ${nodeId}`,
|
|
1027
|
-
);
|
|
1028
|
-
continue;
|
|
150
|
+
const values = propertyValues(node, check.property);
|
|
151
|
+
if (values.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
...check,
|
|
154
|
+
verdict: 'unknown',
|
|
155
|
+
reason: `${check.property} is not set on ${check.nodeId}`,
|
|
156
|
+
};
|
|
1029
157
|
}
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1038
|
-
continue;
|
|
158
|
+
const observed = serialized(values.length === 1 ? values[0] : values);
|
|
159
|
+
if (check.contains && !observed.toLowerCase().includes(check.contains.toLowerCase())) {
|
|
160
|
+
return {
|
|
161
|
+
...check,
|
|
162
|
+
verdict: 'contradicted',
|
|
163
|
+
reason: `${check.property} on ${check.nodeId} does not contain ${check.contains}`,
|
|
164
|
+
observedValue: observed.slice(0, MAX_OBSERVED_CHARS),
|
|
165
|
+
};
|
|
1039
166
|
}
|
|
1040
|
-
contradict(
|
|
1041
|
-
state,
|
|
1042
|
-
`${nodeId} points to ${facts.map((fact) => fact.value).join(', ')} instead of ${expectedValues.join(' or ')}`,
|
|
1043
|
-
);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
function verifyExistingComponent(
|
|
1048
|
-
check,
|
|
1049
|
-
behavior,
|
|
1050
|
-
snapshots,
|
|
1051
|
-
state,
|
|
1052
|
-
context,
|
|
1053
|
-
) {
|
|
1054
|
-
const trigger = snapshotFor(snapshots, check.triggerNodeId);
|
|
1055
|
-
verifyComponentInstances(check, behavior, snapshots, state, context);
|
|
1056
|
-
if (
|
|
1057
|
-
behavior.presentation === 'navigation'
|
|
1058
|
-
|| behavior.layoutEffect === 'navigation'
|
|
1059
|
-
) {
|
|
1060
|
-
verifyNativeLink(check, behavior, snapshots, state, context);
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
const triggerEvent = verifyTrigger(check, behavior, trigger, state);
|
|
1064
|
-
if (trigger && !belongsTo(trigger, check.componentNodeId)) {
|
|
1065
|
-
contradict(state, 'the trigger is not inside the existing component definition');
|
|
1066
|
-
}
|
|
1067
|
-
const permittedActions =
|
|
1068
|
-
behavior.presentation === 'floating' || behavior.presentation === 'modal'
|
|
1069
|
-
? ['show_overlay', 'trigger_event']
|
|
1070
|
-
: behavior.stateChange
|
|
1071
|
-
? ['set_variant', 'trigger_event']
|
|
1072
|
-
: [];
|
|
1073
|
-
if (
|
|
1074
|
-
permittedActions.length > 0
|
|
1075
|
-
&& triggerEvent
|
|
1076
|
-
&& !permittedActions.some((action) => triggerEvent.includes(action))
|
|
1077
|
-
) {
|
|
1078
|
-
contradict(
|
|
1079
|
-
state,
|
|
1080
|
-
'the component trigger does not expose an action capable of the declared behavior',
|
|
1081
|
-
);
|
|
1082
|
-
}
|
|
1083
|
-
verifyExpectedContent(
|
|
1084
|
-
behavior,
|
|
1085
|
-
snapshots,
|
|
1086
|
-
[check.componentNodeId, ...check.instanceNodeIds],
|
|
1087
|
-
state,
|
|
1088
|
-
);
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
function hasCodeComponentEvidence(snapshot) {
|
|
1092
|
-
const node = nodeFromSnapshot(snapshot);
|
|
1093
|
-
if (!node) return false;
|
|
1094
|
-
const serialized = normalizedEvidenceText(node);
|
|
1095
|
-
return (
|
|
1096
|
-
serialized.includes('codecomponent')
|
|
1097
|
-
|| serialized.includes('code_file')
|
|
1098
|
-
|| serialized.includes('codefile')
|
|
1099
|
-
|| serialized.includes('componentidentifier')
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function verifyCodeComponent(check, behavior, snapshots, state, context) {
|
|
1104
|
-
// Code files are not native canvas ComponentNodes. Their durable structural
|
|
1105
|
-
// proof is an installed ComponentInstanceNode whose component reference
|
|
1106
|
-
// resolves to the authored code-file ID.
|
|
1107
|
-
verifyComponentInstances(
|
|
1108
|
-
check,
|
|
1109
|
-
behavior,
|
|
1110
|
-
snapshots,
|
|
1111
|
-
state,
|
|
1112
|
-
context,
|
|
1113
|
-
{ requireDefinition: false },
|
|
1114
|
-
);
|
|
1115
|
-
const nodes = check.instanceNodeIds.map((id) => snapshotFor(snapshots, id));
|
|
1116
|
-
if (
|
|
1117
|
-
!nodes.some(hasCodeComponentEvidence)
|
|
1118
|
-
&& Number(context?.lowLevelWriteCalls || 0) < 1
|
|
1119
|
-
) {
|
|
1120
|
-
unknown(
|
|
1121
|
-
state,
|
|
1122
|
-
'no code-component source or installed code-component evidence was found',
|
|
1123
|
-
);
|
|
1124
|
-
}
|
|
1125
|
-
// Content is covered by the mandatory structural and visual checks. Behavior
|
|
1126
|
-
// plans may describe content semantically rather than quote literal canvas
|
|
1127
|
-
// text, so treating that prose as an exact interaction assertion creates
|
|
1128
|
-
// false failures for otherwise valid code components.
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function verifyNativeEvent(check, behavior, snapshots, state) {
|
|
1132
|
-
verifyTrigger(
|
|
1133
|
-
check,
|
|
1134
|
-
behavior,
|
|
1135
|
-
snapshotFor(snapshots, check.triggerNodeId),
|
|
1136
|
-
state,
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
function verifyNativeEffect(check, behavior, snapshots, state) {
|
|
1141
|
-
const trigger = snapshotFor(snapshots, check.triggerNodeId);
|
|
1142
|
-
if (!check.triggerNodeId || !trigger) {
|
|
1143
|
-
unknown(state, 'effect target node was not found');
|
|
1144
|
-
return null;
|
|
1145
|
-
}
|
|
1146
|
-
observe(state, 'node_found', {
|
|
1147
|
-
nodeId: check.triggerNodeId,
|
|
1148
|
-
nodeType: nodeType(trigger),
|
|
1149
|
-
});
|
|
1150
|
-
if (!FRAME_EFFECT_NODE_TYPES.has(nodeType(trigger))) {
|
|
1151
|
-
contradict(
|
|
1152
|
-
state,
|
|
1153
|
-
`${nodeType(trigger) || 'the target node'} does not support verified Framer effects`,
|
|
1154
|
-
);
|
|
1155
|
-
}
|
|
1156
|
-
const effectName = NATIVE_EFFECT_BY_ACTIVATION[behavior.activation];
|
|
1157
|
-
if (!effectName) {
|
|
1158
|
-
contradict(
|
|
1159
|
-
state,
|
|
1160
|
-
`native_effect does not support ${behavior.activation} activation`,
|
|
1161
|
-
);
|
|
1162
|
-
return null;
|
|
1163
|
-
}
|
|
1164
|
-
if (check.event !== effectName) {
|
|
1165
|
-
contradict(
|
|
1166
|
-
state,
|
|
1167
|
-
`${check.event || 'the supplied effect'} does not match ${effectName}`,
|
|
1168
|
-
);
|
|
1169
|
-
}
|
|
1170
|
-
const effect = directNodeProperty(trigger, effectName);
|
|
1171
|
-
if (!isRecord(effect) || Object.keys(effect).length === 0) {
|
|
1172
|
-
unknown(state, `${effectName} is not visible on the serialized target`);
|
|
1173
|
-
return null;
|
|
1174
|
-
}
|
|
1175
|
-
observe(state, 'native_effect', {
|
|
1176
|
-
nodeId: check.triggerNodeId,
|
|
1177
|
-
effectName,
|
|
1178
|
-
value: effect,
|
|
1179
|
-
});
|
|
1180
|
-
if (
|
|
1181
|
-
effectName === 'appearEffect'
|
|
1182
|
-
&& !['onInView', 'onMount', 'onScrollTarget', 'onScrollDirection']
|
|
1183
|
-
.includes(effect.trigger)
|
|
1184
|
-
) {
|
|
1185
|
-
contradict(state, 'appearEffect does not declare a supported trigger');
|
|
1186
|
-
}
|
|
1187
|
-
return {
|
|
1188
|
-
effectName,
|
|
1189
|
-
observedEffect: effect,
|
|
1190
|
-
};
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
function verdictFor(state) {
|
|
1194
|
-
if (state.contradictions.length > 0) return 'contradicted';
|
|
1195
|
-
if (state.unknowns.length > 0) return 'unknown';
|
|
1196
|
-
return 'verified';
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
function applySemanticAssessment(
|
|
1200
|
-
check,
|
|
1201
|
-
result,
|
|
1202
|
-
mode,
|
|
1203
|
-
liveEvidenceNodeIds = new Set(),
|
|
1204
|
-
) {
|
|
1205
|
-
const assessment = check.semanticAssessment;
|
|
1206
|
-
const base = {
|
|
1207
|
-
...result,
|
|
1208
|
-
deterministicVerdict: result.verdict,
|
|
1209
|
-
semanticAssessment: assessment
|
|
1210
|
-
? {
|
|
1211
|
-
...assessment,
|
|
1212
|
-
mode,
|
|
1213
|
-
accepted: false,
|
|
1214
|
-
}
|
|
1215
|
-
: null,
|
|
1216
|
-
};
|
|
1217
|
-
if (
|
|
1218
|
-
mode !== 'enforce'
|
|
1219
|
-
|| result.verdict !== 'unknown'
|
|
1220
|
-
|| !assessment
|
|
1221
|
-
|| assessment.verdict === 'unknown'
|
|
1222
|
-
) return base;
|
|
1223
|
-
if (
|
|
1224
|
-
assessment.evidenceNodeIds.length === 0
|
|
1225
|
-
|| !assessment.evidenceNodeIds.every((nodeId) =>
|
|
1226
|
-
liveEvidenceNodeIds.has(nodeId))
|
|
1227
|
-
) {
|
|
1228
167
|
return {
|
|
1229
|
-
...base,
|
|
1230
|
-
unknowns: uniqueStrings([
|
|
1231
|
-
...base.unknowns,
|
|
1232
|
-
'semantic verification was not accepted because its evidence IDs were not backed by live observations',
|
|
1233
|
-
]),
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
return {
|
|
1237
|
-
...base,
|
|
1238
|
-
verdict: assessment.verdict,
|
|
1239
|
-
contradictions:
|
|
1240
|
-
assessment.verdict === 'contradicted'
|
|
1241
|
-
? uniqueStrings([
|
|
1242
|
-
...base.contradictions,
|
|
1243
|
-
`Semantic assessment contradicted the behavior: ${assessment.rationale}`,
|
|
1244
|
-
], 30)
|
|
1245
|
-
: base.contradictions,
|
|
1246
|
-
semanticAssessment: {
|
|
1247
|
-
...base.semanticAssessment,
|
|
1248
|
-
accepted: true,
|
|
1249
|
-
},
|
|
1250
|
-
};
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
export function verifyBehaviorEvidence({
|
|
1254
|
-
plan,
|
|
1255
|
-
checks,
|
|
1256
|
-
snapshots,
|
|
1257
|
-
context = {},
|
|
1258
|
-
}) {
|
|
1259
|
-
const behaviorById = new Map(
|
|
1260
|
-
(plan?.behaviors || []).map((behavior) => [behavior.id, behavior]),
|
|
1261
|
-
);
|
|
1262
|
-
const mechanismByBehavior = new Map(
|
|
1263
|
-
(plan?.mechanisms || []).map((decision) => [
|
|
1264
|
-
decision.behaviorId,
|
|
1265
|
-
decision,
|
|
1266
|
-
]),
|
|
1267
|
-
);
|
|
1268
|
-
const liveEvidenceNodeIds = new Set(
|
|
1269
|
-
snapshots instanceof Map
|
|
1270
|
-
? [...snapshots.entries()]
|
|
1271
|
-
.filter(([, snapshot]) => Boolean(snapshot))
|
|
1272
|
-
.map(([nodeId]) => nodeId)
|
|
1273
|
-
: Object.entries(snapshots || {})
|
|
1274
|
-
.filter(([, snapshot]) => Boolean(snapshot))
|
|
1275
|
-
.map(([nodeId]) => nodeId),
|
|
1276
|
-
);
|
|
1277
|
-
const verifiedChecks = checks.map((check) => {
|
|
1278
|
-
const behavior = behaviorById.get(check.behaviorId);
|
|
1279
|
-
const decision = mechanismByBehavior.get(check.behaviorId);
|
|
1280
|
-
const state = evidenceState();
|
|
1281
|
-
let nativeEffectEvidence = null;
|
|
1282
|
-
if (!behavior || !decision) {
|
|
1283
|
-
contradict(state, 'the planned behavior or mechanism was not found');
|
|
1284
|
-
} else if (decision.mechanism === 'native_link') {
|
|
1285
|
-
verifyNativeLink(check, behavior, snapshots, state, context);
|
|
1286
|
-
} else if (decision.mechanism === 'native_overlay') {
|
|
1287
|
-
verifyOverlay(check, behavior, snapshots, state);
|
|
1288
|
-
} else if (decision.mechanism === 'native_component_variants') {
|
|
1289
|
-
verifyComponentVariants(check, behavior, snapshots, state, context);
|
|
1290
|
-
} else if (decision.mechanism === 'existing_component_control') {
|
|
1291
|
-
verifyExistingComponent(check, behavior, snapshots, state, context);
|
|
1292
|
-
} else if (decision.mechanism === 'code_component') {
|
|
1293
|
-
verifyCodeComponent(check, behavior, snapshots, state, context);
|
|
1294
|
-
} else if (decision.mechanism === 'native_effect') {
|
|
1295
|
-
nativeEffectEvidence = verifyNativeEffect(
|
|
1296
|
-
check,
|
|
1297
|
-
behavior,
|
|
1298
|
-
snapshots,
|
|
1299
|
-
state,
|
|
1300
|
-
);
|
|
1301
|
-
} else {
|
|
1302
|
-
verifyNativeEvent(check, behavior, snapshots, state);
|
|
1303
|
-
}
|
|
1304
|
-
const result = {
|
|
1305
168
|
...check,
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
verdict: verdictFor(state),
|
|
1309
|
-
contradictions: uniqueStrings(state.contradictions, 30),
|
|
1310
|
-
unknowns: uniqueStrings(state.unknowns, 30),
|
|
1311
|
-
observations: state.observations.slice(0, 50),
|
|
1312
|
-
repairAllowed: state.contradictions.length > 0,
|
|
1313
|
-
nextSafeActions:
|
|
1314
|
-
state.contradictions.length > 0
|
|
1315
|
-
? ['Repair only the observed contradictory properties, then verify once.']
|
|
1316
|
-
: state.unknowns.length > 0
|
|
1317
|
-
? ['Collect a different live evidence source or provide a grounded semantic assessment; do not mutate based only on unknown evidence.']
|
|
1318
|
-
: [],
|
|
169
|
+
verdict: 'verified',
|
|
170
|
+
observedValue: observed.slice(0, MAX_OBSERVED_CHARS),
|
|
1319
171
|
};
|
|
1320
|
-
return applySemanticAssessment(
|
|
1321
|
-
check,
|
|
1322
|
-
result,
|
|
1323
|
-
['off', 'shadow', 'enforce'].includes(context?.semanticVerifierMode)
|
|
1324
|
-
? context.semanticVerifierMode
|
|
1325
|
-
: 'shadow',
|
|
1326
|
-
liveEvidenceNodeIds,
|
|
1327
|
-
);
|
|
1328
172
|
});
|
|
173
|
+
}
|
|
1329
174
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
&& check.instanceNodeIds.length > 0
|
|
1341
|
-
)
|
|
1342
|
-
? check.instanceNodeIds
|
|
1343
|
-
: [check.triggerNodeId].filter(Boolean);
|
|
1344
|
-
const observedTargets = new Set(matching.flatMap(targetIdsFor));
|
|
1345
|
-
const verifiedTargets = new Set(
|
|
1346
|
-
matching
|
|
1347
|
-
.filter((check) => check.verdict === 'verified')
|
|
1348
|
-
.flatMap(targetIdsFor),
|
|
1349
|
-
);
|
|
1350
|
-
const contradictedTargets = new Set(
|
|
1351
|
-
matching
|
|
1352
|
-
.filter((check) => check.verdict === 'contradicted')
|
|
1353
|
-
.flatMap(targetIdsFor),
|
|
175
|
+
// Summarizes the current check results for every planned behavior. A behavior
|
|
176
|
+
// is verified once each of its declared target nodes has a verified check.
|
|
177
|
+
// status: missing (a behavior or target was never checked), contradicted (a
|
|
178
|
+
// property holds a different value), unverified (some properties could not be
|
|
179
|
+
// found, so the behavior is unconfirmed), or verified.
|
|
180
|
+
export function summarizeBehaviorVerification(plan, checkResults) {
|
|
181
|
+
const behaviors = (plan?.behaviors || []).map((behavior) => {
|
|
182
|
+
const results = checkResults.filter((result) => result.behaviorId === behavior.id);
|
|
183
|
+
const verifiedNodeIds = new Set(
|
|
184
|
+
results.filter((result) => result.verdict === 'verified').map((result) => result.nodeId),
|
|
1354
185
|
);
|
|
1355
|
-
const contradictions =
|
|
1356
|
-
.filter((
|
|
1357
|
-
.
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
`verified ${verifiedTargets.size} of ${behavior.expectedTargets} expected targets`,
|
|
1365
|
-
);
|
|
1366
|
-
}
|
|
1367
|
-
const verdict =
|
|
1368
|
-
matching.some((check) => check.verdict === 'contradicted')
|
|
1369
|
-
? 'contradicted'
|
|
1370
|
-
: matching.some((check) => check.verdict === 'unknown')
|
|
1371
|
-
|| unknowns.length > 0
|
|
1372
|
-
? 'unknown'
|
|
1373
|
-
: 'verified';
|
|
186
|
+
const contradictions = results
|
|
187
|
+
.filter((result) => result.verdict === 'contradicted')
|
|
188
|
+
.map((result) => result.reason);
|
|
189
|
+
const checkedNodeIds = new Set(results.map((result) => result.nodeId));
|
|
190
|
+
const uncheckedTargets = behavior.targetNodeIds.filter((nodeId) => !checkedNodeIds.has(nodeId));
|
|
191
|
+
const unverifiedTargets = behavior.targetNodeIds.filter((nodeId) => !verifiedNodeIds.has(nodeId));
|
|
192
|
+
const covered = behavior.targetNodeIds.length > 0
|
|
193
|
+
? unverifiedTargets.length === 0
|
|
194
|
+
: verifiedNodeIds.size > 0;
|
|
1374
195
|
return {
|
|
1375
196
|
behaviorId: behavior.id,
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
- contradictedTargets.size,
|
|
197
|
+
checked: results.length > 0 && uncheckedTargets.length === 0,
|
|
198
|
+
verdict: contradictions.length > 0 ? 'contradicted' : covered ? 'verified' : 'unknown',
|
|
199
|
+
verifiedTargets: verifiedNodeIds.size,
|
|
200
|
+
expectedTargets: behavior.targetNodeIds.length,
|
|
201
|
+
unverifiedTargets,
|
|
202
|
+
uncheckedTargets,
|
|
203
|
+
contradictions: uniqueStrings(contradictions, 20),
|
|
204
|
+
unknowns: uniqueStrings(
|
|
205
|
+
results.filter((result) => result.verdict === 'unknown').map((result) => result.reason),
|
|
206
|
+
20,
|
|
1387
207
|
),
|
|
1388
|
-
verdict,
|
|
1389
|
-
contradictions: uniqueStrings(contradictions, 50),
|
|
1390
|
-
unknowns: uniqueStrings(unknowns, 50),
|
|
1391
208
|
};
|
|
1392
209
|
});
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
&& behaviorResults.every((result) => result.verdict === 'verified');
|
|
1402
|
-
const hasContradictions = behaviorResults.some((result) =>
|
|
1403
|
-
result.verdict === 'contradicted');
|
|
1404
|
-
const usedSemanticAssessment = verifiedChecks.some((check) =>
|
|
1405
|
-
check.semanticAssessment?.accepted === true);
|
|
1406
|
-
return {
|
|
1407
|
-
completed,
|
|
1408
|
-
verificationLevel:
|
|
1409
|
-
completed && usedSemanticAssessment
|
|
1410
|
-
? 'semantically_verified'
|
|
1411
|
-
: completed
|
|
1412
|
-
? 'structurally_verified'
|
|
1413
|
-
: hasContradictions
|
|
1414
|
-
? 'contradicted'
|
|
1415
|
-
: 'unknown',
|
|
1416
|
-
checks: verifiedChecks,
|
|
1417
|
-
behaviors: behaviorResults,
|
|
1418
|
-
representativeBehaviorIds,
|
|
1419
|
-
};
|
|
210
|
+
const status = behaviors.some((behavior) => !behavior.checked)
|
|
211
|
+
? 'missing'
|
|
212
|
+
: behaviors.some((behavior) => behavior.verdict === 'contradicted')
|
|
213
|
+
? 'contradicted'
|
|
214
|
+
: behaviors.some((behavior) => behavior.verdict === 'unknown')
|
|
215
|
+
? 'unverified'
|
|
216
|
+
: 'verified';
|
|
217
|
+
return { status, behaviors };
|
|
1420
218
|
}
|
|
1421
219
|
|
|
1422
220
|
export function summarizeFramerChanges(changes) {
|