outcome-cli 1.0.0

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.
Files changed (113) hide show
  1. package/README.md +261 -0
  2. package/package.json +95 -0
  3. package/src/agents/README.md +139 -0
  4. package/src/agents/adapters/anthropic.adapter.ts +166 -0
  5. package/src/agents/adapters/dalle.adapter.ts +145 -0
  6. package/src/agents/adapters/gemini.adapter.ts +134 -0
  7. package/src/agents/adapters/imagen.adapter.ts +106 -0
  8. package/src/agents/adapters/nano-banana.adapter.ts +129 -0
  9. package/src/agents/adapters/openai.adapter.ts +165 -0
  10. package/src/agents/adapters/veo.adapter.ts +130 -0
  11. package/src/agents/agent.schema.property.test.ts +379 -0
  12. package/src/agents/agent.schema.test.ts +148 -0
  13. package/src/agents/agent.schema.ts +263 -0
  14. package/src/agents/index.ts +60 -0
  15. package/src/agents/registered-agent.schema.ts +356 -0
  16. package/src/agents/registry.ts +97 -0
  17. package/src/agents/tournament-configs.property.test.ts +266 -0
  18. package/src/cli/README.md +145 -0
  19. package/src/cli/commands/define.ts +79 -0
  20. package/src/cli/commands/list.ts +46 -0
  21. package/src/cli/commands/logs.ts +83 -0
  22. package/src/cli/commands/run.ts +416 -0
  23. package/src/cli/commands/verify.ts +110 -0
  24. package/src/cli/index.ts +81 -0
  25. package/src/config/README.md +128 -0
  26. package/src/config/env.ts +262 -0
  27. package/src/config/index.ts +19 -0
  28. package/src/eval/README.md +318 -0
  29. package/src/eval/ai-judge.test.ts +435 -0
  30. package/src/eval/ai-judge.ts +368 -0
  31. package/src/eval/code-validators.ts +414 -0
  32. package/src/eval/evaluateOutcome.property.test.ts +1174 -0
  33. package/src/eval/evaluateOutcome.ts +591 -0
  34. package/src/eval/immigration-validators.ts +122 -0
  35. package/src/eval/index.ts +90 -0
  36. package/src/eval/judge-cache.ts +402 -0
  37. package/src/eval/tournament-validators.property.test.ts +439 -0
  38. package/src/eval/validators.property.test.ts +1118 -0
  39. package/src/eval/validators.ts +1199 -0
  40. package/src/eval/weighted-scorer.ts +285 -0
  41. package/src/index.ts +17 -0
  42. package/src/league/README.md +188 -0
  43. package/src/league/health-check.ts +353 -0
  44. package/src/league/index.ts +93 -0
  45. package/src/league/killAgent.ts +151 -0
  46. package/src/league/league.test.ts +1151 -0
  47. package/src/league/runLeague.ts +843 -0
  48. package/src/league/scoreAgent.ts +175 -0
  49. package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
  50. package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
  51. package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
  52. package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
  53. package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
  54. package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
  55. package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
  56. package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
  57. package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
  58. package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
  59. package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
  60. package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
  61. package/src/modules/omnibridge/api/.gitkeep +1 -0
  62. package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
  63. package/src/modules/omnibridge/auth/.gitkeep +1 -0
  64. package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
  65. package/src/modules/omnibridge/auth/session-vault.ts +577 -0
  66. package/src/modules/omnibridge/core/.gitkeep +1 -0
  67. package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
  68. package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
  69. package/src/modules/omnibridge/core/types.ts +610 -0
  70. package/src/modules/omnibridge/execution/.gitkeep +1 -0
  71. package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
  72. package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
  73. package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
  74. package/src/modules/omnibridge/index.ts +212 -0
  75. package/src/modules/omnibridge/omnibridge.ts +510 -0
  76. package/src/modules/omnibridge/verification/.gitkeep +1 -0
  77. package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
  78. package/src/outcomes/README.md +75 -0
  79. package/src/outcomes/acquire-pilot-customer.ts +297 -0
  80. package/src/outcomes/code-delivery-outcomes.ts +89 -0
  81. package/src/outcomes/code-outcomes.ts +256 -0
  82. package/src/outcomes/code_review_battle.test.ts +135 -0
  83. package/src/outcomes/code_review_battle.ts +135 -0
  84. package/src/outcomes/cold_email_battle.ts +97 -0
  85. package/src/outcomes/content_creation_battle.ts +160 -0
  86. package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
  87. package/src/outcomes/index.ts +107 -0
  88. package/src/outcomes/lead_gen_battle.test.ts +113 -0
  89. package/src/outcomes/lead_gen_battle.ts +99 -0
  90. package/src/outcomes/outcome.schema.property.test.ts +229 -0
  91. package/src/outcomes/outcome.schema.ts +187 -0
  92. package/src/outcomes/qualified_sales_interest.ts +118 -0
  93. package/src/outcomes/swarm_planner.property.test.ts +370 -0
  94. package/src/outcomes/swarm_planner.ts +96 -0
  95. package/src/outcomes/web_extraction.ts +234 -0
  96. package/src/runtime/README.md +220 -0
  97. package/src/runtime/agentRunner.test.ts +341 -0
  98. package/src/runtime/agentRunner.ts +746 -0
  99. package/src/runtime/claudeAdapter.ts +232 -0
  100. package/src/runtime/costTracker.ts +123 -0
  101. package/src/runtime/index.ts +34 -0
  102. package/src/runtime/modelAdapter.property.test.ts +305 -0
  103. package/src/runtime/modelAdapter.ts +144 -0
  104. package/src/runtime/openaiAdapter.ts +235 -0
  105. package/src/utils/README.md +122 -0
  106. package/src/utils/command-runner.ts +134 -0
  107. package/src/utils/cost-guard.ts +379 -0
  108. package/src/utils/errors.test.ts +290 -0
  109. package/src/utils/errors.ts +442 -0
  110. package/src/utils/index.ts +37 -0
  111. package/src/utils/logger.test.ts +361 -0
  112. package/src/utils/logger.ts +419 -0
  113. package/src/utils/output-parsers.ts +216 -0
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Triangulation Engine
3
+ *
4
+ * Self-healing system using three anchor types (Spatial, Semantic, Functional)
5
+ * to locate elements even when UI changes.
6
+ *
7
+ * Requirements: 2.1, 2.2, 2.4, 2.5, 2.6
8
+ */
9
+
10
+ import type {
11
+ IntentDocument,
12
+ IntentElement,
13
+ AnchorSet,
14
+ SpatialAnchor,
15
+ SemanticAnchor,
16
+ FunctionalAnchor,
17
+ LocateResult,
18
+ HealResult,
19
+ OmniBridgeError,
20
+ } from './types.js';
21
+
22
+ /**
23
+ * Confidence threshold below which elements are flagged for review
24
+ * Requirement 2.5
25
+ */
26
+ const CONFIDENCE_THRESHOLD = 0.7;
27
+
28
+ /**
29
+ * Minimum anchors required for successful location
30
+ * Requirement 2.2
31
+ */
32
+ const MIN_ANCHORS_FOR_MATCH = 2;
33
+
34
+ /**
35
+ * Triangulation Engine class
36
+ * Implements self-healing element location using three-anchor system
37
+ */
38
+ export class TriangulationEngine {
39
+ /**
40
+ * Store anchors for an element (Requirement 2.1)
41
+ * Creates all three anchor types: Spatial, Semantic, and Functional
42
+ */
43
+ storeAnchors(
44
+ element: IntentElement,
45
+ document: IntentDocument,
46
+ elementIndex: number
47
+ ): AnchorSet {
48
+ const spatial = this.createSpatialAnchor(element, document, elementIndex);
49
+ const semantic = this.createSemanticAnchor(element);
50
+ const functional = this.createFunctionalAnchor(element);
51
+
52
+ return {
53
+ spatial,
54
+ semantic,
55
+ functional,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Create a Spatial anchor for an element
61
+ * Stores region, relative position, and nearby landmarks
62
+ */
63
+ private createSpatialAnchor(
64
+ element: IntentElement,
65
+ document: IntentDocument,
66
+ elementIndex: number
67
+ ): SpatialAnchor {
68
+ // Determine region based on element context and position
69
+ const region = this.inferRegion(element, document, elementIndex);
70
+
71
+ // Calculate relative position (normalized 0-1)
72
+ const totalElements = document.elements.length;
73
+ const relativePosition = {
74
+ x: 0.5, // Default to center (would be calculated from actual DOM position)
75
+ y: totalElements > 0 ? elementIndex / totalElements : 0,
76
+ };
77
+
78
+ // Find nearby landmarks (other semantic elements)
79
+ const nearbyLandmarks = this.findNearbyLandmarks(document, elementIndex);
80
+
81
+ return {
82
+ region,
83
+ relativePosition,
84
+ nearbyLandmarks,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Infer the region of an element based on its context
90
+ */
91
+ private inferRegion(
92
+ element: IntentElement,
93
+ document: IntentDocument,
94
+ elementIndex: number
95
+ ): SpatialAnchor['region'] {
96
+ const totalElements = document.elements.length;
97
+ const positionRatio = totalElements > 0 ? elementIndex / totalElements : 0.5;
98
+
99
+ // Check for explicit region indicators
100
+ if (element.tagName === 'header' || element.ariaRole === 'banner') {
101
+ return 'header';
102
+ }
103
+ if (element.tagName === 'footer' || element.ariaRole === 'contentinfo') {
104
+ return 'footer';
105
+ }
106
+ if (element.tagName === 'aside' || element.ariaRole === 'complementary') {
107
+ return 'sidebar';
108
+ }
109
+ if (element.tagName === 'nav' || element.ariaRole === 'navigation') {
110
+ // Navigation at top is header, at bottom is footer
111
+ return positionRatio < 0.3 ? 'header' : positionRatio > 0.7 ? 'footer' : 'main';
112
+ }
113
+
114
+ // Infer from position
115
+ if (positionRatio < 0.15) return 'header';
116
+ if (positionRatio > 0.85) return 'footer';
117
+
118
+ return 'main';
119
+ }
120
+
121
+
122
+ /**
123
+ * Find nearby landmark elements for spatial context
124
+ */
125
+ private findNearbyLandmarks(
126
+ document: IntentDocument,
127
+ elementIndex: number,
128
+ range: number = 3
129
+ ): string[] {
130
+ const landmarks: string[] = [];
131
+ const elements = document.elements;
132
+
133
+ // Look at elements before and after
134
+ for (let i = Math.max(0, elementIndex - range); i < Math.min(elements.length, elementIndex + range + 1); i++) {
135
+ if (i === elementIndex) continue;
136
+
137
+ const el = elements[i];
138
+ // Include elements with strong semantic meaning as landmarks
139
+ if (
140
+ el.ariaRole ||
141
+ el.tagName === 'nav' ||
142
+ el.tagName === 'header' ||
143
+ el.tagName === 'footer' ||
144
+ el.tagName === 'main' ||
145
+ el.role === 'navigation' ||
146
+ el.intentId.includes('NAV_ID') ||
147
+ el.intentId.includes('ACTION_ID')
148
+ ) {
149
+ landmarks.push(el.intentId);
150
+ }
151
+ }
152
+
153
+ return landmarks.slice(0, 5); // Limit to 5 landmarks
154
+ }
155
+
156
+ /**
157
+ * Create a Semantic anchor for an element
158
+ * Stores Intent_ID, labels, ARIA roles, and text content
159
+ */
160
+ private createSemanticAnchor(element: IntentElement): SemanticAnchor {
161
+ return {
162
+ intentId: element.intentId,
163
+ labels: [element.label, ...element.contextHints].filter(Boolean),
164
+ ariaRoles: element.ariaRole ? [element.ariaRole] : [],
165
+ textContent: element.label,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Create a Functional anchor for an element
171
+ * Stores event types, form targets, and navigation targets
172
+ */
173
+ private createFunctionalAnchor(element: IntentElement): FunctionalAnchor {
174
+ const eventTypes: string[] = [];
175
+ let formTarget: string | undefined;
176
+ let navigationTarget: string | undefined;
177
+
178
+ // Infer event types from element role and tag
179
+ if (element.role === 'action' || element.tagName === 'button') {
180
+ eventTypes.push('click');
181
+ }
182
+ if (element.role === 'input' || ['input', 'textarea', 'select'].includes(element.tagName)) {
183
+ eventTypes.push('input', 'change', 'focus', 'blur');
184
+ }
185
+ if (element.role === 'navigation' || element.tagName === 'a') {
186
+ eventTypes.push('click');
187
+ // Extract navigation target from intentId if it's a nav element
188
+ if (element.intentId.includes('NAV_ID')) {
189
+ navigationTarget = element.intentId;
190
+ }
191
+ }
192
+
193
+ // Check if element is part of a form
194
+ if (element.intentId.includes('INPUT_ID') || element.intentId.includes('SUBMIT')) {
195
+ formTarget = 'form';
196
+ }
197
+
198
+ return {
199
+ eventTypes,
200
+ formTarget,
201
+ navigationTarget,
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Locate an element using stored anchors (Requirement 2.2)
207
+ * Returns found=true if 2+ anchors match
208
+ */
209
+ locate(anchors: AnchorSet, document: IntentDocument): LocateResult {
210
+ let bestMatch: IntentElement | undefined;
211
+ let bestMatchCount = 0;
212
+ let bestConfidence = 0;
213
+
214
+ for (let i = 0; i < document.elements.length; i++) {
215
+ const element = document.elements[i];
216
+ const currentAnchors = this.storeAnchors(element, document, i);
217
+
218
+ const { matchCount, confidence } = this.calculateAnchorMatch(anchors, currentAnchors);
219
+
220
+ if (matchCount > bestMatchCount || (matchCount === bestMatchCount && confidence > bestConfidence)) {
221
+ bestMatch = element;
222
+ bestMatchCount = matchCount;
223
+ bestConfidence = confidence;
224
+ }
225
+ }
226
+
227
+ // Requirement 2.2: Return found=true if 2+ anchors match
228
+ const found = bestMatchCount >= MIN_ANCHORS_FOR_MATCH;
229
+
230
+ // Requirement 2.5: Flag for review if confidence < 70%
231
+ const flaggedForReview = bestConfidence < CONFIDENCE_THRESHOLD;
232
+
233
+ return {
234
+ found,
235
+ element: found ? bestMatch : undefined,
236
+ matchedAnchors: bestMatchCount,
237
+ confidence: bestConfidence,
238
+ flaggedForReview,
239
+ };
240
+ }
241
+
242
+
243
+ /**
244
+ * Calculate how well two anchor sets match
245
+ * Returns match count (0-3) and confidence score (0-1)
246
+ */
247
+ private calculateAnchorMatch(
248
+ stored: AnchorSet,
249
+ current: AnchorSet
250
+ ): { matchCount: number; confidence: number } {
251
+ let matchCount = 0;
252
+ let totalConfidence = 0;
253
+
254
+ // Check Spatial anchor match
255
+ const spatialMatch = this.matchSpatialAnchor(stored.spatial, current.spatial);
256
+ if (spatialMatch > 0.5) {
257
+ matchCount++;
258
+ totalConfidence += spatialMatch;
259
+ }
260
+
261
+ // Check Semantic anchor match
262
+ const semanticMatch = this.matchSemanticAnchor(stored.semantic, current.semantic);
263
+ if (semanticMatch > 0.5) {
264
+ matchCount++;
265
+ totalConfidence += semanticMatch;
266
+ }
267
+
268
+ // Check Functional anchor match
269
+ const functionalMatch = this.matchFunctionalAnchor(stored.functional, current.functional);
270
+ if (functionalMatch > 0.5) {
271
+ matchCount++;
272
+ totalConfidence += functionalMatch;
273
+ }
274
+
275
+ // Calculate overall confidence
276
+ const confidence = matchCount > 0 ? totalConfidence / 3 : 0;
277
+
278
+ return { matchCount, confidence };
279
+ }
280
+
281
+ /**
282
+ * Match two Spatial anchors
283
+ * Returns similarity score 0-1
284
+ */
285
+ private matchSpatialAnchor(stored: SpatialAnchor, current: SpatialAnchor): number {
286
+ let score = 0;
287
+
288
+ // Region match (40% weight)
289
+ if (stored.region === current.region) {
290
+ score += 0.4;
291
+ }
292
+
293
+ // Position proximity (30% weight)
294
+ const positionDistance = Math.sqrt(
295
+ Math.pow(stored.relativePosition.x - current.relativePosition.x, 2) +
296
+ Math.pow(stored.relativePosition.y - current.relativePosition.y, 2)
297
+ );
298
+ const positionScore = Math.max(0, 1 - positionDistance);
299
+ score += positionScore * 0.3;
300
+
301
+ // Landmark overlap (30% weight)
302
+ const landmarkOverlap = this.calculateSetOverlap(
303
+ stored.nearbyLandmarks,
304
+ current.nearbyLandmarks
305
+ );
306
+ score += landmarkOverlap * 0.3;
307
+
308
+ return score;
309
+ }
310
+
311
+ /**
312
+ * Match two Semantic anchors
313
+ * Returns similarity score 0-1
314
+ */
315
+ private matchSemanticAnchor(stored: SemanticAnchor, current: SemanticAnchor): number {
316
+ let score = 0;
317
+
318
+ // Intent ID match (50% weight) - most important
319
+ if (stored.intentId === current.intentId) {
320
+ score += 0.5;
321
+ } else {
322
+ // Partial match on intent category
323
+ const storedCategory = stored.intentId.split('_ID:')[0];
324
+ const currentCategory = current.intentId.split('_ID:')[0];
325
+ if (storedCategory === currentCategory) {
326
+ score += 0.2;
327
+ }
328
+ }
329
+
330
+ // Label overlap (25% weight)
331
+ const labelOverlap = this.calculateSetOverlap(stored.labels, current.labels);
332
+ score += labelOverlap * 0.25;
333
+
334
+ // ARIA role match (15% weight)
335
+ const ariaOverlap = this.calculateSetOverlap(stored.ariaRoles, current.ariaRoles);
336
+ score += ariaOverlap * 0.15;
337
+
338
+ // Text content similarity (10% weight)
339
+ const textSimilarity = this.calculateTextSimilarity(stored.textContent, current.textContent);
340
+ score += textSimilarity * 0.1;
341
+
342
+ return score;
343
+ }
344
+
345
+ /**
346
+ * Match two Functional anchors
347
+ * Returns similarity score 0-1
348
+ */
349
+ private matchFunctionalAnchor(stored: FunctionalAnchor, current: FunctionalAnchor): number {
350
+ let score = 0;
351
+
352
+ // Event types overlap (40% weight)
353
+ const eventOverlap = this.calculateSetOverlap(stored.eventTypes, current.eventTypes);
354
+ score += eventOverlap * 0.4;
355
+
356
+ // Form target match (30% weight)
357
+ if (stored.formTarget && current.formTarget) {
358
+ if (stored.formTarget === current.formTarget) {
359
+ score += 0.3;
360
+ }
361
+ } else if (!stored.formTarget && !current.formTarget) {
362
+ score += 0.3; // Both have no form target
363
+ }
364
+
365
+ // Navigation target match (30% weight)
366
+ if (stored.navigationTarget && current.navigationTarget) {
367
+ if (stored.navigationTarget === current.navigationTarget) {
368
+ score += 0.3;
369
+ }
370
+ } else if (!stored.navigationTarget && !current.navigationTarget) {
371
+ score += 0.3; // Both have no navigation target
372
+ }
373
+
374
+ return score;
375
+ }
376
+
377
+
378
+ /**
379
+ * Calculate overlap between two string arrays
380
+ * Returns 0-1 (Jaccard similarity)
381
+ */
382
+ private calculateSetOverlap(a: string[], b: string[]): number {
383
+ if (a.length === 0 && b.length === 0) return 1;
384
+ if (a.length === 0 || b.length === 0) return 0;
385
+
386
+ const setA = new Set(a.map(s => s.toLowerCase()));
387
+ const setB = new Set(b.map(s => s.toLowerCase()));
388
+
389
+ let intersection = 0;
390
+ for (const item of setA) {
391
+ if (setB.has(item)) {
392
+ intersection++;
393
+ }
394
+ }
395
+
396
+ const union = setA.size + setB.size - intersection;
397
+ return union > 0 ? intersection / union : 0;
398
+ }
399
+
400
+ /**
401
+ * Calculate text similarity between two strings
402
+ * Uses simple word overlap for efficiency
403
+ */
404
+ private calculateTextSimilarity(a: string, b: string): number {
405
+ if (!a && !b) return 1;
406
+ if (!a || !b) return 0;
407
+
408
+ const wordsA = a.toLowerCase().split(/\s+/).filter(Boolean);
409
+ const wordsB = b.toLowerCase().split(/\s+/).filter(Boolean);
410
+
411
+ return this.calculateSetOverlap(wordsA, wordsB);
412
+ }
413
+
414
+ /**
415
+ * Heal (re-locate) an element after UI changes (Requirement 2.2)
416
+ * Updates anchors if element is found with 2+ anchor matches
417
+ */
418
+ heal(oldAnchors: AnchorSet, newDocument: IntentDocument): HealResult {
419
+ const locateResult = this.locate(oldAnchors, newDocument);
420
+
421
+ if (!locateResult.found || !locateResult.element) {
422
+ return {
423
+ healed: false,
424
+ changeLog: `Element not found. Matched ${locateResult.matchedAnchors}/3 anchors with confidence ${(locateResult.confidence * 100).toFixed(1)}%`,
425
+ };
426
+ }
427
+
428
+ // Find the element index to create new anchors
429
+ const elementIndex = newDocument.elements.findIndex(
430
+ el => el.intentId === locateResult.element!.intentId
431
+ );
432
+
433
+ const newAnchors = this.storeAnchors(locateResult.element, newDocument, elementIndex);
434
+
435
+ // Generate change log
436
+ const changes: string[] = [];
437
+ if (oldAnchors.spatial.region !== newAnchors.spatial.region) {
438
+ changes.push(`Region changed: ${oldAnchors.spatial.region} → ${newAnchors.spatial.region}`);
439
+ }
440
+ if (oldAnchors.semantic.intentId !== newAnchors.semantic.intentId) {
441
+ changes.push(`Intent ID changed: ${oldAnchors.semantic.intentId} → ${newAnchors.semantic.intentId}`);
442
+ }
443
+
444
+ return {
445
+ healed: true,
446
+ newAnchors,
447
+ changeLog: changes.length > 0
448
+ ? `Healed with ${locateResult.matchedAnchors}/3 anchors. Changes: ${changes.join('; ')}`
449
+ : `Healed with ${locateResult.matchedAnchors}/3 anchors. No significant changes.`,
450
+ };
451
+ }
452
+
453
+ /**
454
+ * Detect schema drift when all anchors fail (Requirement 2.6)
455
+ * Returns a schema_drift error with details about what changed
456
+ */
457
+ detectSchemaDrift(
458
+ anchors: AnchorSet,
459
+ document: IntentDocument
460
+ ): OmniBridgeError | null {
461
+ const locateResult = this.locate(anchors, document);
462
+
463
+ // If element was found, no schema drift
464
+ if (locateResult.found) {
465
+ return null;
466
+ }
467
+
468
+ // If all anchors failed (0 matches), this is schema drift
469
+ if (locateResult.matchedAnchors === 0) {
470
+ const details = this.generateSchemaDriftDetails(anchors, document);
471
+ return {
472
+ type: 'schema_drift',
473
+ details,
474
+ confidence: locateResult.confidence,
475
+ };
476
+ }
477
+
478
+ // Partial match (1 anchor) - not schema drift, just element moved
479
+ return null;
480
+ }
481
+
482
+ /**
483
+ * Generate details about what changed during schema drift
484
+ */
485
+ private generateSchemaDriftDetails(anchors: AnchorSet, document: IntentDocument): string {
486
+ const details: string[] = [];
487
+
488
+ // Check if the region still exists
489
+ const regionsInDoc = new Set(document.elements.map((_, i) =>
490
+ this.inferRegion(document.elements[i], document, i)
491
+ ));
492
+ if (!regionsInDoc.has(anchors.spatial.region)) {
493
+ details.push(`Region '${anchors.spatial.region}' no longer exists`);
494
+ }
495
+
496
+ // Check if similar intent IDs exist
497
+ const intentCategory = anchors.semantic.intentId.split('_ID:')[0];
498
+ const similarIntents = document.elements.filter(el =>
499
+ el.intentId.startsWith(intentCategory)
500
+ );
501
+ if (similarIntents.length === 0) {
502
+ details.push(`No elements with category '${intentCategory}' found`);
503
+ } else {
504
+ details.push(`Found ${similarIntents.length} elements with category '${intentCategory}' but none match`);
505
+ }
506
+
507
+ // Check if any landmarks remain
508
+ const currentLandmarks = document.elements
509
+ .filter(el => el.ariaRole || el.role === 'navigation')
510
+ .map(el => el.intentId);
511
+ const landmarkOverlap = this.calculateSetOverlap(
512
+ anchors.spatial.nearbyLandmarks,
513
+ currentLandmarks
514
+ );
515
+ if (landmarkOverlap < 0.2) {
516
+ details.push('Most nearby landmarks have changed');
517
+ }
518
+
519
+ return details.length > 0
520
+ ? details.join('. ')
521
+ : 'Element and surrounding context have changed significantly';
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Create a new Triangulation Engine instance
527
+ */
528
+ export function createTriangulationEngine(): TriangulationEngine {
529
+ return new TriangulationEngine();
530
+ }