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.
- package/README.md +261 -0
- package/package.json +95 -0
- package/src/agents/README.md +139 -0
- package/src/agents/adapters/anthropic.adapter.ts +166 -0
- package/src/agents/adapters/dalle.adapter.ts +145 -0
- package/src/agents/adapters/gemini.adapter.ts +134 -0
- package/src/agents/adapters/imagen.adapter.ts +106 -0
- package/src/agents/adapters/nano-banana.adapter.ts +129 -0
- package/src/agents/adapters/openai.adapter.ts +165 -0
- package/src/agents/adapters/veo.adapter.ts +130 -0
- package/src/agents/agent.schema.property.test.ts +379 -0
- package/src/agents/agent.schema.test.ts +148 -0
- package/src/agents/agent.schema.ts +263 -0
- package/src/agents/index.ts +60 -0
- package/src/agents/registered-agent.schema.ts +356 -0
- package/src/agents/registry.ts +97 -0
- package/src/agents/tournament-configs.property.test.ts +266 -0
- package/src/cli/README.md +145 -0
- package/src/cli/commands/define.ts +79 -0
- package/src/cli/commands/list.ts +46 -0
- package/src/cli/commands/logs.ts +83 -0
- package/src/cli/commands/run.ts +416 -0
- package/src/cli/commands/verify.ts +110 -0
- package/src/cli/index.ts +81 -0
- package/src/config/README.md +128 -0
- package/src/config/env.ts +262 -0
- package/src/config/index.ts +19 -0
- package/src/eval/README.md +318 -0
- package/src/eval/ai-judge.test.ts +435 -0
- package/src/eval/ai-judge.ts +368 -0
- package/src/eval/code-validators.ts +414 -0
- package/src/eval/evaluateOutcome.property.test.ts +1174 -0
- package/src/eval/evaluateOutcome.ts +591 -0
- package/src/eval/immigration-validators.ts +122 -0
- package/src/eval/index.ts +90 -0
- package/src/eval/judge-cache.ts +402 -0
- package/src/eval/tournament-validators.property.test.ts +439 -0
- package/src/eval/validators.property.test.ts +1118 -0
- package/src/eval/validators.ts +1199 -0
- package/src/eval/weighted-scorer.ts +285 -0
- package/src/index.ts +17 -0
- package/src/league/README.md +188 -0
- package/src/league/health-check.ts +353 -0
- package/src/league/index.ts +93 -0
- package/src/league/killAgent.ts +151 -0
- package/src/league/league.test.ts +1151 -0
- package/src/league/runLeague.ts +843 -0
- package/src/league/scoreAgent.ts +175 -0
- package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
- package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
- package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
- package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
- package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
- package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
- package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
- package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
- package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
- package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
- package/src/modules/omnibridge/api/.gitkeep +1 -0
- package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
- package/src/modules/omnibridge/auth/.gitkeep +1 -0
- package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
- package/src/modules/omnibridge/auth/session-vault.ts +577 -0
- package/src/modules/omnibridge/core/.gitkeep +1 -0
- package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
- package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
- package/src/modules/omnibridge/core/types.ts +610 -0
- package/src/modules/omnibridge/execution/.gitkeep +1 -0
- package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
- package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
- package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
- package/src/modules/omnibridge/index.ts +212 -0
- package/src/modules/omnibridge/omnibridge.ts +510 -0
- package/src/modules/omnibridge/verification/.gitkeep +1 -0
- package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
- package/src/outcomes/README.md +75 -0
- package/src/outcomes/acquire-pilot-customer.ts +297 -0
- package/src/outcomes/code-delivery-outcomes.ts +89 -0
- package/src/outcomes/code-outcomes.ts +256 -0
- package/src/outcomes/code_review_battle.test.ts +135 -0
- package/src/outcomes/code_review_battle.ts +135 -0
- package/src/outcomes/cold_email_battle.ts +97 -0
- package/src/outcomes/content_creation_battle.ts +160 -0
- package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
- package/src/outcomes/index.ts +107 -0
- package/src/outcomes/lead_gen_battle.test.ts +113 -0
- package/src/outcomes/lead_gen_battle.ts +99 -0
- package/src/outcomes/outcome.schema.property.test.ts +229 -0
- package/src/outcomes/outcome.schema.ts +187 -0
- package/src/outcomes/qualified_sales_interest.ts +118 -0
- package/src/outcomes/swarm_planner.property.test.ts +370 -0
- package/src/outcomes/swarm_planner.ts +96 -0
- package/src/outcomes/web_extraction.ts +234 -0
- package/src/runtime/README.md +220 -0
- package/src/runtime/agentRunner.test.ts +341 -0
- package/src/runtime/agentRunner.ts +746 -0
- package/src/runtime/claudeAdapter.ts +232 -0
- package/src/runtime/costTracker.ts +123 -0
- package/src/runtime/index.ts +34 -0
- package/src/runtime/modelAdapter.property.test.ts +305 -0
- package/src/runtime/modelAdapter.ts +144 -0
- package/src/runtime/openaiAdapter.ts +235 -0
- package/src/utils/README.md +122 -0
- package/src/utils/command-runner.ts +134 -0
- package/src/utils/cost-guard.ts +379 -0
- package/src/utils/errors.test.ts +290 -0
- package/src/utils/errors.ts +442 -0
- package/src/utils/index.ts +37 -0
- package/src/utils/logger.test.ts +361 -0
- package/src/utils/logger.ts +419 -0
- 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
|
+
}
|