@mobileai/react-native 0.9.27 → 0.9.29
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 +28 -16
- package/android/build.gradle +17 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
- package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
- package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
- package/bin/generate-map.cjs +45 -6
- package/ios/MobileAIFloatingOverlayComponentView.h +8 -0
- package/ios/MobileAIFloatingOverlayComponentView.mm +12 -41
- package/ios/Podfile +63 -0
- package/ios/Podfile.lock +2290 -0
- package/ios/Podfile.properties.json +4 -0
- package/ios/mobileaireactnative/AppDelegate.swift +69 -0
- package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
- package/ios/mobileaireactnative/Info.plist +55 -0
- package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
- package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
- package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
- package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
- package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
- package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
- package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
- package/lib/module/components/AIAgent.js +501 -191
- package/lib/module/components/AgentChatBar.js +250 -59
- package/lib/module/components/FloatingOverlayWrapper.js +68 -32
- package/lib/module/config/endpoints.js +22 -1
- package/lib/module/core/AgentRuntime.js +110 -8
- package/lib/module/core/FiberTreeWalker.js +211 -10
- package/lib/module/core/OutcomeVerifier.js +149 -0
- package/lib/module/core/systemPrompt.js +96 -25
- package/lib/module/providers/GeminiProvider.js +9 -3
- package/lib/module/services/telemetry/TelemetryService.js +21 -2
- package/lib/module/services/telemetry/TouchAutoCapture.js +235 -38
- package/lib/module/services/telemetry/analyticsLabeling.js +187 -0
- package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/lib/module/support/supportPrompt.js +22 -7
- package/lib/module/support/supportStyle.js +55 -0
- package/lib/module/support/types.js +2 -0
- package/lib/module/tools/typeTool.js +20 -0
- package/lib/module/utils/humanizeScreenName.js +49 -0
- package/lib/typescript/src/components/AIAgent.d.ts +6 -2
- package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
- package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
- package/lib/typescript/src/config/endpoints.d.ts +4 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts +12 -3
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
- package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
- package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
- package/lib/typescript/src/core/types.d.ts +63 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
- package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +6 -1
- package/lib/typescript/src/services/telemetry/analyticsLabeling.d.ts +20 -0
- package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
- package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
- package/lib/typescript/src/support/index.d.ts +1 -0
- package/lib/typescript/src/support/supportStyle.d.ts +9 -0
- package/lib/typescript/src/support/types.d.ts +3 -0
- package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
- package/package.json +10 -10
- package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/ios/MobileAIPilotIntents.swift +0 -51
|
@@ -15,10 +15,12 @@ import { logger } from "../utils/logger.js";
|
|
|
15
15
|
import { walkFiberTree } from "./FiberTreeWalker.js";
|
|
16
16
|
import { dehydrateScreen } from "./ScreenDehydrator.js";
|
|
17
17
|
import { buildSystemPrompt, buildKnowledgeOnlyPrompt } from "./systemPrompt.js";
|
|
18
|
+
import { buildVerificationAction, createVerificationSnapshot, OutcomeVerifier } from "./OutcomeVerifier.js";
|
|
18
19
|
import { KnowledgeBaseService } from "../services/KnowledgeBaseService.js";
|
|
19
20
|
import { installAlertInterceptor, uninstallAlertInterceptor } from "./NativeAlertInterceptor.js";
|
|
20
21
|
import { createTapTool, createLongPressTool, createTypeTool, createScrollTool, createSliderTool, createPickerTool, createDatePickerTool, createKeyboardTool, createGuideTool, createSimplifyTool, createRestoreTool } from "../tools/index.js";
|
|
21
22
|
import { actionRegistry } from "./ActionRegistry.js";
|
|
23
|
+
import { createProvider } from "../providers/ProviderFactory.js";
|
|
22
24
|
const DEFAULT_MAX_STEPS = 25;
|
|
23
25
|
function generateTraceId() {
|
|
24
26
|
return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -39,6 +41,10 @@ export class AgentRuntime {
|
|
|
39
41
|
knowledgeService = null;
|
|
40
42
|
lastDehydratedRoot = null;
|
|
41
43
|
currentTraceId = null;
|
|
44
|
+
currentUserGoal = '';
|
|
45
|
+
verifierProvider = null;
|
|
46
|
+
outcomeVerifier = null;
|
|
47
|
+
pendingCriticalVerification = null;
|
|
42
48
|
|
|
43
49
|
// ─── Task-scoped error suppression ──────────────────────────
|
|
44
50
|
// Installed once at execute() start, removed after grace period.
|
|
@@ -147,6 +153,77 @@ export class AgentRuntime {
|
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
}
|
|
156
|
+
getVerifier() {
|
|
157
|
+
if (this.config.verifier?.enabled === false) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (!this.outcomeVerifier) {
|
|
161
|
+
const verifierConfig = this.config.verifier;
|
|
162
|
+
if (verifierConfig?.provider || verifierConfig?.model || verifierConfig?.proxyUrl || verifierConfig?.proxyHeaders) {
|
|
163
|
+
this.verifierProvider = createProvider(verifierConfig.provider || this.config.provider || 'gemini', this.config.apiKey, verifierConfig.model || this.config.model, verifierConfig.proxyUrl || this.config.proxyUrl, verifierConfig.proxyHeaders || this.config.proxyHeaders);
|
|
164
|
+
} else {
|
|
165
|
+
this.verifierProvider = this.provider;
|
|
166
|
+
}
|
|
167
|
+
this.outcomeVerifier = new OutcomeVerifier(this.verifierProvider, this.config);
|
|
168
|
+
}
|
|
169
|
+
return this.outcomeVerifier;
|
|
170
|
+
}
|
|
171
|
+
createCurrentVerificationSnapshot(screenName, screenContent, elements, screenshot) {
|
|
172
|
+
return createVerificationSnapshot(screenName, screenContent, elements, screenshot);
|
|
173
|
+
}
|
|
174
|
+
async updateCriticalVerification(screenName, screenContent, elements, screenshot, stepIndex) {
|
|
175
|
+
if (!this.pendingCriticalVerification) return;
|
|
176
|
+
const verifier = this.getVerifier();
|
|
177
|
+
if (!verifier) {
|
|
178
|
+
this.pendingCriticalVerification = null;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const postAction = this.createCurrentVerificationSnapshot(screenName, screenContent, elements, screenshot);
|
|
182
|
+
this.pendingCriticalVerification.followupSteps += 1;
|
|
183
|
+
const result = await verifier.verify({
|
|
184
|
+
goal: this.pendingCriticalVerification.goal,
|
|
185
|
+
action: this.pendingCriticalVerification.action,
|
|
186
|
+
preAction: this.pendingCriticalVerification.preAction,
|
|
187
|
+
postAction
|
|
188
|
+
});
|
|
189
|
+
this.emitTrace('critical_action_verified', {
|
|
190
|
+
action: this.pendingCriticalVerification.action.toolName,
|
|
191
|
+
label: this.pendingCriticalVerification.action.label,
|
|
192
|
+
status: result.status,
|
|
193
|
+
failureKind: result.failureKind,
|
|
194
|
+
evidence: result.evidence,
|
|
195
|
+
source: result.source,
|
|
196
|
+
followupSteps: this.pendingCriticalVerification.followupSteps
|
|
197
|
+
}, stepIndex);
|
|
198
|
+
if (result.status === 'success') {
|
|
199
|
+
this.pendingCriticalVerification = null;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (result.status === 'error') {
|
|
203
|
+
this.observations.push(`Outcome verifier: The previous action "${this.pendingCriticalVerification.action.label}" did NOT complete successfully. ${result.evidence} Treat this as a ${result.failureKind} failure, do not claim success, and either recover or explain the issue clearly.`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const maxFollowupSteps = verifier.getMaxFollowupSteps();
|
|
207
|
+
const ageNote = this.pendingCriticalVerification.followupSteps >= maxFollowupSteps ? ` This critical action is still unverified after ${this.pendingCriticalVerification.followupSteps} follow-up checks.` : '';
|
|
208
|
+
this.observations.push(`Outcome verifier: The previous action "${this.pendingCriticalVerification.action.label}" is still unverified. ${result.evidence}${ageNote} Before calling done(success=true), keep checking for success or error evidence on the current screen.`);
|
|
209
|
+
}
|
|
210
|
+
maybeStartCriticalVerification(toolName, args, preAction) {
|
|
211
|
+
const verifier = this.getVerifier();
|
|
212
|
+
if (!verifier) return;
|
|
213
|
+
const action = buildVerificationAction(toolName, args, preAction.elements, this.getToolStatusLabel(toolName, args));
|
|
214
|
+
if (!verifier.isCriticalAction(action)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.pendingCriticalVerification = {
|
|
218
|
+
goal: this.currentUserGoal,
|
|
219
|
+
action,
|
|
220
|
+
preAction,
|
|
221
|
+
followupSteps: 0
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
shouldBlockSuccessCompletion() {
|
|
225
|
+
return this.pendingCriticalVerification !== null;
|
|
226
|
+
}
|
|
150
227
|
|
|
151
228
|
// ─── Tool Registration ─────────────────────────────────────
|
|
152
229
|
|
|
@@ -365,14 +442,14 @@ export class AgentRuntime {
|
|
|
365
442
|
// capture_screenshot — on-demand visual capture (for image/video content questions)
|
|
366
443
|
this.tools.set('capture_screenshot', {
|
|
367
444
|
name: 'capture_screenshot',
|
|
368
|
-
description: 'Capture
|
|
445
|
+
description: 'Capture the SDK root component as an image. Use when the user asks about visual content (images, videos, colors, layout appearance) that cannot be determined from the element tree alone.',
|
|
369
446
|
parameters: {},
|
|
370
447
|
execute: async () => {
|
|
371
448
|
const screenshot = await this.captureScreenshot();
|
|
372
449
|
if (screenshot) {
|
|
373
450
|
return `✅ Screenshot captured (${Math.round(screenshot.length / 1024)}KB). Visual content is now available for analysis.`;
|
|
374
451
|
}
|
|
375
|
-
return '❌ Screenshot capture failed. react-native-view-shot
|
|
452
|
+
return '❌ Screenshot capture failed. react-native-view-shot is required and must be installed in your app.';
|
|
376
453
|
}
|
|
377
454
|
});
|
|
378
455
|
|
|
@@ -599,12 +676,12 @@ export class AgentRuntime {
|
|
|
599
676
|
}
|
|
600
677
|
}
|
|
601
678
|
|
|
602
|
-
// ─── Screenshot Capture (
|
|
679
|
+
// ─── Screenshot Capture (react-native-view-shot) ─────
|
|
603
680
|
|
|
604
681
|
/**
|
|
605
|
-
* Captures the
|
|
606
|
-
* Uses react-native-view-shot as
|
|
607
|
-
* Returns null
|
|
682
|
+
* Captures the root component as a base64 JPEG for vision tools.
|
|
683
|
+
* Uses react-native-view-shot as a required peer dependency.
|
|
684
|
+
* Returns null only when capture is temporarily unavailable.
|
|
608
685
|
*/
|
|
609
686
|
async captureScreenshot() {
|
|
610
687
|
try {
|
|
@@ -622,7 +699,7 @@ export class AgentRuntime {
|
|
|
622
699
|
return uri || undefined;
|
|
623
700
|
} catch (error) {
|
|
624
701
|
if (error.message?.includes('Cannot find module') || error.code === 'MODULE_NOT_FOUND' || error.message?.includes('unknown module')) {
|
|
625
|
-
logger.warn('AgentRuntime', 'Screenshot requires react-native-view-shot.
|
|
702
|
+
logger.warn('AgentRuntime', 'Screenshot requires react-native-view-shot. It is a peer dependency; install it in your host app with: npx expo install react-native-view-shot');
|
|
626
703
|
} else {
|
|
627
704
|
logger.debug('AgentRuntime', `Screenshot skipped: ${error.message}`);
|
|
628
705
|
}
|
|
@@ -1328,6 +1405,10 @@ ${screen.elementsText}
|
|
|
1328
1405
|
this.currentTraceId = generateTraceId();
|
|
1329
1406
|
this.observations = [];
|
|
1330
1407
|
this.lastScreenName = '';
|
|
1408
|
+
this.pendingCriticalVerification = null;
|
|
1409
|
+
this.outcomeVerifier = null;
|
|
1410
|
+
this.verifierProvider = null;
|
|
1411
|
+
this.currentUserGoal = userMessage;
|
|
1331
1412
|
// Reset workflow approval for each new task
|
|
1332
1413
|
this.resetAppActionApproval('new task');
|
|
1333
1414
|
const maxSteps = this.config.maxSteps || DEFAULT_MAX_STEPS;
|
|
@@ -1347,6 +1428,7 @@ ${screen.elementsText}
|
|
|
1347
1428
|
contextualMessage = `(Note: You just asked the user: "${this.lastAskUserQuestion}")\n\nUser replied: ${userMessage}`;
|
|
1348
1429
|
this.lastAskUserQuestion = null; // Consume the question
|
|
1349
1430
|
}
|
|
1431
|
+
this.currentUserGoal = contextualMessage;
|
|
1350
1432
|
logger.info('AgentRuntime', `Starting execution: "${contextualMessage}"`);
|
|
1351
1433
|
|
|
1352
1434
|
// Lifecycle: onBeforeTask
|
|
@@ -1491,12 +1573,13 @@ ${screen.elementsText}
|
|
|
1491
1573
|
|
|
1492
1574
|
// 4.5. Capture screenshot for Gemini vision (optional)
|
|
1493
1575
|
const screenshot = await this.captureScreenshot();
|
|
1576
|
+
await this.updateCriticalVerification(screenName, screenContent, screen.elements, screenshot, step);
|
|
1494
1577
|
|
|
1495
1578
|
// 5. Send to AI provider
|
|
1496
1579
|
this.config.onStatusUpdate?.('Thinking...');
|
|
1497
1580
|
const hasKnowledge = !!this.knowledgeService;
|
|
1498
1581
|
const isCopilot = this.config.interactionMode !== 'autopilot';
|
|
1499
|
-
const systemPrompt = buildSystemPrompt('en', hasKnowledge, isCopilot);
|
|
1582
|
+
const systemPrompt = buildSystemPrompt('en', hasKnowledge, isCopilot, this.config.supportStyle);
|
|
1500
1583
|
const tools = this.buildToolsForProvider();
|
|
1501
1584
|
logger.info('AgentRuntime', `Sending to AI with ${tools.length} tools...`);
|
|
1502
1585
|
logger.debug('AgentRuntime', 'System prompt length:', systemPrompt.length);
|
|
@@ -1561,6 +1644,13 @@ ${screen.elementsText}
|
|
|
1561
1644
|
|
|
1562
1645
|
// 6. Process tool calls
|
|
1563
1646
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
1647
|
+
if (this.shouldBlockSuccessCompletion()) {
|
|
1648
|
+
this.emitTrace('task_completion_blocked_needs_verification', {
|
|
1649
|
+
responseText: response.text,
|
|
1650
|
+
pendingVerification: this.pendingCriticalVerification
|
|
1651
|
+
}, step);
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1564
1654
|
logger.warn('AgentRuntime', 'No tool calls in response. Text:', response.text);
|
|
1565
1655
|
this.emitTrace('task_completed_without_tool', {
|
|
1566
1656
|
responseText: response.text
|
|
@@ -1605,6 +1695,7 @@ ${screen.elementsText}
|
|
|
1605
1695
|
// Prefer the human-readable plan over the raw tool status if available to avoid double statuses
|
|
1606
1696
|
const statusDisplay = reasoning.plan || statusLabel;
|
|
1607
1697
|
this.config.onStatusUpdate?.(statusDisplay);
|
|
1698
|
+
const preActionSnapshot = this.createCurrentVerificationSnapshot(screenName, screenContent, screen.elements, screenshot);
|
|
1608
1699
|
|
|
1609
1700
|
// Find and execute the tool
|
|
1610
1701
|
const tool = this.tools.get(toolCall.name) || this.buildToolsForProvider().find(t => t.name === toolCall.name);
|
|
@@ -1624,6 +1715,11 @@ ${screen.elementsText}
|
|
|
1624
1715
|
args: toolCall.args,
|
|
1625
1716
|
output
|
|
1626
1717
|
}, step);
|
|
1718
|
+
if (output.startsWith('✅')) {
|
|
1719
|
+
this.maybeStartCriticalVerification(toolCall.name, toolCall.args, preActionSnapshot);
|
|
1720
|
+
} else if (toolCall.name !== 'done') {
|
|
1721
|
+
this.pendingCriticalVerification = null;
|
|
1722
|
+
}
|
|
1627
1723
|
if (output === APPROVAL_ALREADY_DONE_TOKEN) {
|
|
1628
1724
|
const result = {
|
|
1629
1725
|
success: true,
|
|
@@ -1652,6 +1748,12 @@ ${screen.elementsText}
|
|
|
1652
1748
|
|
|
1653
1749
|
// Check if done
|
|
1654
1750
|
if (toolCall.name === 'done') {
|
|
1751
|
+
if (toolCall.args.success !== false && this.shouldBlockSuccessCompletion()) {
|
|
1752
|
+
this.emitTrace('done_blocked_needs_verification', {
|
|
1753
|
+
pendingVerification: this.pendingCriticalVerification
|
|
1754
|
+
}, step);
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1655
1757
|
const result = {
|
|
1656
1758
|
success: toolCall.args.success !== false,
|
|
1657
1759
|
message: toolCall.args.text || toolCall.args.message || output || reasoning.plan || (toolCall.args.success === false ? 'Action stopped.' : 'Action completed.'),
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
*
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { Dimensions } from 'react-native';
|
|
12
13
|
import { logger } from "../utils/logger.js";
|
|
13
14
|
import { getChild, getSibling, getParent, getProps, getStateNode, getType, getDisplayName } from "./FiberAdapter.js";
|
|
14
15
|
import { getActiveAlert } from "./NativeAlertInterceptor.js";
|
|
16
|
+
import { chooseBestAnalyticsTarget, getAnalyticsElementKind } from "../services/telemetry/analyticsLabeling.js";
|
|
15
17
|
|
|
16
18
|
// ─── Walk Configuration ─────────
|
|
17
19
|
|
|
@@ -129,6 +131,69 @@ function getComponentName(fiber) {
|
|
|
129
131
|
if (type.render?.name) return type.render.name;
|
|
130
132
|
return null;
|
|
131
133
|
}
|
|
134
|
+
function getCustomAncestorPath(fiber, maxDepth = 6) {
|
|
135
|
+
const path = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
let current = getParent(fiber);
|
|
138
|
+
let depth = 0;
|
|
139
|
+
while (current && depth < maxDepth) {
|
|
140
|
+
const name = getComponentName(current);
|
|
141
|
+
const props = getProps(current);
|
|
142
|
+
const candidate = name === 'AIZone' && typeof props.id === 'string' && props.id.trim() ? props.id.trim() : name;
|
|
143
|
+
if (candidate && !RN_INTERNAL_NAMES.has(candidate) && !PRESSABLE_TYPES.has(candidate) && !seen.has(candidate)) {
|
|
144
|
+
path.push(candidate);
|
|
145
|
+
seen.add(candidate);
|
|
146
|
+
}
|
|
147
|
+
current = getParent(current);
|
|
148
|
+
depth++;
|
|
149
|
+
}
|
|
150
|
+
return path;
|
|
151
|
+
}
|
|
152
|
+
function getAnalyticsTargetForNode(fiber, resolvedType) {
|
|
153
|
+
const props = getProps(fiber);
|
|
154
|
+
const siblingTextLabel = resolvedType && EXTERNALLY_LABELED_TYPES.has(resolvedType) ? extractSiblingTextLabel(fiber) : null;
|
|
155
|
+
return chooseBestAnalyticsTarget([{
|
|
156
|
+
text: props.accessibilityLabel,
|
|
157
|
+
source: 'accessibility'
|
|
158
|
+
}, {
|
|
159
|
+
text: extractDeepTextContent(fiber),
|
|
160
|
+
source: 'deep-text'
|
|
161
|
+
}, {
|
|
162
|
+
text: siblingTextLabel,
|
|
163
|
+
source: 'sibling-text'
|
|
164
|
+
}, {
|
|
165
|
+
text: props.title,
|
|
166
|
+
source: 'title'
|
|
167
|
+
}, {
|
|
168
|
+
text: resolvedType === 'text-input' ? props.placeholder : null,
|
|
169
|
+
source: 'placeholder'
|
|
170
|
+
}, {
|
|
171
|
+
text: props.testID || props.nativeID,
|
|
172
|
+
source: 'test-id'
|
|
173
|
+
}], getAnalyticsElementKind(resolvedType));
|
|
174
|
+
}
|
|
175
|
+
function getSiblingAnalyticsLabels(fiber, maxLabels = 6) {
|
|
176
|
+
const parent = getParent(fiber);
|
|
177
|
+
if (!parent) return [];
|
|
178
|
+
const labels = [];
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
let sibling = getChild(parent);
|
|
181
|
+
while (sibling) {
|
|
182
|
+
if (sibling !== fiber) {
|
|
183
|
+
const siblingType = getElementType(sibling);
|
|
184
|
+
if (siblingType && !isDisabled(sibling)) {
|
|
185
|
+
const label = getAnalyticsTargetForNode(sibling, siblingType).label;
|
|
186
|
+
if (label && !seen.has(label.toLowerCase())) {
|
|
187
|
+
labels.push(label);
|
|
188
|
+
seen.add(label.toLowerCase());
|
|
189
|
+
if (labels.length >= maxLabels) break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
sibling = getSibling(sibling);
|
|
194
|
+
}
|
|
195
|
+
return labels;
|
|
196
|
+
}
|
|
132
197
|
function hasSliderLikeSemantics(props) {
|
|
133
198
|
if (!props || typeof props !== 'object') return false;
|
|
134
199
|
if (typeof props.onSlidingComplete === 'function') return true;
|
|
@@ -420,7 +485,7 @@ function isDisabled(fiber) {
|
|
|
420
485
|
* Recursively extract ALL text content from a fiber's children.
|
|
421
486
|
* Pierces through nested interactive elements — unlike typical tree walkers
|
|
422
487
|
* that stop at inner Pressable/TouchableOpacity boundaries.
|
|
423
|
-
*
|
|
488
|
+
*
|
|
424
489
|
* This is critical for wrapper components (e.g. ZButton → internal
|
|
425
490
|
* TouchableOpacity → Text) where stopping at nested interactives
|
|
426
491
|
* would lose the text label entirely.
|
|
@@ -463,7 +528,7 @@ function isIconGlyph(text) {
|
|
|
463
528
|
const trimmed = text.trim();
|
|
464
529
|
if (trimmed.length === 0 || trimmed.length > 2) return false; // Glyphs are 1-2 chars (surrogate pairs)
|
|
465
530
|
const code = trimmed.codePointAt(0) || 0;
|
|
466
|
-
return code >=
|
|
531
|
+
return code >= 0xe000 && code <= 0xf8ff || code >= 0xf0000 && code <= 0xfffff || code >= 0x100000 && code <= 0x10ffff;
|
|
467
532
|
}
|
|
468
533
|
function normalizeRuntimeLabel(text) {
|
|
469
534
|
if (!text) return '';
|
|
@@ -540,11 +605,11 @@ function extractRawText(children) {
|
|
|
540
605
|
/**
|
|
541
606
|
* Recursively search a fiber subtree for icon/symbol components and
|
|
542
607
|
* return their `name` prop as a semantic label.
|
|
543
|
-
*
|
|
608
|
+
*
|
|
544
609
|
* Works generically: any non-RN-internal child component with a string
|
|
545
610
|
* `name` prop is treated as an icon (covers Ionicons, MaterialIcons,
|
|
546
611
|
* FontAwesome, custom SVG wrappers, etc. — no hardcoded list needed).
|
|
547
|
-
*
|
|
612
|
+
*
|
|
548
613
|
* e.g. a TouchableOpacity wrapping <Ionicons name="add-circle" /> → "icon:add-circle"
|
|
549
614
|
*/
|
|
550
615
|
function extractIconName(fiber, maxDepth = 5) {
|
|
@@ -869,6 +934,28 @@ export function walkFiberTree(rootRef, config) {
|
|
|
869
934
|
text: parentContext && !new Set(['ScrollViewContext', 'VirtualizedListContext', 'ViewabilityHelper', 'ScrollResponder', 'AnimatedComponent', 'TouchableOpacity']).has(parentContext) ? parentContext : null,
|
|
870
935
|
source: 'context'
|
|
871
936
|
}]);
|
|
937
|
+
const analyticsTarget = chooseBestAnalyticsTarget([{
|
|
938
|
+
text: derivedProps.accessibilityLabel,
|
|
939
|
+
source: 'accessibility'
|
|
940
|
+
}, {
|
|
941
|
+
text: extractDeepTextContent(node),
|
|
942
|
+
source: 'deep-text'
|
|
943
|
+
}, {
|
|
944
|
+
text: siblingTextLabel,
|
|
945
|
+
source: 'sibling-text'
|
|
946
|
+
}, {
|
|
947
|
+
text: derivedProps.title,
|
|
948
|
+
source: 'title'
|
|
949
|
+
}, {
|
|
950
|
+
text: resolvedType === 'text-input' ? derivedProps.placeholder : null,
|
|
951
|
+
source: 'placeholder'
|
|
952
|
+
}, {
|
|
953
|
+
text: derivedProps.testID || derivedProps.nativeID,
|
|
954
|
+
source: 'test-id'
|
|
955
|
+
}], getAnalyticsElementKind(resolvedType));
|
|
956
|
+
const ancestorPath = getCustomAncestorPath(node);
|
|
957
|
+
const siblingLabels = getSiblingAnalyticsLabels(node);
|
|
958
|
+
const componentName = getComponentName(node);
|
|
872
959
|
interactives.push({
|
|
873
960
|
index: currentIndex,
|
|
874
961
|
type: resolvedType,
|
|
@@ -877,7 +964,14 @@ export function walkFiberTree(rootRef, config) {
|
|
|
877
964
|
zoneId: currentZoneId,
|
|
878
965
|
fiberNode: node,
|
|
879
966
|
props: derivedProps,
|
|
880
|
-
requiresConfirmation: derivedProps.aiConfirm === true
|
|
967
|
+
requiresConfirmation: derivedProps.aiConfirm === true,
|
|
968
|
+
analyticsLabel: analyticsTarget.label,
|
|
969
|
+
analyticsElementKind: analyticsTarget.elementKind,
|
|
970
|
+
analyticsLabelConfidence: analyticsTarget.labelConfidence,
|
|
971
|
+
analyticsZoneId: currentZoneId,
|
|
972
|
+
analyticsAncestorPath: ancestorPath,
|
|
973
|
+
analyticsSiblingLabels: siblingLabels,
|
|
974
|
+
analyticsComponentName: componentName
|
|
881
975
|
});
|
|
882
976
|
|
|
883
977
|
// Build output tag with state attributes
|
|
@@ -1079,7 +1173,7 @@ export function findScrollableContainers(rootRef, screenName) {
|
|
|
1079
1173
|
const contextLabel = getNearestCustomComponentName(node) || name || 'Unknown';
|
|
1080
1174
|
|
|
1081
1175
|
// For scrollable containers, we need the native scroll ref.
|
|
1082
|
-
// FlatList Fiber stateNode may be the component instance —
|
|
1176
|
+
// FlatList Fiber stateNode may be the component instance —
|
|
1083
1177
|
// we need to find the underlying native ScrollView.
|
|
1084
1178
|
let scrollRef = isPagerLike ? getStateNode(node) : resolveNativeScrollRef(node);
|
|
1085
1179
|
if (scrollRef) {
|
|
@@ -1108,13 +1202,13 @@ export function findScrollableContainers(rootRef, screenName) {
|
|
|
1108
1202
|
|
|
1109
1203
|
/**
|
|
1110
1204
|
* Resolve the native scroll view reference from a Fiber node.
|
|
1111
|
-
*
|
|
1205
|
+
*
|
|
1112
1206
|
* Handles multiple React Native internals:
|
|
1113
1207
|
* - RCTScrollView: stateNode IS the native scroll view
|
|
1114
1208
|
* - FlatList/VirtualizedList: stateNode is a component instance,
|
|
1115
1209
|
* need to find the inner ScrollView via getNativeScrollRef() or
|
|
1116
1210
|
* by walking down the Fiber tree to find the RCTScrollView child
|
|
1117
|
-
*/
|
|
1211
|
+
*/
|
|
1118
1212
|
function resolveNativeScrollRef(fiberNode) {
|
|
1119
1213
|
const stateNode = getStateNode(fiberNode);
|
|
1120
1214
|
|
|
@@ -1128,7 +1222,9 @@ function resolveNativeScrollRef(fiberNode) {
|
|
|
1128
1222
|
try {
|
|
1129
1223
|
const ref = stateNode.getNativeScrollRef();
|
|
1130
1224
|
if (ref && typeof ref.scrollTo === 'function') return ref;
|
|
1131
|
-
} catch {
|
|
1225
|
+
} catch {
|
|
1226
|
+
/* fall through */
|
|
1227
|
+
}
|
|
1132
1228
|
}
|
|
1133
1229
|
|
|
1134
1230
|
// Case 3: stateNode has getScrollRef (another VirtualizedList pattern)
|
|
@@ -1141,7 +1237,9 @@ function resolveNativeScrollRef(fiberNode) {
|
|
|
1141
1237
|
const nativeRef = ref.getNativeScrollRef();
|
|
1142
1238
|
if (nativeRef && typeof nativeRef.scrollTo === 'function') return nativeRef;
|
|
1143
1239
|
}
|
|
1144
|
-
} catch {
|
|
1240
|
+
} catch {
|
|
1241
|
+
/* fall through */
|
|
1242
|
+
}
|
|
1145
1243
|
}
|
|
1146
1244
|
|
|
1147
1245
|
// Case 4: stateNode has scrollToOffset directly (VirtualizedList instance)
|
|
@@ -1171,4 +1269,107 @@ function resolveNativeScrollRef(fiberNode) {
|
|
|
1171
1269
|
logger.debug('FiberTreeWalker', 'Could not resolve native scroll ref — returning stateNode as fallback');
|
|
1172
1270
|
return stateNode;
|
|
1173
1271
|
}
|
|
1272
|
+
|
|
1273
|
+
// ─── Wireframe Capture ─────────────────────────────────────────
|
|
1274
|
+
|
|
1275
|
+
/** Max elements to measure — keeps bridge work bounded */
|
|
1276
|
+
const WIREFRAME_MAX_ELEMENTS = 50;
|
|
1277
|
+
/** Measure this many elements per frame, then yield */
|
|
1278
|
+
const WIREFRAME_BATCH_SIZE = 10;
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Measure a single element on the native bridge.
|
|
1282
|
+
* Returns null if the element is off-screen or unmeasurable.
|
|
1283
|
+
*/
|
|
1284
|
+
function measureElement(el) {
|
|
1285
|
+
return new Promise(resolve => {
|
|
1286
|
+
try {
|
|
1287
|
+
const stateNode = getStateNode(el.fiberNode);
|
|
1288
|
+
if (!stateNode || typeof stateNode.measure !== 'function') {
|
|
1289
|
+
resolve(null);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
stateNode.measure((_x, _y, width, height, pageX, pageY) => {
|
|
1293
|
+
if (width > 0 && height > 0) {
|
|
1294
|
+
resolve({
|
|
1295
|
+
type: el.type,
|
|
1296
|
+
label: el.analyticsLabel || '',
|
|
1297
|
+
elementKind: el.analyticsElementKind,
|
|
1298
|
+
labelConfidence: el.analyticsLabelConfidence,
|
|
1299
|
+
zoneId: el.analyticsZoneId,
|
|
1300
|
+
ancestorPath: el.analyticsAncestorPath,
|
|
1301
|
+
siblingLabels: el.analyticsSiblingLabels,
|
|
1302
|
+
componentName: el.analyticsComponentName,
|
|
1303
|
+
x: pageX,
|
|
1304
|
+
y: pageY,
|
|
1305
|
+
width,
|
|
1306
|
+
height
|
|
1307
|
+
});
|
|
1308
|
+
} else {
|
|
1309
|
+
resolve(null);
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
} catch {
|
|
1313
|
+
resolve(null);
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Yield one frame so measure work doesn't block gestures/animations.
|
|
1320
|
+
* Uses requestAnimationFrame where available, falls back to setTimeout(16ms).
|
|
1321
|
+
*/
|
|
1322
|
+
function yieldFrame() {
|
|
1323
|
+
return new Promise(resolve => {
|
|
1324
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
1325
|
+
requestAnimationFrame(() => resolve());
|
|
1326
|
+
} else {
|
|
1327
|
+
setTimeout(resolve, 16);
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Capture a privacy-safe wireframe of the current screen.
|
|
1334
|
+
*
|
|
1335
|
+
* Performance guarantees:
|
|
1336
|
+
* - Capped at WIREFRAME_MAX_ELEMENTS (50) — enough for wireframe context
|
|
1337
|
+
* - Measures in batches of WIREFRAME_BATCH_SIZE (10), yielding a frame
|
|
1338
|
+
* between batches so the bridge stays free for user interactions
|
|
1339
|
+
* - The caller (AIAgent) defers this via InteractionManager so it
|
|
1340
|
+
* never competes with screen transitions or gestures
|
|
1341
|
+
*/
|
|
1342
|
+
export async function captureWireframe(rootRef, config = {}) {
|
|
1343
|
+
const result = walkFiberTree(rootRef, config);
|
|
1344
|
+
const elements = result.interactives;
|
|
1345
|
+
if (elements.length === 0) return null;
|
|
1346
|
+
|
|
1347
|
+
// Cap the number of elements to keep bridge work bounded
|
|
1348
|
+
const capped = elements.slice(0, WIREFRAME_MAX_ELEMENTS);
|
|
1349
|
+
const components = [];
|
|
1350
|
+
for (let i = 0; i < capped.length; i += WIREFRAME_BATCH_SIZE) {
|
|
1351
|
+
const batch = capped.slice(i, i + WIREFRAME_BATCH_SIZE);
|
|
1352
|
+
const batchResults = await Promise.all(batch.map(measureElement));
|
|
1353
|
+
for (const r of batchResults) {
|
|
1354
|
+
if (r) components.push(r);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Yield between batches — never monopolize the bridge
|
|
1358
|
+
if (i + WIREFRAME_BATCH_SIZE < capped.length) {
|
|
1359
|
+
await yieldFrame();
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if (components.length === 0) return null;
|
|
1363
|
+
const {
|
|
1364
|
+
width: deviceWidth,
|
|
1365
|
+
height: deviceHeight
|
|
1366
|
+
} = Dimensions.get('window');
|
|
1367
|
+
return {
|
|
1368
|
+
screen: config.screenName || 'Unknown',
|
|
1369
|
+
components,
|
|
1370
|
+
deviceWidth,
|
|
1371
|
+
deviceHeight,
|
|
1372
|
+
capturedAt: new Date().toISOString()
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1174
1375
|
//# sourceMappingURL=FiberTreeWalker.js.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const COMMIT_ACTION_PATTERN = /\b(save|submit|confirm|apply|pay|place|update|continue|finish|send|checkout|complete|verify|review|publish|post|delete|cancel)\b/i;
|
|
4
|
+
const SUCCESS_SIGNAL_PATTERNS = [/\b(success|successful|saved|updated|submitted|completed|done|confirmed|applied|verified)\b/i, /\bthank you\b/i, /\border confirmed\b/i, /\bchanges saved\b/i];
|
|
5
|
+
const ERROR_SIGNAL_PATTERNS = [/\berror\b/i, /\bfailed\b/i, /\binvalid\b/i, /\brequired\b/i, /\bincorrect\b/i, /\btry again\b/i, /\bcould not\b/i, /\bunable to\b/i, /\bverification\b.{0,30}\b(error|failed|invalid|required)\b/i, /\bcode\b.{0,30}\b(error|failed|invalid|required)\b/i];
|
|
6
|
+
const UNCONTROLLABLE_ERROR_PATTERNS = [/\bnetwork\b/i, /\bserver\b/i, /\bservice unavailable\b/i, /\btemporarily unavailable\b/i, /\btimeout\b/i, /\btry later\b/i, /\bconnection\b/i];
|
|
7
|
+
function normalizeText(text) {
|
|
8
|
+
return text.replace(/\[[^\]]+\]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
9
|
+
}
|
|
10
|
+
function elementStillPresent(elements, target) {
|
|
11
|
+
if (!target) return false;
|
|
12
|
+
return elements.some(element => element.index === target.index || element.type === target.type && element.label.trim().length > 0 && element.label.trim() === target.label.trim());
|
|
13
|
+
}
|
|
14
|
+
export function createVerificationSnapshot(screenName, screenContent, elements, screenshot) {
|
|
15
|
+
return {
|
|
16
|
+
screenName,
|
|
17
|
+
screenContent,
|
|
18
|
+
elements,
|
|
19
|
+
screenshot
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function buildVerificationAction(toolName, args, elements, fallbackLabel) {
|
|
23
|
+
const targetElement = typeof args.index === 'number' ? elements.find(element => element.index === args.index) : undefined;
|
|
24
|
+
return {
|
|
25
|
+
toolName,
|
|
26
|
+
args,
|
|
27
|
+
label: targetElement?.label || fallbackLabel,
|
|
28
|
+
targetElement
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function isCriticalVerificationAction(action) {
|
|
32
|
+
if (action.targetElement?.requiresConfirmation) return true;
|
|
33
|
+
if (!['tap', 'long_press', 'adjust_slider', 'select_picker', 'set_date'].includes(action.toolName)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const label = action.label || '';
|
|
37
|
+
return COMMIT_ACTION_PATTERN.test(label);
|
|
38
|
+
}
|
|
39
|
+
function deterministicVerify(context) {
|
|
40
|
+
const normalizedPost = normalizeText(context.postAction.screenContent);
|
|
41
|
+
if (ERROR_SIGNAL_PATTERNS.some(pattern => pattern.test(normalizedPost))) {
|
|
42
|
+
const failureKind = UNCONTROLLABLE_ERROR_PATTERNS.some(pattern => pattern.test(normalizedPost)) ? 'uncontrollable' : 'controllable';
|
|
43
|
+
return {
|
|
44
|
+
status: 'error',
|
|
45
|
+
failureKind,
|
|
46
|
+
evidence: 'Visible validation or error feedback appeared after the action.',
|
|
47
|
+
source: 'deterministic'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (context.postAction.screenName !== context.preAction.screenName) {
|
|
51
|
+
return {
|
|
52
|
+
status: 'success',
|
|
53
|
+
failureKind: 'controllable',
|
|
54
|
+
evidence: `The app navigated from "${context.preAction.screenName}" to "${context.postAction.screenName}".`,
|
|
55
|
+
source: 'deterministic'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (SUCCESS_SIGNAL_PATTERNS.some(pattern => pattern.test(normalizedPost))) {
|
|
59
|
+
return {
|
|
60
|
+
status: 'success',
|
|
61
|
+
failureKind: 'controllable',
|
|
62
|
+
evidence: 'The current screen shows explicit success or completion language.',
|
|
63
|
+
source: 'deterministic'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (context.action.targetElement && elementStillPresent(context.preAction.elements, context.action.targetElement) && !elementStillPresent(context.postAction.elements, context.action.targetElement)) {
|
|
67
|
+
return {
|
|
68
|
+
status: 'success',
|
|
69
|
+
failureKind: 'controllable',
|
|
70
|
+
evidence: 'The commit control is no longer present on the current screen.',
|
|
71
|
+
source: 'deterministic'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
status: 'uncertain',
|
|
76
|
+
failureKind: 'controllable',
|
|
77
|
+
evidence: 'The current UI does not yet prove either success or failure.',
|
|
78
|
+
source: 'deterministic'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function llmVerify(provider, context) {
|
|
82
|
+
const verificationTool = {
|
|
83
|
+
name: 'report_verification',
|
|
84
|
+
description: 'Report whether the action succeeded, failed, or remains uncertain based only on the UI evidence.',
|
|
85
|
+
parameters: {
|
|
86
|
+
status: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'success, error, or uncertain',
|
|
89
|
+
required: true,
|
|
90
|
+
enum: ['success', 'error', 'uncertain']
|
|
91
|
+
},
|
|
92
|
+
failureKind: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'controllable or uncontrollable',
|
|
95
|
+
required: true,
|
|
96
|
+
enum: ['controllable', 'uncontrollable']
|
|
97
|
+
},
|
|
98
|
+
evidence: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Brief explanation grounded in the current UI evidence',
|
|
101
|
+
required: true
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
execute: async () => 'reported'
|
|
105
|
+
};
|
|
106
|
+
const systemPrompt = ['You are an outcome verifier for a mobile app agent.', 'Your job is to decide whether the last critical UI action actually succeeded.', 'The current UI is the source of truth. Ignore the actor model’s prior claims when they conflict with the UI.', 'Return success only when the current UI clearly proves completion.', 'Return error when the UI shows validation, verification, submission, or other failure feedback.', 'Return uncertain when the UI does not yet prove either success or error.'].join(' ');
|
|
107
|
+
const userPrompt = [`<goal>${context.goal}</goal>`, `<action tool="${context.action.toolName}" label="${context.action.label}">${JSON.stringify(context.action.args)}</action>`, `<pre_action screen="${context.preAction.screenName}">\n${context.preAction.screenContent}\n</pre_action>`, `<post_action screen="${context.postAction.screenName}">\n${context.postAction.screenContent}\n</post_action>`].join('\n\n');
|
|
108
|
+
const response = await provider.generateContent(systemPrompt, userPrompt, [verificationTool], [], context.postAction.screenshot);
|
|
109
|
+
const toolCall = response.toolCalls?.[0];
|
|
110
|
+
if (!toolCall || toolCall.name !== 'report_verification') {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const status = toolCall.args.status;
|
|
114
|
+
const failureKind = toolCall.args.failureKind;
|
|
115
|
+
const evidence = typeof toolCall.args.evidence === 'string' ? toolCall.args.evidence : '';
|
|
116
|
+
if (!status || !failureKind || !evidence) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
status,
|
|
121
|
+
failureKind,
|
|
122
|
+
evidence,
|
|
123
|
+
source: 'llm'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export class OutcomeVerifier {
|
|
127
|
+
constructor(provider, config) {
|
|
128
|
+
this.provider = provider;
|
|
129
|
+
this.config = config;
|
|
130
|
+
}
|
|
131
|
+
isEnabled() {
|
|
132
|
+
return this.config.verifier?.enabled !== false;
|
|
133
|
+
}
|
|
134
|
+
getMaxFollowupSteps() {
|
|
135
|
+
return this.config.verifier?.maxFollowupSteps ?? 2;
|
|
136
|
+
}
|
|
137
|
+
isCriticalAction(action) {
|
|
138
|
+
return isCriticalVerificationAction(action);
|
|
139
|
+
}
|
|
140
|
+
async verify(context) {
|
|
141
|
+
const stageA = deterministicVerify(context);
|
|
142
|
+
if (stageA.status !== 'uncertain') {
|
|
143
|
+
return stageA;
|
|
144
|
+
}
|
|
145
|
+
const stageB = await llmVerify(this.provider, context);
|
|
146
|
+
return stageB ?? stageA;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=OutcomeVerifier.js.map
|