@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 { createSegmentedFactStoreAdapter } = require('./segmented-fact-store');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stable FactStore Module boundary.
|
|
7
|
+
*
|
|
8
|
+
* Runtime callers use record for an immediate commit or offer for a bounded
|
|
9
|
+
* asynchronous write, then read/status. Segment layout, recovery, quotas,
|
|
10
|
+
* cursors and diagnostic adapters remain implementation details.
|
|
11
|
+
*/
|
|
12
|
+
class FactStore {
|
|
13
|
+
constructor(adapter, { queueLimit = 256, schedule = setImmediate } = {}) {
|
|
14
|
+
assertAdapter(adapter);
|
|
15
|
+
if (!Number.isInteger(queueLimit) || queueLimit < 1) {
|
|
16
|
+
throw new TypeError('queueLimit must be a positive integer');
|
|
17
|
+
}
|
|
18
|
+
if (typeof schedule !== 'function') throw new TypeError('schedule must be a function');
|
|
19
|
+
this.adapter = adapter;
|
|
20
|
+
this.queueLimit = queueLimit;
|
|
21
|
+
this.schedule = schedule;
|
|
22
|
+
this.queue = [];
|
|
23
|
+
this.scheduled = false;
|
|
24
|
+
this.flushing = false;
|
|
25
|
+
this.flushPromise = null;
|
|
26
|
+
this.drainWaiters = [];
|
|
27
|
+
this.closed = false;
|
|
28
|
+
this.writerAvailable = true;
|
|
29
|
+
this.writerReason = null;
|
|
30
|
+
this.writerDropped = 0;
|
|
31
|
+
this.writerLastError = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
record(facts, options = {}) {
|
|
35
|
+
if (this.closed) throw new Error('FactStore is closed');
|
|
36
|
+
const list = Array.isArray(facts) ? facts : [facts];
|
|
37
|
+
if (list.length === 0) return { ok: true, receipts: [], count: 0 };
|
|
38
|
+
const receipts = list.map((fact) => this.adapter.record(fact, options));
|
|
39
|
+
if (receipts.some(isPromise)) {
|
|
40
|
+
throw new TypeError('FactStore.record must complete synchronously; use offer for asynchronous writes');
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
ok: receipts.every((receipt) => receipt?.ok !== false),
|
|
44
|
+
receipts,
|
|
45
|
+
count: receipts.length,
|
|
46
|
+
...(Array.isArray(facts) ? {} : { receipt: receipts[0] }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
offer(facts, options = {}) {
|
|
51
|
+
if (this.closed) return rejectedOffer('closed');
|
|
52
|
+
if (!this.writerAvailable) return rejectedOffer(this.writerReason || 'unavailable');
|
|
53
|
+
if (this.queue.length >= this.queueLimit) {
|
|
54
|
+
this.writerDropped += 1;
|
|
55
|
+
return rejectedOffer('queue_full');
|
|
56
|
+
}
|
|
57
|
+
let resolveCompletion;
|
|
58
|
+
const completion = new Promise((resolve) => {
|
|
59
|
+
resolveCompletion = resolve;
|
|
60
|
+
});
|
|
61
|
+
this.queue.push({ facts, options, resolveCompletion });
|
|
62
|
+
this.kick();
|
|
63
|
+
return { accepted: true, completion };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
read(query = {}) {
|
|
67
|
+
return this.adapter.read(query);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
status() {
|
|
71
|
+
return {
|
|
72
|
+
...this.adapter.status(),
|
|
73
|
+
writer: {
|
|
74
|
+
available: !this.closed && this.writerAvailable,
|
|
75
|
+
reason: this.closed ? 'closed' : this.writerReason,
|
|
76
|
+
queueLength: this.queue.length,
|
|
77
|
+
dropped: this.writerDropped,
|
|
78
|
+
lastError: this.writerLastError,
|
|
79
|
+
flushing: this.flushing,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
close() {
|
|
85
|
+
if (this.closed) return;
|
|
86
|
+
this.closed = true;
|
|
87
|
+
if (this.queue.length > 0) {
|
|
88
|
+
this.writerDropped += this.queue.length;
|
|
89
|
+
this.settleQueuedFailures('closed');
|
|
90
|
+
}
|
|
91
|
+
this.adapter.close?.();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async drain() {
|
|
95
|
+
if (this.queue.length === 0 && !this.flushing && !this.scheduled) return;
|
|
96
|
+
this.kick();
|
|
97
|
+
await new Promise((resolve) => this.drainWaiters.push(resolve));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
kick() {
|
|
101
|
+
if (this.scheduled || this.flushing || this.queue.length === 0 || !this.writerAvailable) return;
|
|
102
|
+
this.scheduled = true;
|
|
103
|
+
this.schedule(() => {
|
|
104
|
+
this.scheduled = false;
|
|
105
|
+
void this.flush();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
flush() {
|
|
110
|
+
if (this.flushPromise) return this.flushPromise;
|
|
111
|
+
this.flushing = true;
|
|
112
|
+
this.flushPromise = (async () => {
|
|
113
|
+
while (this.queue.length > 0 && this.writerAvailable) {
|
|
114
|
+
const item = this.queue[0];
|
|
115
|
+
const result = await recordAsync(this.adapter, item.facts, item.options);
|
|
116
|
+
item.resolveCompletion(result);
|
|
117
|
+
this.queue.shift();
|
|
118
|
+
if (result.ok === false) {
|
|
119
|
+
this.writerLastError = result.error || 'persist_failed';
|
|
120
|
+
if (isTerminalWriterError(result.error)) {
|
|
121
|
+
this.writerAvailable = false;
|
|
122
|
+
this.writerReason = result.error;
|
|
123
|
+
this.writerDropped += this.queue.length;
|
|
124
|
+
this.settleQueuedFailures(result.error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
})().finally(() => {
|
|
129
|
+
this.flushing = false;
|
|
130
|
+
this.flushPromise = null;
|
|
131
|
+
this.resolveDrainWaiters();
|
|
132
|
+
this.kick();
|
|
133
|
+
});
|
|
134
|
+
return this.flushPromise;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
settleQueuedFailures(error) {
|
|
138
|
+
while (this.queue.length > 0) {
|
|
139
|
+
const item = this.queue.shift();
|
|
140
|
+
item.resolveCompletion({ ok: false, stored: false, error });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
resolveDrainWaiters() {
|
|
145
|
+
if (this.queue.length > 0 || this.flushing || this.scheduled) return;
|
|
146
|
+
for (const resolve of this.drainWaiters.splice(0)) resolve();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createSegmentedFactStore(options = {}) {
|
|
152
|
+
return new FactStore(createSegmentedFactStoreAdapter(options), {
|
|
153
|
+
queueLimit: options.queueLimit,
|
|
154
|
+
schedule: options.schedule,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Production factory. Segmented mmap is the only authoritative fact-payload
|
|
160
|
+
* implementation. SQLite belongs exclusively to the rebuildable query
|
|
161
|
+
* projection inside SegmentedFactStore; it is never a second payload backend
|
|
162
|
+
* and native-load failures are never hidden by switching storage engines.
|
|
163
|
+
*/
|
|
164
|
+
function createFactStore(options = {}) {
|
|
165
|
+
if (Object.prototype.hasOwnProperty.call(options, 'backend')) {
|
|
166
|
+
throw new TypeError('FactStore backend selection is not supported; segmented mmap is authoritative');
|
|
167
|
+
}
|
|
168
|
+
return createSegmentedFactStore(options);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function assertAdapter(adapter) {
|
|
172
|
+
if (
|
|
173
|
+
!adapter
|
|
174
|
+
|| typeof adapter.record !== 'function'
|
|
175
|
+
|| typeof adapter.read !== 'function'
|
|
176
|
+
|| typeof adapter.status !== 'function'
|
|
177
|
+
) {
|
|
178
|
+
throw new TypeError('FactStore adapter must implement record/read/status');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function recordAsync(adapter, facts, options) {
|
|
183
|
+
const list = Array.isArray(facts) ? facts : [facts];
|
|
184
|
+
if (list.length === 0) return { ok: true, receipts: [], count: 0 };
|
|
185
|
+
const receipts = [];
|
|
186
|
+
try {
|
|
187
|
+
for (const fact of list) {
|
|
188
|
+
receipts.push(await adapter.record(fact, options));
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
stored: false,
|
|
194
|
+
error: error.code || error.message || 'persist_failed',
|
|
195
|
+
detail: error.message || String(error),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: receipts.every((receipt) => receipt?.ok !== false),
|
|
200
|
+
receipts,
|
|
201
|
+
count: receipts.length,
|
|
202
|
+
...(Array.isArray(facts) ? {} : {
|
|
203
|
+
receipt: receipts[0],
|
|
204
|
+
stored: receipts[0]?.stored,
|
|
205
|
+
error: receipts[0]?.error,
|
|
206
|
+
}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isPromise(value) {
|
|
211
|
+
return value && typeof value.then === 'function';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isTerminalWriterError(error) {
|
|
215
|
+
return error === 'ENOSPC' || error === 'fact_store_disabled';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function rejectedOffer(reason) {
|
|
219
|
+
return { accepted: false, reason };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
FactStore,
|
|
224
|
+
createFactStore,
|
|
225
|
+
createSegmentedFactStore,
|
|
226
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
async function runWithFeedbackProbe({
|
|
4
|
+
command,
|
|
5
|
+
args = {},
|
|
6
|
+
runner,
|
|
7
|
+
sleep = delay,
|
|
8
|
+
maxWaitMs = 400,
|
|
9
|
+
intervalMs = 100,
|
|
10
|
+
} = {}) {
|
|
11
|
+
if (typeof runner !== 'function') throw new TypeError('runner is required');
|
|
12
|
+
if (!isFullFeedback(args.feedback) || !isMutationCommand(command)) {
|
|
13
|
+
return { result: await runner(command, args), observation: null, evidence: [] };
|
|
14
|
+
}
|
|
15
|
+
if (command === 'launch-app' || command === 'launch-activity') {
|
|
16
|
+
return captureLaunchFeedback(command, args, runner);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const eventCommand = eventCommandFor(command);
|
|
20
|
+
const before = await safeRun(runner, eventCommand, { ...args, limit: 1 });
|
|
21
|
+
let sinceId = latestId(before?.items);
|
|
22
|
+
const result = await runner(command, args);
|
|
23
|
+
const evidence = [];
|
|
24
|
+
const uiEvents = [];
|
|
25
|
+
const semanticEvents = [];
|
|
26
|
+
const renderEvents = [];
|
|
27
|
+
const interactionEvents = [];
|
|
28
|
+
const attempts = Math.max(1, Math.ceil(maxWaitMs / Math.max(1, intervalMs)));
|
|
29
|
+
let pollCount = 0;
|
|
30
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
31
|
+
await sleep(intervalMs);
|
|
32
|
+
pollCount += 1;
|
|
33
|
+
const captured = await safeRun(runner, eventCommand, {
|
|
34
|
+
...args,
|
|
35
|
+
sinceId,
|
|
36
|
+
limit: 100,
|
|
37
|
+
});
|
|
38
|
+
if (!captured) continue;
|
|
39
|
+
evidence.push({ command: eventCommand, result: captured });
|
|
40
|
+
const items = Array.isArray(captured.items) ? captured.items : [];
|
|
41
|
+
sinceId = Math.max(sinceId, latestId(items));
|
|
42
|
+
for (const item of items.filter(isUiEvent)) {
|
|
43
|
+
const kind = uiEventKind(item);
|
|
44
|
+
if (kind === 'stable') continue;
|
|
45
|
+
uiEvents.push(item);
|
|
46
|
+
if (kind === 'semantic') semanticEvents.push(item);
|
|
47
|
+
if (kind === 'render' || item?.data?.renderChanged === true) renderEvents.push(item);
|
|
48
|
+
if (kind === 'interaction' || uiEventHasInteraction(item)) interactionEvents.push(item);
|
|
49
|
+
}
|
|
50
|
+
if (semanticEvents.length > 0) break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (semanticEvents.length > 0) {
|
|
54
|
+
return {
|
|
55
|
+
result,
|
|
56
|
+
evidence,
|
|
57
|
+
observation: {
|
|
58
|
+
mode: 'full',
|
|
59
|
+
changed: true,
|
|
60
|
+
semanticChanged: true,
|
|
61
|
+
renderChanged: renderEvents.length > 0,
|
|
62
|
+
interactionObserved: interactionEvents.length > 0,
|
|
63
|
+
inconclusive: false,
|
|
64
|
+
waitedMs: Math.min(maxWaitMs, pollCount * intervalMs),
|
|
65
|
+
events: uiEvents.slice(0, 20).map(summarizeUiEvent),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const fallback = {};
|
|
71
|
+
const treeCommand = treeCommandFor(command);
|
|
72
|
+
const tree = await safeRun(runner, treeCommand, args);
|
|
73
|
+
if (tree) {
|
|
74
|
+
evidence.push({ command: treeCommand, result: tree });
|
|
75
|
+
fallback.tree = summarizeTree(tree);
|
|
76
|
+
}
|
|
77
|
+
if (args.feedbackScreenshot !== false && !String(command).startsWith('web-')) {
|
|
78
|
+
const screenshotCommand = screenshotCommandFor(command);
|
|
79
|
+
const screenshot = await safeRun(runner, screenshotCommand, args);
|
|
80
|
+
if (screenshot) {
|
|
81
|
+
evidence.push({ command: screenshotCommand, result: screenshot });
|
|
82
|
+
fallback.screenshot = summarizeScreenshot(screenshot);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
result,
|
|
87
|
+
evidence,
|
|
88
|
+
observation: {
|
|
89
|
+
mode: 'full',
|
|
90
|
+
changed: renderEvents.length > 0,
|
|
91
|
+
semanticChanged: false,
|
|
92
|
+
renderChanged: renderEvents.length > 0,
|
|
93
|
+
interactionObserved: interactionEvents.length > 0,
|
|
94
|
+
inconclusive: true,
|
|
95
|
+
waitedMs: Math.min(maxWaitMs, pollCount * intervalMs),
|
|
96
|
+
events: uiEvents.slice(0, 20).map(summarizeUiEvent),
|
|
97
|
+
fallback,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function captureLaunchFeedback(command, args, runner) {
|
|
103
|
+
// Launch must work without a running App SDK. Observe the system only after
|
|
104
|
+
// the original launch has settled, without claiming a before/after UI change.
|
|
105
|
+
const result = await runner(command, args);
|
|
106
|
+
if (result?.ok !== true) return { result, observation: null, evidence: [] };
|
|
107
|
+
const evidence = [];
|
|
108
|
+
const current = { foreground: result.foreground ?? null };
|
|
109
|
+
const targetArgs = pick(args, ['serial', 'adb', 'adbPath', 'packageName']);
|
|
110
|
+
const reads = [['uia-tree', { ...targetArgs, compact: true }, 'tree', summarizeTree]];
|
|
111
|
+
if (args.feedbackScreenshot !== false) reads.push(['screenshot', targetArgs, 'screenshot', summarizeScreenshot]);
|
|
112
|
+
for (const [readCommand, readArgs, key, summarize] of reads) {
|
|
113
|
+
let captured;
|
|
114
|
+
try { captured = await runner(readCommand, readArgs); }
|
|
115
|
+
catch (error) { captured = { ok: false, error: error.code || 'feedback_capture_failed', message: String(error.message || error) }; }
|
|
116
|
+
evidence.push({ command: readCommand, result: captured });
|
|
117
|
+
current[key] = summarize(captured);
|
|
118
|
+
}
|
|
119
|
+
return { result, evidence, observation: {
|
|
120
|
+
mode: 'full', basis: 'post-launch-system-snapshot', changed: false,
|
|
121
|
+
semanticChanged: false, renderChanged: false, interactionObserved: false,
|
|
122
|
+
inconclusive: true, events: [], current,
|
|
123
|
+
} };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isFullFeedback(value) {
|
|
127
|
+
return String(value || '').toLowerCase() === 'full';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isMutationCommand(command) {
|
|
131
|
+
const normalized = String(command || '').toLowerCase();
|
|
132
|
+
const readOnly = new Set([
|
|
133
|
+
'status', 'tree', 'uia-tree', 'screenshot', 'logs', 'network', 'state', 'events',
|
|
134
|
+
'logcat', 'keyboard-state', 'wait-text', 'flutter-tree', 'flutter-nodes', 'h5-dom',
|
|
135
|
+
'flutter-h5-dom', 'webview-pages', 'webview-network', 'webview-console',
|
|
136
|
+
'ios-status', 'ios-tree', 'ios-logs', 'ios-network', 'ios-state', 'ios-events',
|
|
137
|
+
'ios-h5-dom', 'ios-flutter-tree', 'ios-flutter-nodes', 'ios-screenshot',
|
|
138
|
+
'ios-uia-tree', 'ios-wda-status', 'ios-devices', 'ios-doctor',
|
|
139
|
+
'web-provider-status', 'web-connect-info', 'web-sessions', 'web-status', 'web-dom',
|
|
140
|
+
'web-logs', 'web-network', 'web-state', 'web-events', 'web-wait',
|
|
141
|
+
]);
|
|
142
|
+
return !readOnly.has(normalized);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function eventCommandFor(command) {
|
|
146
|
+
const normalized = String(command || '');
|
|
147
|
+
if (normalized.startsWith('ios-')) return 'ios-events';
|
|
148
|
+
if (normalized.startsWith('web-')) return 'web-events';
|
|
149
|
+
return 'events';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function treeCommandFor(command) {
|
|
153
|
+
const normalized = String(command || '');
|
|
154
|
+
if (normalized.startsWith('ios-')) return 'ios-tree';
|
|
155
|
+
if (normalized.startsWith('web-')) return 'web-dom';
|
|
156
|
+
return 'tree';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function screenshotCommandFor(command) {
|
|
160
|
+
return String(command || '').startsWith('ios-') ? 'ios-screenshot' : 'screenshot';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function safeRun(runner, command, args) {
|
|
164
|
+
try {
|
|
165
|
+
return await runner(command, args);
|
|
166
|
+
} catch (_) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function latestId(items) {
|
|
172
|
+
if (!Array.isArray(items)) return 0;
|
|
173
|
+
return items.reduce((latest, item) => {
|
|
174
|
+
const id = Number(item?.id);
|
|
175
|
+
return Number.isFinite(id) ? Math.max(latest, id) : latest;
|
|
176
|
+
}, 0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isUiEvent(item) {
|
|
180
|
+
const category = String(item?.category || '').toLowerCase();
|
|
181
|
+
const name = String(item?.name || '').toLowerCase();
|
|
182
|
+
return category === 'ui' || category.startsWith('ui.') || name.startsWith('ui.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function uiEventKind(item) {
|
|
186
|
+
const name = String(item?.name || '').toLowerCase();
|
|
187
|
+
const data = item?.data && typeof item.data === 'object' ? item.data : {};
|
|
188
|
+
if (name === 'ui.stable' || name.endsWith('.stable')) return 'stable';
|
|
189
|
+
if (data.semanticChanged === true) return 'semantic';
|
|
190
|
+
if (data.semanticChanged === false && data.renderChanged === true) return 'render';
|
|
191
|
+
if (data.renderOnly === true) return 'render';
|
|
192
|
+
if (name.includes('animation') || name.includes('frame') || name.includes('render')) return 'render';
|
|
193
|
+
if (
|
|
194
|
+
name === 'ui.changed'
|
|
195
|
+
&& String(data.platform || '').toLowerCase() === 'flutter'
|
|
196
|
+
&& (Number(data.frameCount) > 0 || Number(data.batchFrames) > 0)
|
|
197
|
+
) {
|
|
198
|
+
return 'render';
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
name === 'ui.changed'
|
|
202
|
+
&& data.activeAnimations === true
|
|
203
|
+
&& Number(data.changedComponentCount || 0) === 0
|
|
204
|
+
) {
|
|
205
|
+
return 'render';
|
|
206
|
+
}
|
|
207
|
+
if (name === 'batch' || name === 'ui.batch') return uiBatchKind(data.events);
|
|
208
|
+
if (name.includes('interaction') || name.endsWith('.click') || name.endsWith('.tap')) {
|
|
209
|
+
return 'interaction';
|
|
210
|
+
}
|
|
211
|
+
return 'semantic';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function uiBatchKind(events) {
|
|
215
|
+
if (!Array.isArray(events) || events.length === 0) return 'semantic';
|
|
216
|
+
let interactionOnly = true;
|
|
217
|
+
for (const event of events) {
|
|
218
|
+
const type = String(event?.type || '').toLowerCase();
|
|
219
|
+
if (!type.startsWith('interaction.')) interactionOnly = false;
|
|
220
|
+
}
|
|
221
|
+
return interactionOnly ? 'interaction' : 'semantic';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function uiEventHasInteraction(item) {
|
|
225
|
+
const name = String(item?.name || '').toLowerCase();
|
|
226
|
+
if (name.includes('interaction') || name.endsWith('.click') || name.endsWith('.tap')) return true;
|
|
227
|
+
const events = item?.data?.events;
|
|
228
|
+
return Array.isArray(events) && events.some((event) => (
|
|
229
|
+
String(event?.type || '').toLowerCase().startsWith('interaction.')
|
|
230
|
+
));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function summarizeUiEvent(item) {
|
|
234
|
+
return {
|
|
235
|
+
id: item?.id,
|
|
236
|
+
name: item?.name,
|
|
237
|
+
category: item?.category,
|
|
238
|
+
timestampMs: item?.timestampMs,
|
|
239
|
+
data: boundedObject(item?.data),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function summarizeTree(tree) {
|
|
244
|
+
return pick(tree, [
|
|
245
|
+
'ok', 'error', 'activity', 'nodeCount', 'windowCount', 'targetId', 'sessionId',
|
|
246
|
+
'updatedAtMs',
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function summarizeScreenshot(screenshot) {
|
|
251
|
+
return pick(screenshot, [
|
|
252
|
+
'ok', 'error', 'path', 'outFile', 'format', 'width', 'height', 'deviceId',
|
|
253
|
+
'updatedAtMs',
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function pick(value, keys) {
|
|
258
|
+
if (!value || typeof value !== 'object') return { value: String(value) };
|
|
259
|
+
const result = {};
|
|
260
|
+
for (const key of keys) {
|
|
261
|
+
if (value[key] !== undefined) result[key] = value[key];
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function boundedObject(value) {
|
|
267
|
+
if (!value || typeof value !== 'object') return value ?? null;
|
|
268
|
+
const result = {};
|
|
269
|
+
for (const [key, child] of Object.entries(value).slice(0, 20)) {
|
|
270
|
+
if (typeof child === 'string') result[key] = child.slice(0, 300);
|
|
271
|
+
else if (child === null || typeof child !== 'object') result[key] = child;
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function delay(ms) {
|
|
277
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = {
|
|
281
|
+
eventCommandFor,
|
|
282
|
+
isMutationCommand,
|
|
283
|
+
runWithFeedbackProbe,
|
|
284
|
+
uiEventKind,
|
|
285
|
+
};
|