@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.
@@ -1,118 +1,12 @@
1
1
  import crypto from 'node:crypto';
2
2
 
3
- export const BEHAVIOR_ACTIVATIONS = [
4
- 'tap',
5
- 'hover',
6
- 'submit',
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
- export const BEHAVIOR_PRESENTATIONS = [
12
- 'none',
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
- if (!isRecord(rawBehavior)) {
166
- throw contractError(
167
- 'FRAMER_BEHAVIOR_CONTRACT_INVALID',
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} must declare its identity, trigger, activation, presentation, initial state, activated state, and layout effect.`,
39
+ `Behavior ${index + 1} needs an id and a description of what the user should experience.`,
192
40
  );
193
41
  }
194
- const behavior = {
42
+ return {
195
43
  id,
196
44
  description,
197
- triggerDescription,
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, inheritedRejected = []) {
328
- const behaviors = (Array.isArray(rawArguments.behaviors)
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 mechanisms = (Array.isArray(rawArguments.mechanisms)
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 (behaviorIds.has(behavior.id)) {
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
- behaviorIds.add(behavior.id);
61
+ ids.add(behavior.id);
347
62
  }
348
- const mechanismByBehavior = new Map();
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
- const mechanisms = new Map(
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 behaviorById = new Map(
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
- 'Declare evidence for at least one planned behavior.',
79
+ 'Name at least one node and the Framer property that carries a planned behavior.',
674
80
  );
675
81
  }
676
- return checks.map((raw) => normalizeEvidence(raw, behaviorById));
677
- }
678
-
679
- export function evidenceNodeIds(checks) {
680
- return uniqueStrings(checks.flatMap((check) => [
681
- check.triggerNodeId,
682
- check.targetNodeId,
683
- check.componentNodeId,
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
- const authorizedContainerIds = uniqueStrings(
842
- context?.authorizedInstanceContainerIds,
843
- 100,
844
- );
845
- if (instance.snapshot && authorizedContainerIds.length > 0) {
846
- const ancestry = new Set([
847
- instance.id,
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 verifyComponentVariants(
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 destinationCandidates(value) {
950
- const candidate = text(value, 2000);
951
- if (!candidate) return [];
952
- const normalized = candidate.toLowerCase().replace(/\/+$/, '') || '/';
953
- const candidates = new Set([normalized]);
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
- function linkFacts(value) {
968
- const facts = [];
969
- const visit = (current, path = []) => {
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((nested, index) => visit(nested, [...path, String(index)]));
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
- const nextPath = [...path, key];
977
- if (
978
- typeof nested === 'string'
979
- && /(?:href|url|link|destination|route|path)$/i.test(key)
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(value);
991
- return facts.filter((fact) => fact.candidates.length > 0);
132
+ visit(node, 0);
133
+ return values;
992
134
  }
993
135
 
994
- function verifyNativeLink(check, behavior, snapshots, state, context) {
995
- const targetIds = check.instanceNodeIds.length > 0
996
- ? check.instanceNodeIds
997
- : [check.triggerNodeId].filter(Boolean);
998
- const expectedValues = check.expectedDestination
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
- for (const nodeId of targetIds) {
1007
- const snapshot = snapshotFor(snapshots, nodeId);
1008
- if (!snapshot) {
1009
- unknown(state, `navigation node ${nodeId} was not found`);
1010
- continue;
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 facts = linkFacts([
1013
- nodeFromSnapshot(snapshot),
1014
- snapshot?.controls,
1015
- ]);
1016
- if (facts.length === 0) {
1017
- const controlFacts = linkFacts(context?.componentControls);
1018
- if (controlFacts.length > 0) {
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 matchingFact = facts.find((fact) =>
1031
- fact.candidates.some((candidate) => expected.has(candidate)));
1032
- if (matchingFact) {
1033
- observe(state, 'navigation_destination', {
1034
- nodeId,
1035
- source: matchingFact.path,
1036
- value: matchingFact.value,
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
- mechanism: decision?.mechanism,
1307
- ...(nativeEffectEvidence || {}),
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
- const behaviorResults = (plan?.behaviors || []).map((behavior) => {
1331
- const decision = mechanismByBehavior.get(behavior.id);
1332
- const matching = verifiedChecks.filter((check) =>
1333
- check.behaviorId === behavior.id);
1334
- const targetIdsFor = (check) =>
1335
- decision?.mechanism === 'native_component_variants'
1336
- || decision?.mechanism === 'existing_component_control'
1337
- || decision?.mechanism === 'code_component'
1338
- || (
1339
- decision?.mechanism === 'native_link'
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 = matching
1356
- .filter((check) => check.verdict === 'contradicted')
1357
- .flatMap((check) => check.contradictions);
1358
- const unknowns = matching
1359
- .filter((check) => check.verdict === 'unknown')
1360
- .flatMap((check) => check.unknowns);
1361
- if (matching.length === 0) unknowns.push('no evidence was supplied');
1362
- if (verifiedTargets.size < behavior.expectedTargets) {
1363
- unknowns.push(
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
- behaviorSignature: behavior.signature,
1377
- mechanism: decision?.mechanism,
1378
- expectedTargets: behavior.expectedTargets,
1379
- observedTargets: observedTargets.size,
1380
- verifiedTargets: verifiedTargets.size,
1381
- contradictedTargets: contradictedTargets.size,
1382
- unknownTargets: Math.max(
1383
- 0,
1384
- behavior.expectedTargets
1385
- - verifiedTargets.size
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 representativeBehaviorIds = behaviorResults
1394
- .filter((result) =>
1395
- verifiedChecks.some((check) =>
1396
- check.behaviorId === result.behaviorId
1397
- && check.verdict === 'verified'))
1398
- .map((result) => result.behaviorId);
1399
- const completed =
1400
- behaviorResults.length > 0
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) {