@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.2
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/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +306 -53
- package/bin/ai-app-bridge.js +51 -4504
- package/bin/android-permissions.js +152 -0
- package/bin/android-uia-xml.js +74 -0
- package/bin/artifact-paths.js +127 -1
- package/bin/bridge-forward.js +56 -0
- package/bin/command-discovery.js +86 -0
- package/bin/command-errors.js +60 -0
- package/bin/command-registry.js +504 -0
- package/bin/command-request.js +14 -0
- package/bin/command-router.js +80 -0
- package/bin/connection-cache.js +119 -0
- package/bin/device-provider.js +2934 -0
- package/bin/execution-host.js +596 -0
- package/bin/execution-runtime.js +101 -0
- package/bin/fact-codec.js +321 -0
- package/bin/fact-recorder.js +691 -0
- package/bin/fact-store.js +226 -0
- package/bin/feedback-probe.js +285 -0
- package/bin/intent/install-intent.js +267 -0
- package/bin/intent/intent-action-executor.js +129 -0
- package/bin/intent/intent-autonomous-adapter.js +46 -0
- package/bin/intent/intent-capture-port.js +64 -0
- package/bin/intent/intent-entry.js +226 -0
- package/bin/intent/intent-errors.js +28 -0
- package/bin/intent/intent-evidence-store.js +121 -0
- package/bin/intent/intent-lifetime.js +62 -0
- package/bin/intent/intent-observation-target.js +33 -0
- package/bin/intent/intent-observer.js +194 -0
- package/bin/intent/intent-production-adapter.js +334 -0
- package/bin/intent/intent-provider.js +27 -0
- package/bin/intent/intent-runtime.js +63 -0
- package/bin/intent/intent-worker.js +432 -0
- package/bin/intent/ios-intent-adapter.js +90 -0
- package/bin/intent/permission-intent.js +239 -0
- package/bin/intent/web-intent-adapter.js +31 -0
- package/bin/ios-device-outcome.js +47 -0
- package/bin/ios-execution.js +108 -0
- package/bin/ios-provider.js +497 -583
- package/bin/ios-runtime-binding.js +54 -0
- package/bin/ios-wda-execution.js +70 -0
- package/bin/ios-wda-port.js +98 -0
- package/bin/ios-wda-project.js +79 -0
- package/bin/mcp-server.js +83 -1040
- package/bin/mmap-scan-index.js +329 -0
- package/bin/observation-collector.js +862 -0
- package/bin/runtime-client.js +154 -0
- package/bin/runtime-directory.js +107 -0
- package/bin/runtime-protocol.js +36 -0
- package/bin/script/bounded-script-registry.js +103 -0
- package/bin/script/node-runtime-adapter.js +233 -0
- package/bin/script/progress-projector.js +63 -0
- package/bin/script/python-runtime-adapter.js +111 -0
- package/bin/script/rolling-summary.js +134 -0
- package/bin/script/script-agent-port.js +40 -0
- package/bin/script/script-assert.js +95 -0
- package/bin/script/script-capture-port.js +76 -0
- package/bin/script/script-catalog.js +85 -0
- package/bin/script/script-durable-restore.js +195 -0
- package/bin/script/script-entry-code.js +26 -0
- package/bin/script/script-entry-route.js +38 -0
- package/bin/script/script-entry.js +3 -0
- package/bin/script/script-errors.js +29 -0
- package/bin/script/script-evidence-store.js +22 -0
- package/bin/script/script-format-removed.js +26 -0
- package/bin/script/script-host-port.js +397 -0
- package/bin/script/script-ledger.js +64 -0
- package/bin/script/script-result.js +57 -0
- package/bin/script/script-sdk.js +152 -0
- package/bin/script/script-sdk.py +153 -0
- package/bin/script/script-session-channel.js +127 -0
- package/bin/script/script-spec.js +86 -0
- package/bin/script/script-supervisor.js +919 -0
- package/bin/script/templates/checkpoint-reentry.js +13 -0
- package/bin/segment-index.js +481 -0
- package/bin/segmented-fact-store.js +1571 -0
- package/bin/shared-kernel/android-h5-target.js +10 -0
- package/bin/shared-kernel/android-install-execution.js +176 -0
- package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
- package/bin/shared-kernel/android-shell-execution.js +195 -0
- package/bin/shared-kernel/argument-schema.js +117 -0
- package/bin/shared-kernel/canonical-path.js +17 -0
- package/bin/shared-kernel/device-acknowledgements.js +53 -0
- package/bin/shared-kernel/device-completion-history.js +52 -0
- package/bin/shared-kernel/device-mutation-lease.js +219 -0
- package/bin/shared-kernel/device-ownership-recovery.js +95 -0
- package/bin/shared-kernel/device-ownership-store.js +94 -0
- package/bin/shared-kernel/evidence-adapters.js +251 -0
- package/bin/shared-kernel/evidence-archive.js +329 -0
- package/bin/shared-kernel/evidence-recording.js +131 -0
- package/bin/shared-kernel/evidence-schema.js +194 -0
- package/bin/shared-kernel/evidence-store.js +190 -0
- package/bin/shared-kernel/execution-admission.js +22 -0
- package/bin/shared-kernel/execution-contracts.js +171 -0
- package/bin/shared-kernel/execution-io.js +106 -0
- package/bin/shared-kernel/execution-ledger.js +125 -0
- package/bin/shared-kernel/execution-scope.js +87 -0
- package/bin/shared-kernel/execution-target.js +115 -0
- package/bin/shared-kernel/flutter-execution.js +13 -0
- package/bin/shared-kernel/flutter-h5-port.js +60 -0
- package/bin/shared-kernel/flutter-h5-target.js +9 -0
- package/bin/shared-kernel/flutter-target.js +75 -0
- package/bin/shared-kernel/h5-execution.js +11 -0
- package/bin/shared-kernel/h5-target.js +31 -0
- package/bin/shared-kernel/host-fact-store.js +49 -0
- package/bin/shared-kernel/ios-h5-target.js +9 -0
- package/bin/shared-kernel/ios-native-target.js +71 -0
- package/bin/shared-kernel/live-capture-query.js +115 -0
- package/bin/shared-kernel/managed-sdk-execution.js +78 -0
- package/bin/shared-kernel/native-execution.js +13 -0
- package/bin/shared-kernel/native-target.js +156 -0
- package/bin/shared-kernel/provider-command-contracts.js +55 -0
- package/bin/shared-kernel/recorded-payload-archive.js +195 -0
- package/bin/shared-kernel/request-context.js +35 -0
- package/bin/shared-kernel/semantic-node.js +55 -0
- package/bin/shared-kernel/summary-transformer.js +352 -0
- package/bin/shared-kernel/target-lease-protocol.js +47 -0
- package/bin/shared-kernel/text-wait.js +111 -0
- package/bin/shared-kernel/uia-execution.js +96 -0
- package/bin/shared-kernel/uia-protocol.js +214 -0
- package/bin/shared-kernel/uia-runtime-port.js +377 -0
- package/bin/shared-kernel/uia-target.js +39 -0
- package/bin/shared-kernel/web-dom-target.js +44 -0
- package/bin/shared-kernel/xml-attributes.js +25 -0
- package/bin/target-execution.js +275 -0
- package/bin/web/command-schema.js +60 -0
- package/bin/web/session-store.js +157 -0
- package/bin/web-provider.js +334 -553
- package/docs/COMMAND_CONTRACT.md +1563 -0
- package/docs/EVIDENCE_ARCHIVE.md +214 -0
- package/docs/INTENT_FOREGROUND.md +71 -0
- package/docs/INTENT_NATIVE_EDITING.md +79 -0
- package/docs/RELEASE.md +59 -0
- package/docs/SCRIPT_AUTHORING.md +489 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
- package/package.json +60 -5
- package/runtime/ios-wda/AABWDABinding.h +19 -0
- package/runtime/ios-wda/AABWDABinding.m +97 -0
- package/runtime/ios-wda/AABWDAExecution.h +26 -0
- package/runtime/ios-wda/AABWDAExecution.m +172 -0
- package/runtime/ios-wda/AABWDAIntegration.h +71 -0
- package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
- package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
- package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
- package/runtime/uia/ai-app-bridge-uia.jar +0 -0
- package/runtime/uia/manifest.json +22 -0
- package/skills/ai-app-bridge-use/SKILL.md +23 -360
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createMemoryEvidenceAdapter } = require('../shared-kernel/evidence-adapters');
|
|
4
|
+
const { createAutonomousAgentAdapter, createIntentBudget } = require('./intent-autonomous-adapter');
|
|
5
|
+
const { createProductionIntentDeviceAdapter } = require('./intent-production-adapter');
|
|
6
|
+
const { createIOSIntentDeviceAdapter } = require('./ios-intent-adapter');
|
|
7
|
+
const { createWebIntentDeviceAdapter } = require('./web-intent-adapter');
|
|
8
|
+
const { intentError } = require('./intent-errors');
|
|
9
|
+
const { intentProviderError } = require('./intent-provider');
|
|
10
|
+
const { createIntentEvidenceStore } = require('./intent-evidence-store');
|
|
11
|
+
const { createIntentWorker } = require('./intent-worker');
|
|
12
|
+
const { createEvidenceRecording } = require('../shared-kernel/evidence-recording');
|
|
13
|
+
const { normalizeIntentTarget, normalizeObservationTarget } = require('./intent-observation-target');
|
|
14
|
+
|
|
15
|
+
const operations = new Map();
|
|
16
|
+
const ledgers = new Map();
|
|
17
|
+
const pendingStarts = new Set();
|
|
18
|
+
const reservedIds = new Set();
|
|
19
|
+
let closing = false;
|
|
20
|
+
const MAX_OPERATIONS = 256;
|
|
21
|
+
let sequence = 0;
|
|
22
|
+
|
|
23
|
+
function handle(args = {}) {
|
|
24
|
+
const operation = args.operation || 'start';
|
|
25
|
+
if (operation === 'start') {
|
|
26
|
+
if (closing) return intentError('runtime_stopping');
|
|
27
|
+
const task = start(args);
|
|
28
|
+
pendingStarts.add(task);
|
|
29
|
+
task.then(() => pendingStarts.delete(task), () => pendingStarts.delete(task));
|
|
30
|
+
return task;
|
|
31
|
+
}
|
|
32
|
+
if (operation === 'decide') return read(args, (worker) => worker.decide(args.decision || args));
|
|
33
|
+
if (operation === 'status') return status(args);
|
|
34
|
+
if (operation === 'observe') return read(args, (worker) => {
|
|
35
|
+
if (worker.isManagedWorkflow && args.observationTarget !== undefined) return intentError('managed_intent_observation_target_fixed', {
|
|
36
|
+
operationId: args.operationId, field: 'observationTarget', dispatched: false, ambiguous: false,
|
|
37
|
+
message: 'Installation and permission workflows use their managed UIAutomator target.',
|
|
38
|
+
});
|
|
39
|
+
if (worker.isManagedWorkflow && args.provider !== undefined && args.provider !== 'uia') return intentError('managed_intent_provider_fixed', {
|
|
40
|
+
operationId: args.operationId, field: 'provider', provider: 'uia', dispatched: false, ambiguous: false,
|
|
41
|
+
message: 'Installation and permission workflows require UIAutomator observations.',
|
|
42
|
+
});
|
|
43
|
+
return worker.observe(args);
|
|
44
|
+
});
|
|
45
|
+
if (operation === 'pause') return read(args, (worker) => worker.pause());
|
|
46
|
+
if (operation === 'resume') return read(args, (worker) => worker.resume());
|
|
47
|
+
if (operation === 'cancel') return read(args, (worker) => worker.cancel());
|
|
48
|
+
if (operation === 'intervene') return read(args, (worker) => worker.intervene(args.reason || 'intervened'));
|
|
49
|
+
return intentError('invalid_operation', { operation });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function start(args) {
|
|
53
|
+
if (!args.goal && !args.install && !args.permissionDialog) {
|
|
54
|
+
return intentError('invalid_intent');
|
|
55
|
+
}
|
|
56
|
+
let target = null, observationTarget = null;
|
|
57
|
+
if (!args.install && !args.permissionDialog) {
|
|
58
|
+
try {
|
|
59
|
+
target = normalizeIntentTarget(args.target);
|
|
60
|
+
observationTarget = normalizeObservationTarget(target, args.provider ?? 'native', args.observationTarget ?? null);
|
|
61
|
+
}
|
|
62
|
+
catch (error) { return intentError(error.code, { field: error.field, message: error.message, dispatched: false, ambiguous: false }); }
|
|
63
|
+
const providerError = intentProviderError(target, args.provider ?? 'native');
|
|
64
|
+
if (providerError) return intentError(providerError.error, { ...providerError, dispatched: false, ambiguous: false });
|
|
65
|
+
}
|
|
66
|
+
sequence += 1;
|
|
67
|
+
const operationId = args.operationId || `intent-${Date.now()}-${sequence}`;
|
|
68
|
+
evictTerminalWorkers();
|
|
69
|
+
evictStaleLedgers();
|
|
70
|
+
if (operations.has(operationId) || reservedIds.has(operationId)) {
|
|
71
|
+
return intentError('operation_exists', { operationId });
|
|
72
|
+
}
|
|
73
|
+
if (operations.size + reservedIds.size >= MAX_OPERATIONS || ledgers.size + reservedIds.size >= MAX_OPERATIONS) {
|
|
74
|
+
return intentError('registry_full', { operationId });
|
|
75
|
+
}
|
|
76
|
+
const store = args.store || createIntentEvidenceStore({ adapter: createMemoryEvidenceAdapter() });
|
|
77
|
+
if (store.list(operationId).length) return intentError('operation_exists', { operationId });
|
|
78
|
+
let recording = null;
|
|
79
|
+
if (args.recordingDir !== undefined) {
|
|
80
|
+
try {
|
|
81
|
+
recording = createEvidenceRecording({ directory: args.recordingDir, namespace: 'intent', operationId, store,
|
|
82
|
+
now: args.now || Date.now });
|
|
83
|
+
} catch (error) { return intentError(error.code || 'recording_failed', { operationId }); }
|
|
84
|
+
}
|
|
85
|
+
reservedIds.add(operationId);
|
|
86
|
+
let worker;
|
|
87
|
+
try { worker = args.install
|
|
88
|
+
? await require('./install-intent').createInstallIntent({ args: args.install, operationId, store, recording })
|
|
89
|
+
: args.permissionDialog
|
|
90
|
+
? await require('./permission-intent').createPermissionIntent({ args: args.permissionDialog, operationId, store, recording })
|
|
91
|
+
: createIntentWorker({
|
|
92
|
+
operationId,
|
|
93
|
+
goal: args.goal || null,
|
|
94
|
+
target,
|
|
95
|
+
provider: args.provider || 'native',
|
|
96
|
+
observationTarget,
|
|
97
|
+
store,
|
|
98
|
+
adapter: resolveAdapter(args),
|
|
99
|
+
mode: args.mode || 'supervised',
|
|
100
|
+
agent: resolveAgent(args),
|
|
101
|
+
budget: resolveBudget(args),
|
|
102
|
+
timeoutMs: args.timeoutMs,
|
|
103
|
+
now: args.now || Date.now,
|
|
104
|
+
capturePort: args.capturePort || null,
|
|
105
|
+
captureRequirements: args.require || null,
|
|
106
|
+
recording,
|
|
107
|
+
}); } finally { reservedIds.delete(operationId); }
|
|
108
|
+
operations.set(operationId, worker);
|
|
109
|
+
ledgers.set(operationId, { store, status: 'created' });
|
|
110
|
+
return closing ? worker.cancel() : worker.start();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function status(args) {
|
|
114
|
+
if (typeof args.operationId !== 'string' || args.operationId.length === 0) return intentError('operationId_required', { field: 'operationId' });
|
|
115
|
+
const worker = operations.get(args.operationId);
|
|
116
|
+
if (worker) return worker.status(args);
|
|
117
|
+
const store = args.store || ledgers.get(args.operationId)?.store;
|
|
118
|
+
if (!store) return intentError('unknown_operation', { operationId: args.operationId || null });
|
|
119
|
+
const records = store.list(args.operationId);
|
|
120
|
+
if (!records.length) return intentError('unknown_operation', { operationId: args.operationId || null });
|
|
121
|
+
for (const record of records) {
|
|
122
|
+
const checked = store.read(record.evidenceId);
|
|
123
|
+
if (!checked.ok) return intentError(checked.error, { operationId: args.operationId });
|
|
124
|
+
}
|
|
125
|
+
const checkpoint = records.findLast(record => record.kind === 'checkpoint');
|
|
126
|
+
const saved = checkpoint?.payloadSummary;
|
|
127
|
+
const history = store.history(args.operationId, args.afterSequence ?? 0, args.limit);
|
|
128
|
+
if (saved && isIntentTerminal(saved.status)) return {
|
|
129
|
+
...saved, command: 'intent', operationId: args.operationId, ok: saved.status === 'completed',
|
|
130
|
+
live: false, recovered: true, restartPolicy: 'none', terminalEvidenceId: checkpoint.evidenceId, history,
|
|
131
|
+
};
|
|
132
|
+
const unmatched = records.filter(record => record.kind === 'dispatch-marker')
|
|
133
|
+
.filter(marker => !records.some(record => record.kind === 'action-receipt' && record.actionId === marker.actionId));
|
|
134
|
+
return { ok: false, command: 'intent', operationId: args.operationId, status: 'interrupted', error: 'runtime_restarted',
|
|
135
|
+
live: false, recovered: true, restartPolicy: 'none', revision: records.at(-1).revision,
|
|
136
|
+
ambiguous: unmatched.length > 0 || records.some(record => record.kind === 'action-receipt' && record.ambiguous),
|
|
137
|
+
unmatchedActionIds: unmatched.map(marker => marker.actionId), history };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function read(args, fn) {
|
|
141
|
+
const worker = operations.get(args.operationId);
|
|
142
|
+
if (!worker) return intentError('unknown_operation', { operationId: args.operationId || null });
|
|
143
|
+
return fn(worker);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveAdapter(args) {
|
|
147
|
+
if (args.adapter && typeof args.adapter.observe === 'function') return args.adapter;
|
|
148
|
+
if (args.adapter != null && args.adapter !== 'production') throw new TypeError('invalid_adapter');
|
|
149
|
+
if (args.target?.platform === 'ios') return createIOSIntentDeviceAdapter({ provider: args.ports?.ios });
|
|
150
|
+
if (args.target?.platform === 'web') return createWebIntentDeviceAdapter({ provider: args.ports?.web });
|
|
151
|
+
return createProductionIntentDeviceAdapter({ ports: args.ports, adb: args.adb });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveAgent(args) {
|
|
155
|
+
if (args.agent) return args.agent;
|
|
156
|
+
if (args.decide) return createAutonomousAgentAdapter({ decide: args.decide });
|
|
157
|
+
if (args.agentModule) {
|
|
158
|
+
const loaded = require(args.agentModule);
|
|
159
|
+
const decide = typeof loaded.decide === 'function' ? loaded.decide.bind(loaded) : loaded;
|
|
160
|
+
return createAutonomousAgentAdapter({ decide });
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resolveBudget(args) {
|
|
166
|
+
if (args.budget && typeof args.budget.check === 'function') return args.budget;
|
|
167
|
+
if (args.mode !== 'autonomous' && !args.budget) return null;
|
|
168
|
+
return createIntentBudget(args.budget || {
|
|
169
|
+
maxSteps: args.maxSteps,
|
|
170
|
+
maxDurationMs: args.maxDurationMs,
|
|
171
|
+
maxAgentCalls: args.maxAgentCalls,
|
|
172
|
+
allowlist: args.allowlist,
|
|
173
|
+
now: args.now,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function evictTerminalWorkers() {
|
|
178
|
+
for (const [id, worker] of operations) {
|
|
179
|
+
if (!worker.isFinished()) continue;
|
|
180
|
+
const snapshot = worker.status();
|
|
181
|
+
if (!isIntentTerminal(snapshot.status)) continue;
|
|
182
|
+
const entry = ledgers.get(id);
|
|
183
|
+
if (entry) entry.status = snapshot.status;
|
|
184
|
+
operations.delete(id);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function evictStaleLedgers() {
|
|
189
|
+
if (ledgers.size < MAX_OPERATIONS) return;
|
|
190
|
+
for (const id of ledgers.keys()) {
|
|
191
|
+
if (operations.has(id)) continue;
|
|
192
|
+
ledgers.delete(id);
|
|
193
|
+
if (ledgers.size < MAX_OPERATIONS) return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isIntentTerminal(status) {
|
|
198
|
+
return status === 'completed'
|
|
199
|
+
|| status === 'failed'
|
|
200
|
+
|| status === 'inconclusive'
|
|
201
|
+
|| status === 'cancelled'
|
|
202
|
+
|| status === 'timeout'
|
|
203
|
+
|| status === 'ambiguous'
|
|
204
|
+
|| status === 'blocked_evidence_store'
|
|
205
|
+
|| status === 'intervention_required';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function resetIntentOperations() {
|
|
209
|
+
const settling = Promise.allSettled([...operations.values()].filter(worker => !worker.isFinished()).map(worker => worker.cancel()));
|
|
210
|
+
operations.clear();
|
|
211
|
+
ledgers.clear();
|
|
212
|
+
closing = false;
|
|
213
|
+
sequence = 0;
|
|
214
|
+
return settling;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function cancelActiveIntents() {
|
|
218
|
+
closing = true;
|
|
219
|
+
const cancelKnown = () => Promise.allSettled([...operations.values()]
|
|
220
|
+
.filter(worker => !worker.isFinished()).map(worker => worker.cancel()));
|
|
221
|
+
await cancelKnown();
|
|
222
|
+
await Promise.allSettled([...pendingStarts]);
|
|
223
|
+
await cancelKnown();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { handle, resetIntentOperations, cancelActiveIntents };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function emptyTimings() {
|
|
4
|
+
return {
|
|
5
|
+
workerOverheadMs: 0,
|
|
6
|
+
targetLeaseWaitMs: 0,
|
|
7
|
+
providerAcquireMs: 0,
|
|
8
|
+
evidenceCommitMs: 0,
|
|
9
|
+
summaryMs: 0,
|
|
10
|
+
decisionWaitMs: 0,
|
|
11
|
+
actionMs: 0,
|
|
12
|
+
receiptCommitMs: 0,
|
|
13
|
+
totalMs: 0,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function intentError(error, extra = {}) {
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
command: 'intent',
|
|
21
|
+
latestEvidenceIds: extra.latestEvidenceIds || {},
|
|
22
|
+
timings: extra.timings || emptyTimings(),
|
|
23
|
+
...extra,
|
|
24
|
+
error,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { intentError, emptyTimings };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createEvidenceStore } = require('../shared-kernel/evidence-store');
|
|
4
|
+
const { createExecutionLedger } = require('../shared-kernel/execution-ledger');
|
|
5
|
+
|
|
6
|
+
function createIntentEvidenceStore(options = {}) {
|
|
7
|
+
const store = createEvidenceStore({ ...options, namespace: 'intent' });
|
|
8
|
+
const ledger = options.ledger || createExecutionLedger();
|
|
9
|
+
const persist = store.persist;
|
|
10
|
+
|
|
11
|
+
async function persistAndRecord(kind, record) {
|
|
12
|
+
const result = await persist(kind, record);
|
|
13
|
+
if (result.ok && typeof record.operationId === 'string' && record.operationId.length > 0) {
|
|
14
|
+
ledger.record({
|
|
15
|
+
executionId: record.operationId,
|
|
16
|
+
revision: record.revision,
|
|
17
|
+
kind,
|
|
18
|
+
target: record.target,
|
|
19
|
+
timestampMs: timestampFor(kind, record),
|
|
20
|
+
actionId: record.actionId,
|
|
21
|
+
parentFactId: kind === 'decision' ? firstId(record.basedOnEvidenceIds) : record.parentFactId,
|
|
22
|
+
payloadSummary: payloadSummaryFor(kind, record),
|
|
23
|
+
evidenceRefs: [result.evidenceId],
|
|
24
|
+
timings: kind === 'action-receipt' ? timingsOf(record) : record.timings,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...store,
|
|
32
|
+
persist: persistAndRecord,
|
|
33
|
+
commit: persistAndRecord,
|
|
34
|
+
ledger,
|
|
35
|
+
history(operationId, afterSequence = 0, limit) {
|
|
36
|
+
const restored = createExecutionLedger();
|
|
37
|
+
for (const record of store.list(operationId)) {
|
|
38
|
+
const checked = store.read(record.evidenceId);
|
|
39
|
+
if (!checked.ok) throw Object.assign(new Error(checked.error), { code: checked.error });
|
|
40
|
+
restored.record({ executionId: operationId, revision: record.revision, kind: record.kind,
|
|
41
|
+
target: record.target, timestampMs: timestampFor(record.kind, record), actionId: record.actionId,
|
|
42
|
+
parentFactId: record.kind === 'decision' ? firstId(record.basedOnEvidenceIds) : record.parentFactId,
|
|
43
|
+
payloadSummary: payloadSummaryFor(record.kind, record), evidenceRefs: [record.evidenceId],
|
|
44
|
+
timings: record.kind === 'action-receipt' ? timingsOf(record) : record.timings });
|
|
45
|
+
}
|
|
46
|
+
return restored.query(operationId, afterSequence, limit);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function timestampFor(kind, record) {
|
|
52
|
+
if (kind === 'observation') return record.capturedAtMs;
|
|
53
|
+
if (kind === 'action-receipt') return record.completedAtMs;
|
|
54
|
+
return record.timestampMs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function firstId(ids) {
|
|
58
|
+
return Array.isArray(ids) && ids.length > 0 ? ids[0] : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function timingsOf(record) {
|
|
62
|
+
if (record.startedAtMs == null && record.completedAtMs == null) return undefined;
|
|
63
|
+
return { startedAtMs: record.startedAtMs, completedAtMs: record.completedAtMs };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function payloadSummaryFor(kind, record) {
|
|
67
|
+
if (kind === 'observation') {
|
|
68
|
+
const summary = {
|
|
69
|
+
provider: record.provider,
|
|
70
|
+
observationTarget: record.observationTarget,
|
|
71
|
+
rawTreeId: record.rawTreeId,
|
|
72
|
+
foregroundTarget: record.foregroundTarget,
|
|
73
|
+
capturedAtMs: record.capturedAtMs,
|
|
74
|
+
};
|
|
75
|
+
if (record.captureRefs !== undefined) summary.captureRefs = record.captureRefs;
|
|
76
|
+
if (record.captureCoverage !== undefined) summary.captureCoverage = record.captureCoverage;
|
|
77
|
+
if (record.capturePages !== undefined) summary.capturePages = record.capturePages;
|
|
78
|
+
return summary;
|
|
79
|
+
}
|
|
80
|
+
if (kind === 'summary') {
|
|
81
|
+
return {
|
|
82
|
+
rawTreeId: record.rawTreeId,
|
|
83
|
+
pageSummary: record.summary,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (kind === 'decision') {
|
|
87
|
+
return {
|
|
88
|
+
decisionId: record.decisionId,
|
|
89
|
+
agentDecision: record.agentDecision,
|
|
90
|
+
actionSpecHash: record.actionSpecHash,
|
|
91
|
+
basedOnEvidenceIds: record.basedOnEvidenceIds,
|
|
92
|
+
mode: record.mode,
|
|
93
|
+
action: record.action,
|
|
94
|
+
reason: record.reason,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (kind === 'dispatch-marker') {
|
|
98
|
+
return {
|
|
99
|
+
decisionId: record.decisionId,
|
|
100
|
+
actionSpecHash: record.actionSpecHash,
|
|
101
|
+
action: record.action,
|
|
102
|
+
state: record.state,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (kind === 'action-receipt') {
|
|
106
|
+
return {
|
|
107
|
+
mechanicalStatus: record.mechanicalStatus,
|
|
108
|
+
error: record.error,
|
|
109
|
+
ambiguous: record.ambiguous === true,
|
|
110
|
+
dispatched: record.dispatched,
|
|
111
|
+
action: record.action,
|
|
112
|
+
resolved: record.resolved,
|
|
113
|
+
matched: record.matched,
|
|
114
|
+
rawTreeId: record.rawTreeId,
|
|
115
|
+
basedOnEvidenceIds: record.basedOnEvidenceIds,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return record.payloadSummary;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { createIntentEvidenceStore };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CommandError } = require('../command-errors');
|
|
4
|
+
const { runExecution } = require('../shared-kernel/execution-scope');
|
|
5
|
+
|
|
6
|
+
// An Intent owns work across multiple requests, including time spent idle.
|
|
7
|
+
// Draining waits for owned provider/persistence work; it never races that work
|
|
8
|
+
// against a timer and pretends it has settled.
|
|
9
|
+
function createIntentLifetime({ timeoutMs, onDeadline }) {
|
|
10
|
+
if (timeoutMs !== null && (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)) {
|
|
11
|
+
throw new CommandError('invalid_argument', 'Intent timeoutMs must be a positive integer in milliseconds.', { field: 'timeoutMs' });
|
|
12
|
+
}
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const pending = new Set();
|
|
15
|
+
let started = false;
|
|
16
|
+
let timer;
|
|
17
|
+
let deadlineMs = null;
|
|
18
|
+
function start() {
|
|
19
|
+
if (started) return;
|
|
20
|
+
started = true;
|
|
21
|
+
if (timeoutMs !== null) {
|
|
22
|
+
deadlineMs = Date.now() + timeoutMs;
|
|
23
|
+
timer = setTimeout(onDeadline, timeoutMs);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function run(action, mutation = false) {
|
|
28
|
+
if (controller.signal.aborted) return Promise.reject(new CommandError('operation_stopped', 'This Intent no longer accepts work.'));
|
|
29
|
+
const task = runExecution({ deadlineMs: deadlineMs ?? Infinity, signal: controller.signal, mutation }, action);
|
|
30
|
+
pending.add(task);
|
|
31
|
+
const remove = () => pending.delete(task);
|
|
32
|
+
task.then(remove, remove);
|
|
33
|
+
return task;
|
|
34
|
+
}
|
|
35
|
+
function stop(code) {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
controller.abort({ code });
|
|
38
|
+
}
|
|
39
|
+
return { start, run, stop, drain: () => Promise.allSettled([...pending]), signal: controller.signal,
|
|
40
|
+
get deadlineMs() { return deadlineMs; }, get pendingCount() { return pending.size; } };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Agent replies are values, not device actions. Pass a cancellation signal and
|
|
44
|
+
// stop accepting replies on pause/cancel. An ignored signal cannot dispatch a
|
|
45
|
+
// late decision through the Intent after this promise has settled.
|
|
46
|
+
function awaitAgentReply(agent, input, signal) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const abort = () => finish(new CommandError(signal.reason?.code || 'cancelled', 'The Intent stopped waiting for this Agent reply.'));
|
|
49
|
+
const finish = (error, value) => {
|
|
50
|
+
signal.removeEventListener('abort', abort);
|
|
51
|
+
error ? reject(error) : resolve(value);
|
|
52
|
+
};
|
|
53
|
+
if (signal.aborted) { abort(); return; }
|
|
54
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
55
|
+
Promise.resolve().then(() => {
|
|
56
|
+
if (signal.aborted) throw new CommandError(signal.reason?.code || 'cancelled', 'Agent request cancelled before dispatch.');
|
|
57
|
+
return agent.decide({ ...input, signal });
|
|
58
|
+
}).then(value => finish(null, value), error => finish(error));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { createIntentLifetime, awaitAgentReply };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { object, text, validateValue } = require('../shared-kernel/argument-schema');
|
|
4
|
+
const { executionTargetSchema, normalizeExecutionTarget } = require('../shared-kernel/execution-target');
|
|
5
|
+
const { CommandError } = require('../command-errors');
|
|
6
|
+
|
|
7
|
+
const observationTargetSchema = { anyOf: [object({ webViewId: text }, ['webViewId']), { type: 'null' }],
|
|
8
|
+
description: 'Android or iOS H5 WebView selection. Null requires one visible WebView; an explicit ID never selects a replacement.' };
|
|
9
|
+
|
|
10
|
+
function intentTargetSchema() {
|
|
11
|
+
const schema = executionTargetSchema({ intent: true });
|
|
12
|
+
// Script defaults and immutable evidence targets retain their own contract.
|
|
13
|
+
// A live Intent selects WebViews as observation state, not its frozen target.
|
|
14
|
+
delete schema.anyOf.find(branch => branch.properties.platform.const === 'ios').properties.webViewId;
|
|
15
|
+
return schema;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeIntentTarget(value) {
|
|
19
|
+
validateValue(value, intentTargetSchema(), 'target');
|
|
20
|
+
return normalizeExecutionTarget(value, { intent: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeObservationTarget(target, provider, value) {
|
|
24
|
+
validateValue(value, observationTargetSchema, 'observationTarget');
|
|
25
|
+
if (value === null) return null;
|
|
26
|
+
if (!['android', 'ios'].includes(target.platform) || provider !== 'h5') {
|
|
27
|
+
throw new CommandError('unsupported_intent_observation_target', 'webViewId selects an Android or iOS H5 observation.',
|
|
28
|
+
{ field: 'observationTarget.webViewId' });
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({ webViewId: value.webViewId });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { observationTargetSchema, intentTargetSchema, normalizeIntentTarget, normalizeObservationTarget };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { summarizeTree } = require('../shared-kernel/summary-transformer');
|
|
4
|
+
const { intentError } = require('./intent-errors');
|
|
5
|
+
const { checkExecution } = require('../shared-kernel/execution-scope');
|
|
6
|
+
const { capturePageMetadata } = require('../shared-kernel/live-capture-query');
|
|
7
|
+
|
|
8
|
+
async function observeAndCommit(context, provider = context.provider, observationTarget = context.observationTarget) {
|
|
9
|
+
const timings = context.timings || {};
|
|
10
|
+
const observeStarted = context.now();
|
|
11
|
+
const observation = await context.adapter.observe({
|
|
12
|
+
...context.target,
|
|
13
|
+
provider,
|
|
14
|
+
observationTarget,
|
|
15
|
+
rawTreeId: `${context.operationId}:${context.revision}`,
|
|
16
|
+
});
|
|
17
|
+
checkExecution();
|
|
18
|
+
timings.providerAcquireMs = (timings.providerAcquireMs || 0) + (context.now() - observeStarted);
|
|
19
|
+
if (!observation || observation.ok === false) {
|
|
20
|
+
const failure = { provider, observationTarget, revision: context.revision, response: observation };
|
|
21
|
+
const persisted = await context.store.persist('checkpoint', { operationId: context.operationId,
|
|
22
|
+
revision: context.revision, target: context.target, stepId: 'observation-failed', timestampMs: context.now(),
|
|
23
|
+
payloadSummary: { observationFailure: failure } });
|
|
24
|
+
if (!persisted.ok) return intentError(persisted.error || 'evidence_not_persisted', { persisted: false });
|
|
25
|
+
checkExecution();
|
|
26
|
+
context.observationFailure = { ...failure, evidenceId: persisted.evidenceId };
|
|
27
|
+
context.latestEvidenceIds = { ...context.latestEvidenceIds, observationFailure: persisted.evidenceId };
|
|
28
|
+
return intentError(observation?.error || 'provider_failed', { stage: 'provider' });
|
|
29
|
+
}
|
|
30
|
+
const routed = context.target.foregroundPackages !== undefined;
|
|
31
|
+
if (routed && (!observation.route || !['native', 'uia', 'flutter', 'h5'].includes(observation.provider))) return intentError('foreground_observation_required');
|
|
32
|
+
const activeProvider = routed ? observation.provider : provider;
|
|
33
|
+
const capture = await observeCapture(context);
|
|
34
|
+
checkExecution();
|
|
35
|
+
const persistStarted = context.now();
|
|
36
|
+
const observationRecord = {
|
|
37
|
+
operationId: context.operationId,
|
|
38
|
+
revision: context.revision,
|
|
39
|
+
target: context.target,
|
|
40
|
+
observedTarget: routed ? { ...context.target, packageName: observation.route.packageName } : context.target,
|
|
41
|
+
provider: activeProvider,
|
|
42
|
+
observationTarget,
|
|
43
|
+
capturedAtMs: context.now(),
|
|
44
|
+
foregroundTarget: observation.foregroundTarget,
|
|
45
|
+
rawTreeId: observation.rawTreeId,
|
|
46
|
+
rawTree: observation.rawTree,
|
|
47
|
+
...(routed ? { route: observation.route, requestedTarget: context.target } : {}),
|
|
48
|
+
};
|
|
49
|
+
if (capture) {
|
|
50
|
+
observationRecord.captureRefs = capture.refs;
|
|
51
|
+
observationRecord.captureCoverage = capture.coverage;
|
|
52
|
+
observationRecord.capturePages = capture.pages.map(capturePageMetadata);
|
|
53
|
+
}
|
|
54
|
+
const persisted = await context.store.persist('observation', observationRecord);
|
|
55
|
+
timings.evidenceCommitMs = (timings.evidenceCommitMs || 0) + (context.now() - persistStarted);
|
|
56
|
+
if (!persisted.ok) {
|
|
57
|
+
return intentError(persisted.error || 'evidence_not_persisted', { persisted: false });
|
|
58
|
+
}
|
|
59
|
+
checkExecution();
|
|
60
|
+
if (capture && context.recording) {
|
|
61
|
+
const saved = await context.recording.record({ kind: 'intent-capture', revision: context.revision,
|
|
62
|
+
target: context.target, parentFactId: persisted.evidenceId,
|
|
63
|
+
data: { rawTreeId: observation.rawTreeId, capture } });
|
|
64
|
+
if (!saved.ok) return intentError(saved.error, { persisted: false });
|
|
65
|
+
checkExecution();
|
|
66
|
+
}
|
|
67
|
+
const summaryStarted = context.now();
|
|
68
|
+
const summary = summarizeTree({
|
|
69
|
+
provider: activeProvider,
|
|
70
|
+
rawTree: observation.rawTree,
|
|
71
|
+
rawTreeId: observation.rawTreeId,
|
|
72
|
+
...(routed ? { maxBytes: 64 * 1024 - Buffer.byteLength(JSON.stringify(observation.route), 'utf8') - 32 } : {}),
|
|
73
|
+
});
|
|
74
|
+
timings.summaryMs = (timings.summaryMs || 0) + (context.now() - summaryStarted);
|
|
75
|
+
if (!summary.ok) return intentError(summary.error || 'summary_failed');
|
|
76
|
+
if (routed) summary.foreground = observation.route;
|
|
77
|
+
const summaryPersist = await context.store.persist('summary', {
|
|
78
|
+
operationId: context.operationId,
|
|
79
|
+
revision: context.revision,
|
|
80
|
+
rawTreeId: observation.rawTreeId,
|
|
81
|
+
target: context.target,
|
|
82
|
+
timestampMs: context.now(),
|
|
83
|
+
observationTarget,
|
|
84
|
+
summary,
|
|
85
|
+
});
|
|
86
|
+
if (!summaryPersist.ok) return intentError(summaryPersist.error || 'summary_not_persisted');
|
|
87
|
+
checkExecution();
|
|
88
|
+
const exposed = context.store.canExposeRevision(context.operationId);
|
|
89
|
+
if (!exposed.ok) return intentError(exposed.error);
|
|
90
|
+
context.provider = provider;
|
|
91
|
+
context.observationTarget = observationTarget;
|
|
92
|
+
context.observationFailure = null;
|
|
93
|
+
delete context.latestEvidenceIds.observationFailure;
|
|
94
|
+
context.latestSummary = summary;
|
|
95
|
+
context.latestProvider = activeProvider;
|
|
96
|
+
context.latestRoute = observation.route;
|
|
97
|
+
context.latestEvidenceId = persisted.evidenceId;
|
|
98
|
+
context.latestCapture = capture;
|
|
99
|
+
context.latestRawTree = observation.rawTree;
|
|
100
|
+
context.latestRawTreeId = observation.rawTreeId;
|
|
101
|
+
context.latestEvidenceIds = {
|
|
102
|
+
...context.latestEvidenceIds,
|
|
103
|
+
observation: persisted.evidenceId,
|
|
104
|
+
summary: summaryPersist.evidenceId,
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
revision: exposed.revision,
|
|
109
|
+
evidenceId: persisted.evidenceId,
|
|
110
|
+
summary: context.latestSummary,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function observeCapture(context) {
|
|
115
|
+
if (!context.capturePort || !context.captureRequirements) return null;
|
|
116
|
+
const { streams, ...requirements } = context.captureRequirements;
|
|
117
|
+
if (streams.length === 0) return null;
|
|
118
|
+
const pages = [];
|
|
119
|
+
context.captureWatermarks ||= new Map();
|
|
120
|
+
for (const stream of streams) {
|
|
121
|
+
const previous = context.captureWatermarks.get(stream);
|
|
122
|
+
const actionCursor = previous && previous.actionId === context.actionId ? previous.actionCursor : previous?.cursor;
|
|
123
|
+
const page = await context.capturePort.observe({
|
|
124
|
+
...requirements,
|
|
125
|
+
stream,
|
|
126
|
+
...(context.actionId && previous && requirements.view !== 'connected-history' && requirements.history !== true
|
|
127
|
+
? { factCursor: requirements.factCursor ?? actionCursor, runtimeEpoch: requirements.runtimeEpoch ?? previous.epoch }
|
|
128
|
+
: {}),
|
|
129
|
+
}, {
|
|
130
|
+
target: context.target,
|
|
131
|
+
platform: context.target.platform,
|
|
132
|
+
actionId: context.actionId,
|
|
133
|
+
runtimeEpoch: context.runtimeEpoch,
|
|
134
|
+
runtimeEpochChanged: context.runtimeEpochChanged === true,
|
|
135
|
+
disconnected: context.disconnected === true,
|
|
136
|
+
});
|
|
137
|
+
// An issued watermark bounds the next action even if earlier retained data
|
|
138
|
+
// has a known gap. The current observation keeps that partial coverage.
|
|
139
|
+
if (page.committed === true && ['complete', 'partial'].includes(page.coverage.status)
|
|
140
|
+
&& page.watermarkCursor && page.runtimeEpoch) {
|
|
141
|
+
context.captureWatermarks.set(stream, { cursor: page.watermarkCursor, epoch: page.runtimeEpoch,
|
|
142
|
+
actionId: context.actionId, actionCursor: context.actionId ? requirements.factCursor ?? actionCursor : null });
|
|
143
|
+
}
|
|
144
|
+
pages.push({ stream, ...page });
|
|
145
|
+
}
|
|
146
|
+
return mergeCapturePages(pages);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function mergeCapturePages(pages) {
|
|
150
|
+
const refs = [];
|
|
151
|
+
const items = [];
|
|
152
|
+
let gap = false;
|
|
153
|
+
let committed = true;
|
|
154
|
+
let status = null;
|
|
155
|
+
for (const page of pages) {
|
|
156
|
+
refs.push(...page.refs);
|
|
157
|
+
items.push(...page.items);
|
|
158
|
+
if (page.gap) gap = true;
|
|
159
|
+
if (page.committed !== true) committed = false;
|
|
160
|
+
const pageStatus = page.coverage && page.coverage.status;
|
|
161
|
+
if (
|
|
162
|
+
pageStatus !== 'complete'
|
|
163
|
+
&& pageStatus !== 'partial'
|
|
164
|
+
&& pageStatus !== 'unavailable'
|
|
165
|
+
) {
|
|
166
|
+
status = 'unavailable';
|
|
167
|
+
} else if (pageStatus === 'unavailable' || status === 'unavailable') {
|
|
168
|
+
status = 'unavailable';
|
|
169
|
+
} else if (pageStatus === 'partial' || status === 'partial') {
|
|
170
|
+
status = 'partial';
|
|
171
|
+
} else {
|
|
172
|
+
status = 'complete';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (status == null) {
|
|
176
|
+
return {
|
|
177
|
+
coverage: { status: 'unavailable', gap: true, committed: false },
|
|
178
|
+
gap: true,
|
|
179
|
+
committed: false,
|
|
180
|
+
refs: [],
|
|
181
|
+
items: [],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
coverage: { status, gap, committed },
|
|
186
|
+
gap,
|
|
187
|
+
committed,
|
|
188
|
+
refs,
|
|
189
|
+
items,
|
|
190
|
+
pages,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { observeAndCommit };
|