@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.
Files changed (156) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +306 -53
  4. package/bin/ai-app-bridge.js +51 -4504
  5. package/bin/android-permissions.js +152 -0
  6. package/bin/android-uia-xml.js +74 -0
  7. package/bin/artifact-paths.js +127 -1
  8. package/bin/bridge-forward.js +56 -0
  9. package/bin/command-discovery.js +86 -0
  10. package/bin/command-errors.js +60 -0
  11. package/bin/command-registry.js +504 -0
  12. package/bin/command-request.js +14 -0
  13. package/bin/command-router.js +80 -0
  14. package/bin/connection-cache.js +119 -0
  15. package/bin/device-provider.js +2934 -0
  16. package/bin/execution-host.js +596 -0
  17. package/bin/execution-runtime.js +101 -0
  18. package/bin/fact-codec.js +321 -0
  19. package/bin/fact-recorder.js +691 -0
  20. package/bin/fact-store.js +226 -0
  21. package/bin/feedback-probe.js +285 -0
  22. package/bin/intent/install-intent.js +267 -0
  23. package/bin/intent/intent-action-executor.js +129 -0
  24. package/bin/intent/intent-autonomous-adapter.js +46 -0
  25. package/bin/intent/intent-capture-port.js +64 -0
  26. package/bin/intent/intent-entry.js +226 -0
  27. package/bin/intent/intent-errors.js +28 -0
  28. package/bin/intent/intent-evidence-store.js +121 -0
  29. package/bin/intent/intent-lifetime.js +62 -0
  30. package/bin/intent/intent-observation-target.js +33 -0
  31. package/bin/intent/intent-observer.js +194 -0
  32. package/bin/intent/intent-production-adapter.js +334 -0
  33. package/bin/intent/intent-provider.js +27 -0
  34. package/bin/intent/intent-runtime.js +63 -0
  35. package/bin/intent/intent-worker.js +432 -0
  36. package/bin/intent/ios-intent-adapter.js +90 -0
  37. package/bin/intent/permission-intent.js +239 -0
  38. package/bin/intent/web-intent-adapter.js +31 -0
  39. package/bin/ios-device-outcome.js +47 -0
  40. package/bin/ios-execution.js +108 -0
  41. package/bin/ios-provider.js +497 -583
  42. package/bin/ios-runtime-binding.js +54 -0
  43. package/bin/ios-wda-execution.js +70 -0
  44. package/bin/ios-wda-port.js +98 -0
  45. package/bin/ios-wda-project.js +79 -0
  46. package/bin/mcp-server.js +83 -1040
  47. package/bin/mmap-scan-index.js +329 -0
  48. package/bin/observation-collector.js +862 -0
  49. package/bin/runtime-client.js +154 -0
  50. package/bin/runtime-directory.js +107 -0
  51. package/bin/runtime-protocol.js +36 -0
  52. package/bin/script/bounded-script-registry.js +103 -0
  53. package/bin/script/node-runtime-adapter.js +233 -0
  54. package/bin/script/progress-projector.js +63 -0
  55. package/bin/script/python-runtime-adapter.js +111 -0
  56. package/bin/script/rolling-summary.js +134 -0
  57. package/bin/script/script-agent-port.js +40 -0
  58. package/bin/script/script-assert.js +95 -0
  59. package/bin/script/script-capture-port.js +76 -0
  60. package/bin/script/script-catalog.js +85 -0
  61. package/bin/script/script-durable-restore.js +195 -0
  62. package/bin/script/script-entry-code.js +26 -0
  63. package/bin/script/script-entry-route.js +38 -0
  64. package/bin/script/script-entry.js +3 -0
  65. package/bin/script/script-errors.js +29 -0
  66. package/bin/script/script-evidence-store.js +22 -0
  67. package/bin/script/script-format-removed.js +26 -0
  68. package/bin/script/script-host-port.js +397 -0
  69. package/bin/script/script-ledger.js +64 -0
  70. package/bin/script/script-result.js +57 -0
  71. package/bin/script/script-sdk.js +152 -0
  72. package/bin/script/script-sdk.py +153 -0
  73. package/bin/script/script-session-channel.js +127 -0
  74. package/bin/script/script-spec.js +86 -0
  75. package/bin/script/script-supervisor.js +919 -0
  76. package/bin/script/templates/checkpoint-reentry.js +13 -0
  77. package/bin/segment-index.js +481 -0
  78. package/bin/segmented-fact-store.js +1571 -0
  79. package/bin/shared-kernel/android-h5-target.js +10 -0
  80. package/bin/shared-kernel/android-install-execution.js +176 -0
  81. package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
  82. package/bin/shared-kernel/android-shell-execution.js +195 -0
  83. package/bin/shared-kernel/argument-schema.js +117 -0
  84. package/bin/shared-kernel/canonical-path.js +17 -0
  85. package/bin/shared-kernel/device-acknowledgements.js +53 -0
  86. package/bin/shared-kernel/device-completion-history.js +52 -0
  87. package/bin/shared-kernel/device-mutation-lease.js +219 -0
  88. package/bin/shared-kernel/device-ownership-recovery.js +95 -0
  89. package/bin/shared-kernel/device-ownership-store.js +94 -0
  90. package/bin/shared-kernel/evidence-adapters.js +251 -0
  91. package/bin/shared-kernel/evidence-archive.js +329 -0
  92. package/bin/shared-kernel/evidence-recording.js +131 -0
  93. package/bin/shared-kernel/evidence-schema.js +194 -0
  94. package/bin/shared-kernel/evidence-store.js +190 -0
  95. package/bin/shared-kernel/execution-admission.js +22 -0
  96. package/bin/shared-kernel/execution-contracts.js +171 -0
  97. package/bin/shared-kernel/execution-io.js +106 -0
  98. package/bin/shared-kernel/execution-ledger.js +125 -0
  99. package/bin/shared-kernel/execution-scope.js +87 -0
  100. package/bin/shared-kernel/execution-target.js +115 -0
  101. package/bin/shared-kernel/flutter-execution.js +13 -0
  102. package/bin/shared-kernel/flutter-h5-port.js +60 -0
  103. package/bin/shared-kernel/flutter-h5-target.js +9 -0
  104. package/bin/shared-kernel/flutter-target.js +75 -0
  105. package/bin/shared-kernel/h5-execution.js +11 -0
  106. package/bin/shared-kernel/h5-target.js +31 -0
  107. package/bin/shared-kernel/host-fact-store.js +49 -0
  108. package/bin/shared-kernel/ios-h5-target.js +9 -0
  109. package/bin/shared-kernel/ios-native-target.js +71 -0
  110. package/bin/shared-kernel/live-capture-query.js +115 -0
  111. package/bin/shared-kernel/managed-sdk-execution.js +78 -0
  112. package/bin/shared-kernel/native-execution.js +13 -0
  113. package/bin/shared-kernel/native-target.js +156 -0
  114. package/bin/shared-kernel/provider-command-contracts.js +55 -0
  115. package/bin/shared-kernel/recorded-payload-archive.js +195 -0
  116. package/bin/shared-kernel/request-context.js +35 -0
  117. package/bin/shared-kernel/semantic-node.js +55 -0
  118. package/bin/shared-kernel/summary-transformer.js +352 -0
  119. package/bin/shared-kernel/target-lease-protocol.js +47 -0
  120. package/bin/shared-kernel/text-wait.js +111 -0
  121. package/bin/shared-kernel/uia-execution.js +96 -0
  122. package/bin/shared-kernel/uia-protocol.js +214 -0
  123. package/bin/shared-kernel/uia-runtime-port.js +377 -0
  124. package/bin/shared-kernel/uia-target.js +39 -0
  125. package/bin/shared-kernel/web-dom-target.js +44 -0
  126. package/bin/shared-kernel/xml-attributes.js +25 -0
  127. package/bin/target-execution.js +275 -0
  128. package/bin/web/command-schema.js +60 -0
  129. package/bin/web/session-store.js +157 -0
  130. package/bin/web-provider.js +334 -553
  131. package/docs/COMMAND_CONTRACT.md +1563 -0
  132. package/docs/EVIDENCE_ARCHIVE.md +214 -0
  133. package/docs/INTENT_FOREGROUND.md +71 -0
  134. package/docs/INTENT_NATIVE_EDITING.md +79 -0
  135. package/docs/RELEASE.md +59 -0
  136. package/docs/SCRIPT_AUTHORING.md +489 -0
  137. package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
  138. package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
  139. package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
  140. package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
  141. package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
  142. package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
  143. package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
  144. package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
  145. package/package.json +60 -5
  146. package/runtime/ios-wda/AABWDABinding.h +19 -0
  147. package/runtime/ios-wda/AABWDABinding.m +97 -0
  148. package/runtime/ios-wda/AABWDAExecution.h +26 -0
  149. package/runtime/ios-wda/AABWDAExecution.m +172 -0
  150. package/runtime/ios-wda/AABWDAIntegration.h +71 -0
  151. package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
  152. package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
  153. package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
  154. package/runtime/uia/ai-app-bridge-uia.jar +0 -0
  155. package/runtime/uia/manifest.json +22 -0
  156. package/skills/ai-app-bridge-use/SKILL.md +23 -360
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ const { checkExecution } = require('./execution-scope');
4
+
5
+ // The top non-hidden Android window owns all semantic selection. Unknown or
6
+ // disabled foreground roots block controls in earlier windows.
7
+
8
+ function visible(node) {
9
+ return node && (node.effectiveVisible === true || node.visible === true)
10
+ && node.effectiveVisible !== false && node.visible !== false && node.enabled !== false
11
+ && node.visibility !== 'gone' && node.visibility !== 'invisible' && node.alpha !== 0;
12
+ }
13
+
14
+ function validBounds(bounds) {
15
+ return bounds && ['left', 'top', 'right', 'bottom'].every((key) => Number.isFinite(bounds[key]))
16
+ && bounds.right > bounds.left && bounds.bottom > bounds.top;
17
+ }
18
+
19
+ function foregroundNativeWindow(rawTree) {
20
+ if (!rawTree || typeof rawTree !== 'object') return null;
21
+ const windows = Array.isArray(rawTree.windows) ? rawTree.windows : [];
22
+ if (windows.length) {
23
+ const index = windows.findLastIndex(item => !explicitlyHidden(item?.root));
24
+ if (index < 0) return null;
25
+ const window = windows[index];
26
+ return { index, root: window?.root, type: window?.type, windowId: window?.windowId,
27
+ bounds: window && Object.hasOwn(window, 'bounds') ? window.bounds : window?.root?.bounds };
28
+ }
29
+ return rawTree.root ? { index: null, root: rawTree.root, bounds: rawTree.root.bounds, type: 'activity' } : null;
30
+ }
31
+
32
+ function nativeWindow(rawTree) {
33
+ const window = foregroundNativeWindow(rawTree);
34
+ return window && visible(window.root) && validBounds(window.bounds) ? window : null;
35
+ }
36
+
37
+ function explicitlyHidden(node) {
38
+ return node && (node.effectiveVisible === false || node.visible === false
39
+ || node.visibility === 'gone' || node.visibility === 'invisible' || node.alpha === 0);
40
+ }
41
+
42
+ function selectNativeNode(rawTree, spec, editable) {
43
+ const reject = (error) => ({ ok: false, error, dispatched: false });
44
+ const window = nativeWindow(rawTree);
45
+ if (!window) return reject('visible_observed_window_required');
46
+ const selector = spec.selector || (typeof spec.text === 'string' ? { text: spec.text } : null);
47
+ if (spec.exact === false || (spec.selector && spec.text != null)) return reject('explicit_native_selector_required');
48
+ const keys = Object.keys(selector || {}).filter(key => key !== 'within');
49
+ if (keys.length !== 1 || !['resourceName', 'text', 'contentDescription'].includes(keys[0]) || typeof selector[keys[0]] !== 'string' || !selector[keys[0]]) return reject('explicit_native_selector_required');
50
+ const hasScope = Object.hasOwn(selector, 'within');
51
+ const scope = selector.within;
52
+ let anchorKey;
53
+ let ancestorKey;
54
+ let ancestorParentKey;
55
+ if (hasScope) {
56
+ if (!scope || typeof scope !== 'object' || Array.isArray(scope)) return reject('invalid_native_scope');
57
+ const anchorKeys = Object.keys(scope).filter(key => key !== 'ancestor');
58
+ if (anchorKeys.length !== 1 || !['resourceName', 'text', 'contentDescription'].includes(anchorKeys[0])
59
+ || typeof scope[anchorKeys[0]] !== 'string' || !scope[anchorKeys[0]]) return reject('invalid_native_scope');
60
+ const ancestor = scope.ancestor;
61
+ if (!ancestor || typeof ancestor !== 'object' || Array.isArray(ancestor)) return reject('invalid_native_scope');
62
+ const ancestorKeys = Object.keys(ancestor).filter(key => key !== 'parent');
63
+ if (ancestorKeys.length !== 1 || !['className', 'resourceName'].includes(ancestorKeys[0])
64
+ || typeof ancestor[ancestorKeys[0]] !== 'string' || !ancestor[ancestorKeys[0]]) return reject('invalid_native_scope');
65
+ if (Object.hasOwn(ancestor, 'parent')) {
66
+ const parent = ancestor.parent;
67
+ if (!parent || typeof parent !== 'object' || Array.isArray(parent)) return reject('invalid_native_scope');
68
+ const parentKeys = Object.keys(parent);
69
+ if (parentKeys.length !== 1 || !['className', 'resourceName'].includes(parentKeys[0])
70
+ || typeof parent[parentKeys[0]] !== 'string' || !parent[parentKeys[0]]) return reject('invalid_native_scope');
71
+ [ancestorParentKey] = parentKeys;
72
+ }
73
+ [anchorKey] = anchorKeys;
74
+ [ancestorKey] = ancestorKeys;
75
+ }
76
+ const eligible = [];
77
+ const visit = (node, ancestors = []) => {
78
+ if (!visible(node)) return;
79
+ const bounds = node.bounds;
80
+ if (validBounds(bounds)) {
81
+ const x = Math.round((bounds.left + bounds.right) / 2);
82
+ const y = Math.round((bounds.top + bounds.bottom) / 2);
83
+ const inViewport = x >= window.bounds.left && x < window.bounds.right && y >= window.bounds.top && y < window.bounds.bottom;
84
+ // effectiveVisible does not prove the chosen center survives RecyclerView clipping.
85
+ // Without clipChildren/clipToPadding geometry, conservatively require every ancestor.
86
+ const inAncestors = ancestors.every(ancestor => validBounds(ancestor.bounds) && pointInsideBounds({ x, y }, ancestor.bounds));
87
+ if (inViewport && inAncestors) eligible.push({ node, x, y, ancestors });
88
+ }
89
+ for (const child of node.children || []) visit(child, [...ancestors, node]);
90
+ };
91
+ visit(window.root);
92
+ let scopeRoot;
93
+ if (hasScope) {
94
+ const anchors = eligible.filter(item => item.node[anchorKey] === scope[anchorKey]);
95
+ if (anchors.length !== 1) return reject(anchors.length ? 'native_scope_anchor_ambiguous' : 'native_scope_anchor_not_found');
96
+ const anchor = anchors[0];
97
+ const ancestors = anchor.ancestors.filter((node, index) => visible(node) && validBounds(node.bounds)
98
+ && node[ancestorKey] === scope.ancestor[ancestorKey]
99
+ && (!ancestorParentKey || anchor.ancestors[index - 1]?.[ancestorParentKey] === scope.ancestor.parent[ancestorParentKey]));
100
+ // A named row type/resource must resolve once. Never choose a nearest arbitrary ancestor.
101
+ if (ancestors.length !== 1) return reject(ancestors.length ? 'native_scope_ancestor_ambiguous' : 'native_scope_ancestor_not_found');
102
+ [scopeRoot] = ancestors;
103
+ if (!pointInsideBounds(anchor, scopeRoot.bounds)) return reject('native_scope_anchor_out_of_bounds');
104
+ }
105
+ const matches = eligible.filter(item => item.node[keys[0]] === selector[keys[0]]
106
+ && (!scopeRoot || ((item.node === scopeRoot || item.ancestors.includes(scopeRoot)) && pointInsideBounds(item, scopeRoot.bounds))));
107
+ if (matches.length !== 1) return reject(matches.length ? 'native_selector_ambiguous' : 'native_selector_not_found');
108
+ const selected = matches[0];
109
+ if (editable && selected.node.editable !== true) return reject('native_target_not_editable');
110
+ return { ok: true, ...selected, windowBounds: window.bounds, window };
111
+ }
112
+
113
+ function nativeNodeIdentity(node) {
114
+ return Object.fromEntries(['className', 'resourceName', 'id', 'text', 'contentDescription', 'editable', 'targetRef']
115
+ .map(key => [key, node?.[key] ?? null]));
116
+ }
117
+
118
+ function nativeWindowIdentity(window) {
119
+ return { type: window?.type ?? null, index: window?.index ?? null, windowId: window?.windowId ?? null, root: nativeNodeIdentity(window?.root) };
120
+ }
121
+
122
+ function nativeTargetRequest(node, selector) {
123
+ const ref = node?.targetRef;
124
+ const reject = error => ({ ok: false, error, dispatched: false, ambiguous: false,
125
+ message: 'Native semantic actions require a current SDK target reference. Update the app SDK and observe again.' });
126
+ if (!ref || ref.schemaVersion !== 'aab.native-target/v1') return reject('native_atomic_target_unavailable');
127
+ const fields = ['schemaVersion', 'runtimeEpoch', 'windowId', 'viewId', 'guard'];
128
+ if (Object.keys(ref).length !== fields.length || fields.some(key => typeof ref[key] !== 'string' || !ref[key])
129
+ || !/^[a-f0-9]{64}$/.test(ref.guard)) return reject('invalid_native_target_reference');
130
+ return { ok: true, request: { selector: structuredClone(selector), targetRef: { ...ref } } };
131
+ }
132
+
133
+ function nativeSelectionIdentity(selection) {
134
+ return { window: nativeWindowIdentity(selection.window), node: nativeNodeIdentity(selection.node),
135
+ ancestors: selection.ancestors.map(nativeNodeIdentity) };
136
+ }
137
+
138
+ async function revalidateNativeNode(readTree, rawTree, spec, editable = false) {
139
+ const observed = selectNativeNode(rawTree, spec, editable);
140
+ if (!observed.ok) return observed;
141
+ checkExecution();
142
+ const current = selectNativeNode(await readTree(), spec, editable);
143
+ checkExecution();
144
+ if (!current.ok) return current;
145
+ if (JSON.stringify(nativeSelectionIdentity(observed)) !== JSON.stringify(nativeSelectionIdentity(current))) {
146
+ return { ok: false, error: 'reobserve_required', dispatched: false };
147
+ }
148
+ return current;
149
+ }
150
+
151
+ function pointInsideBounds(point, bounds) {
152
+ return point.x >= bounds.left && point.x < bounds.right && point.y >= bounds.top && point.y < bounds.bottom;
153
+ }
154
+
155
+ module.exports = { foregroundNativeWindow, nativeWindow, visible, explicitlyHidden, validBounds, selectNativeNode,
156
+ pointInsideBounds, nativeNodeIdentity, nativeWindowIdentity, revalidateNativeNode, nativeTargetRequest };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const { object, integer, text, milliseconds } = require('./argument-schema');
4
+ const { variants, exactlyOne, jsonObject, flutterSelector } = require('./execution-contracts');
5
+
6
+ const coordinate = { type: 'number', minimum: 0, maximum: 2147483647 };
7
+ const delta = { type: 'number', minimum: -2147483647, maximum: 2147483647 };
8
+ const pairedCoordinates = { anyOf: [{ required: ['x', 'y'] }, { not: { anyOf: [{ required: ['x'] }, { required: ['y'] }] } }] };
9
+
10
+ function flutterActionSchema() {
11
+ const h5 = require('./flutter-h5-target');
12
+ const action = (name, properties = {}, required = []) => object({ action: { const: name }, ...properties }, ['action', ...required]);
13
+ return variants('action', [
14
+ action('tapAt', { x: coordinate, y: coordinate }, ['x', 'y']),
15
+ action('tapText', { text }, ['text']),
16
+ action('tapTarget', { selector: flutterSelector }, ['selector']),
17
+ { ...action('inputText', { text: { type: 'string' }, selector: flutterSelector, x: coordinate, y: coordinate }, ['text']), allOf: [pairedCoordinates,
18
+ { not: { required: ['selector'], anyOf: [{ required: ['x'] }, { required: ['y'] }] } }] },
19
+ action('swipe', { startX: coordinate, startY: coordinate, endX: coordinate, endY: coordinate }, ['startX', 'startY', 'endX', 'endY']),
20
+ action('scrollBy', { delta, selector: flutterSelector }, ['delta']),
21
+ action('scrollUntilText', { text, selector: flutterSelector, maxSwipes: { ...integer(0, 1000), default: 12 } }, ['text']),
22
+ ...['hideKeyboard', 'back', 'openHarness', 'h5Adapters'].map(name => action(name)),
23
+ action('h5Dom', { adapterId: text }),
24
+ action('h5Eval', { script: text, expectedPage: h5.pageSchema }, ['script', 'expectedPage']),
25
+ { ...action('h5Control', { operation: { enum: ['click', 'input', 'scroll', 'scrollBy'] },
26
+ expectedTarget: h5.expectedTargetSchema, expectedPage: h5.pageSchema,
27
+ text: { type: 'string', maxLength: 16384 }, deltaX: delta, deltaY: delta }, ['operation']), oneOf: [
28
+ { properties: { operation: { enum: ['click', 'scroll'] }, expectedPage: false, text: false, deltaX: false, deltaY: false }, required: ['expectedTarget'] },
29
+ { properties: { operation: { const: 'input' }, expectedPage: false, deltaX: false, deltaY: false }, required: ['expectedTarget', 'text'] },
30
+ { properties: { operation: { const: 'scrollBy' }, expectedTarget: false, text: false }, required: ['expectedPage', 'deltaX', 'deltaY'],
31
+ not: { properties: { deltaX: { const: 0 }, deltaY: { const: 0 } } } },
32
+ ] },
33
+ ]);
34
+ }
35
+
36
+ function webCommandSchema(common) {
37
+ const command = (name, args, required = []) => object({ ...common, name: { const: name }, arguments: args }, ['sessionId', 'name', ...required]);
38
+ const { selectorSchema, expectedTargetSchema } = require('./web-dom-target');
39
+ const selector = { selector: selectorSchema, expectedTarget: expectedTargetSchema };
40
+ return variants('name', [
41
+ command('domSnapshot', object({ selector: text, maxControls: integer(1, 1000) })),
42
+ command('click', object(selector, ['selector']), ['arguments']),
43
+ command('input', object({ ...selector, value: { type: 'string', maxLength: 16384 } }, ['selector', 'value']), ['arguments']),
44
+ command('key', object({ ...selector, key: { enum: ['Enter', 'Escape'] } }, ['selector', 'key']), ['arguments']),
45
+ command('waitFor', exactlyOne({ selector: selectorSchema, targetText: text, timeoutMs: { ...milliseconds, default: 5000 }, intervalMs: { ...milliseconds, default: 250 } }, ['selector', 'targetText']), ['arguments']),
46
+ command('scroll', { ...object({ ...selector, mode: { enum: ['into-view', 'by'] }, deltaX: delta, deltaY: delta }, ['mode']),
47
+ oneOf: [{ properties: { mode: { const: 'into-view' }, deltaX: false, deltaY: false }, required: ['selector'] },
48
+ { properties: { mode: { const: 'by' } }, anyOf: [{ required: ['deltaX'] }, { required: ['deltaY'] }] }],
49
+ allOf: [{ if: { required: ['expectedTarget'] }, then: { required: ['selector'] } }] }, ['arguments']),
50
+ command('state', object({})),
51
+ command('action', object({ name: text, arguments: { ...jsonObject, description: 'Arguments for the explicitly named registered App action.' } }, ['name', 'arguments']), ['arguments']),
52
+ ]);
53
+ }
54
+
55
+ module.exports = { flutterActionSchema, webCommandSchema };
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const { canonicalJson, checksumOf } = require('./evidence-schema');
4
+ const { capturePageMetadata } = require('./live-capture-query');
5
+ const { requireValue, sha256, jsonHash, checkPng, PAYLOAD_SCHEMA, MAX_FILE_BYTES,
6
+ MAX_RECORDING_BYTES } = require('./evidence-recording');
7
+
8
+ const same = (a, b) => canonicalJson(a) === canonicalJson(b);
9
+ const portableName = (file, extension) => `payload-${file.sha256}.${extension}`;
10
+
11
+ // The same analysis runs while exporting and offline. readFile chooses only the
12
+ // source location; no payload identities or coverage claims come from a manifest.
13
+ function analyzeRecordedPayloads(facts, readFile) {
14
+ const records = facts.map(f => f.payload);
15
+ const byId = new Map(records.map(r => [r.evidenceId, r]));
16
+ const positions = new Map(facts.map(f => [f.payload.evidenceId, f.globalSeq]));
17
+ const calls = new Map();
18
+ const callIds = new Set();
19
+ const sequences = new Set();
20
+ const files = new Map();
21
+ const attachments = [];
22
+ const missingReferences = [];
23
+ const counts = { scriptCalls: 0, assertions: { passed: 0, failed: 0, inconclusive: 0 },
24
+ screenshots: 0, mobilePages: 0, mobileItems: 0, boundMobileItems: 0,
25
+ webPages: 0, webItems: 0, boundWebItems: 0, redactedPayloads: 0 };
26
+ let totalBytes = 0;
27
+ function read(record, file, extension) {
28
+ requireValue(file && /^[a-f0-9]{64}$/.test(file.sha256)
29
+ && Number.isSafeInteger(file.bytes) && file.bytes > 0 && file.bytes <= MAX_FILE_BYTES, 'invalid_attachment_file');
30
+ const name = portableName(file, extension);
31
+ const bytes = readFile(record, file, name);
32
+ requireValue(bytes.length === file.bytes && sha256(bytes) === file.sha256, 'attachment_checksum_mismatch', record.evidenceId);
33
+ if (!files.has(name)) {
34
+ totalBytes += bytes.length;
35
+ requireValue(totalBytes <= MAX_RECORDING_BYTES, 'archive_limit_exceeded');
36
+ files.set(name, { path: name, bytes: file.bytes, sha256: file.sha256 });
37
+ }
38
+ return bytes;
39
+ }
40
+ for (const record of records) {
41
+ if (record.kind !== 'attachment') continue;
42
+ requireValue(Number.isSafeInteger(record.sequence) && record.sequence > 0
43
+ && record.file.name === `${record.sequence}.json`, 'invalid_attachment_sequence');
44
+ const key = `${record.recordingId}:${record.sequence}`;
45
+ requireValue(!sequences.has(key), 'duplicate_attachment');
46
+ sequences.add(key);
47
+ const doc = JSON.parse(read(record, record.file, 'json'));
48
+ requireValue(doc.schemaVersion === PAYLOAD_SCHEMA && doc.namespace === record.namespace
49
+ && doc.operationId === record.operationId && doc.recordingId === record.recordingId
50
+ && doc.sequence === record.sequence && doc.kind === record.payloadKind
51
+ && same(doc.target, record.target), 'attachment_binding_mismatch', record.evidenceId);
52
+ requireValue(jsonHash(doc.data) === record.dataSha256 && /^[a-f0-9]{64}$/.test(record.originalSha256),
53
+ 'attachment_data_mismatch');
54
+ requireValue(record.representation === (record.originalSha256 === record.dataSha256 ? 'original-json' : 'redacted-json'),
55
+ 'attachment_representation_mismatch');
56
+ if (record.representation === 'redacted-json') counts.redactedPayloads += 1;
57
+ requireValue(Array.isArray(record.screenshots) && Array.isArray(doc.screenshots)
58
+ && record.screenshots.length === doc.screenshots.length, 'invalid_screenshot_reference');
59
+ const screenshotFiles = [];
60
+ for (let i = 0; i < record.screenshots.length; i += 1) {
61
+ const file = record.screenshots[i];
62
+ const { ref, ...metadata } = doc.screenshots[i];
63
+ requireValue(file.name === `${record.sequence}-${i + 1}.png` && same(file, metadata)
64
+ && ref?.sha256 === file.sha256, 'invalid_screenshot_reference');
65
+ checkPng(read(record, file, 'png'));
66
+ screenshotFiles.push(portableName(file, 'png'));
67
+ counts.screenshots += 1;
68
+ }
69
+ attachments.push({ evidenceId: record.evidenceId, path: portableName(record.file, 'json'), screenshots: screenshotFiles });
70
+ if (doc.kind === 'script-call') {
71
+ requireValue(record.namespace === 'script', 'attachment_binding_mismatch');
72
+ const { command, args, envelope } = doc.data;
73
+ requireValue(envelope?.command === command && (envelope.execution?.executionId === record.operationId
74
+ || (envelope.ok === false && envelope.execution?.executionId === undefined)), 'call_binding_mismatch');
75
+ if (envelope.execution?.callId) {
76
+ requireValue(!callIds.has(envelope.execution.callId), 'duplicate_call_id');
77
+ callIds.add(envelope.execution.callId);
78
+ }
79
+ const evidence = envelope.evidence;
80
+ if (evidence?.source) {
81
+ requireValue(evidence.source.command === command && evidence.source.scope === 'execution'
82
+ && evidence.source.payloadSha256 === record.sourcePayload?.originalSha256
83
+ && jsonHash(envelope.result) === record.sourcePayload?.archivedSha256, 'call_source_mismatch');
84
+ }
85
+ if (evidence?.observationId) {
86
+ requireValue(!calls.has(evidence.observationId), 'duplicate_observation_id');
87
+ calls.set(evidence.observationId, evidence);
88
+ }
89
+ const screenshotRefs = (evidence?.refs || []).filter(ref => ref.stream === 'screenshot');
90
+ requireValue(same(screenshotRefs, doc.screenshots.map(s => s.ref)), 'invalid_screenshot_reference');
91
+ const actionId = envelope.execution?.actionId;
92
+ if (actionId) {
93
+ const marker = records.find(r => r.kind === 'dispatch-marker' && r.actionId === actionId);
94
+ if (!marker) missingReferences.push({ evidenceId: record.evidenceId, field: 'actionId', value: actionId });
95
+ else requireValue(positions.get(marker.evidenceId) < positions.get(record.evidenceId)
96
+ && same(marker.target, record.target) && (record.representation === 'redacted-json'
97
+ || marker.actionSpecHash === checksumOf({ command, args })), 'call_action_mismatch');
98
+ }
99
+ if (evidence?.capture) {
100
+ analyzeCapturePage({ ...evidence.capture, stream: command.replace(/^web-/, ''),
101
+ window: evidence.window, coverage: evidence.coverage,
102
+ refs: evidence.refs, items: envelope.result.items }, counts, doc.target);
103
+ }
104
+ counts.scriptCalls += 1;
105
+ } else if (doc.kind === 'script-assertion') {
106
+ requireValue(record.namespace === 'script' && doc.screenshots.length === 0, 'attachment_binding_mismatch');
107
+ const { assertion, result } = doc.data;
108
+ requireValue(['passed', 'failed', 'inconclusive'].includes(result?.verdict), 'invalid_recorded_assertion');
109
+ counts.assertions[result.verdict] += 1;
110
+ // Preserve rejected assertion attempts too. A child-supplied predicate is
111
+ // a claim; this verifier checks its Host evidence association, not business truth.
112
+ if (result.scope === 'device' && result.verdict !== 'inconclusive') {
113
+ const id = assertion.evidence?.observationId;
114
+ const evidence = calls.get(id);
115
+ if (!evidence) missingReferences.push({ evidenceId: record.evidenceId, field: 'observationId', value: id ?? null });
116
+ else for (const field of ['observationId', 'source', 'window', 'coverage', 'refs', 'capture']) {
117
+ requireValue(same(assertion.evidence[field], evidence[field]), 'assertion_binding_mismatch', field);
118
+ }
119
+ }
120
+ } else if (doc.kind === 'intent-capture') {
121
+ requireValue(record.namespace === 'intent' && doc.screenshots.length === 0, 'attachment_binding_mismatch');
122
+ const { capture, rawTreeId } = doc.data;
123
+ requireValue(Array.isArray(capture?.pages), 'invalid_recorded_capture');
124
+ const observation = byId.get(record.parentFactId);
125
+ if (observation) {
126
+ requireValue(observation.kind === 'observation' && observation.rawTreeId === rawTreeId
127
+ && observation.revision === record.revision && same(observation.captureRefs, capture.refs)
128
+ && same(observation.captureCoverage, capture.coverage), 'capture_observation_mismatch');
129
+ requireValue(same(observation.capturePages, capture.pages.map(capturePageMetadata)), 'capture_observation_mismatch');
130
+ }
131
+ requireValue(same(capture.refs, capture.pages.flatMap(p => p.refs))
132
+ && same(capture.items, capture.pages.flatMap(p => p.items)), 'capture_page_mismatch');
133
+ for (const page of capture.pages) analyzeCapturePage(page, counts, doc.target);
134
+ } else requireValue(false, 'unsupported_recorded_payload');
135
+ }
136
+ return { scope: 'retained-recorded-payloads', priorHistoryComplete: 'unknown',
137
+ executionStatus: 'not-inferred', businessVerdict: 'not-evaluated',
138
+ referenceClosure: missingReferences.length ? 'partial' : 'complete', missingReferences,
139
+ attachmentCount: attachments.length, bytes: totalBytes, counts, attachments, files: [...files.values()],
140
+ exclusions: ['unrecorded-calls-and-screenshots', 'unqueried-mobile-facts', 'in-memory-events'] };
141
+ }
142
+
143
+ function analyzeCapturePage(page, counts, target) {
144
+ requireValue(Array.isArray(page.items) && Array.isArray(page.refs)
145
+ && ['complete', 'partial', 'unavailable'].includes(page.coverage?.status), 'invalid_recorded_capture');
146
+ if (target.platform === 'web') return analyzeWebPage(page, counts, target);
147
+ counts.mobilePages += 1;
148
+ counts.mobileItems += page.items.length;
149
+ // Uncommitted/memory-only pages remain inspectable without strong fact refs.
150
+ if (page.coverage.committed !== true) return;
151
+ requireValue(page.refs.length === page.items.length, 'capture_ref_count_mismatch');
152
+ const ids = new Set();
153
+ for (let i = 0; i < page.refs.length; i += 1) {
154
+ const ref = page.refs[i];
155
+ const item = page.items[i];
156
+ requireValue(typeof ref.mobileFactId === 'string' && ref.mobileFactId.length > 0 && !ids.has(ref.mobileFactId)
157
+ && typeof ref.runtimeEpoch === 'string' && ref.runtimeEpoch.length > 0
158
+ && ref.targetKey === page.targetKey && ref.captureId === item.id && ref.capturedAtMs === item.timestampMs
159
+ && ({ logs: 'log', network: 'network', state: 'state', events: 'event' })[ref.stream] === item.type,
160
+ 'capture_ref_binding_mismatch');
161
+ // Connected history may include older epochs; its page epoch is the current
162
+ // runtime. Only an explicitly filtered window restricts each fact's epoch.
163
+ if (page.window?.runtimeEpoch != null) requireValue(ref.runtimeEpoch === page.window.runtimeEpoch, 'capture_epoch_mismatch');
164
+ ids.add(ref.mobileFactId);
165
+ counts.boundMobileItems += 1;
166
+ }
167
+ }
168
+
169
+ function analyzeWebPage(page, counts, target) {
170
+ counts.webPages += 1; counts.webItems += page.items.length;
171
+ if (page.coverage.committed !== true) return;
172
+ requireValue(['logs', 'network', 'state', 'events'].includes(page.stream), 'invalid_web_capture_stream');
173
+ const targetKey = `web:${JSON.stringify([target.sessionId, target.targetId, page.stream])}`;
174
+ requireValue(page.targetKey === targetKey && page.runtimeEpoch === target.runtimeEpoch
175
+ && page.refs.length === page.items.length, 'web_capture_target_mismatch');
176
+ const ids = new Set();
177
+ for (let index = 0; index < page.items.length; index++) {
178
+ const item = page.items[index], ref = page.refs[index];
179
+ requireValue(ref.source === 'host-fact-store' && ref.stream === page.stream
180
+ && ref.targetKey === targetKey && ref.runtimeEpoch === target.runtimeEpoch
181
+ && Number.isSafeInteger(ref.globalSeq) && ref.globalSeq > 0 && !ids.has(ref.globalSeq)
182
+ && ref.globalSeq === item.id && same(ref, item.ref)
183
+ && item.sessionId === target.sessionId && item.targetId === target.targetId && item.runtimeEpoch === target.runtimeEpoch
184
+ && typeof item.captureId === 'string' && item.captureId.length > 0
185
+ && Number.isSafeInteger(item.sourceSequence) && item.sourceSequence > 0,
186
+ 'web_capture_ref_binding_mismatch');
187
+ requireValue(item.association === 'unattributed' ? item.actionId === null
188
+ : ['explicit', 'synchronous'].includes(item.association) && typeof item.actionId === 'string' && item.actionId.length > 0,
189
+ 'web_capture_action_binding_mismatch');
190
+ if (page.window?.afterActionId != null) requireValue(item.actionId === page.window.afterActionId, 'web_capture_action_binding_mismatch');
191
+ ids.add(ref.globalSeq); counts.boundWebItems += 1;
192
+ }
193
+ }
194
+
195
+ module.exports = { analyzeRecordedPayloads };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { AsyncLocalStorage } = require('node:async_hooks');
5
+ const context = new AsyncLocalStorage();
6
+
7
+ const requestDirectory = () => context.getStore()?.cwd ?? process.cwd();
8
+ const resolveRequestPath = value => path.resolve(requestDirectory(), value);
9
+ const inRequestDirectory = (cwd, action) => context.run({ cwd }, action);
10
+ const localPaths = ['outFile', 'artifactDir', 'apkPath', 'appPath', 'recordingDir', 'outputDir', 'archiveDir', 'wdaProjectPath', 'agentModule'];
11
+ const executables = ['adb', 'aaptPath', 'apksignerPath', 'devicectl', 'xcodebuild', 'pythonPath'];
12
+
13
+ // Only contract-defined filesystem fields are resolved. Web URL paths, source
14
+ // text and arbitrary Script inputs keep their original values.
15
+ function resolveExecutablePaths(args) {
16
+ const resolved = { ...args };
17
+ for (const field of executables) {
18
+ if (typeof resolved[field] === 'string' && /[\\/]/.test(resolved[field])) resolved[field] = resolveRequestPath(resolved[field]);
19
+ }
20
+ return resolved;
21
+ }
22
+
23
+ function resolveCommandPaths(command, args) {
24
+ const resolved = resolveExecutablePaths(args);
25
+ for (const field of localPaths) {
26
+ if (typeof resolved[field] === 'string') resolved[field] = resolveRequestPath(resolved[field]);
27
+ }
28
+ if (command === 'script' && args.operation === 'start') {
29
+ resolved.script = { ...args.script, cwd: resolveRequestPath(args.script.cwd ?? requestDirectory()) };
30
+ if (args.script.sourcePath !== undefined) resolved.script.sourcePath = resolveRequestPath(args.script.sourcePath);
31
+ }
32
+ return resolved;
33
+ }
34
+
35
+ module.exports = { requestDirectory, resolveRequestPath, inRequestDirectory, resolveCommandPaths, resolveExecutablePaths };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ function semanticNode({
4
+ nodeId,
5
+ sourceIndex,
6
+ rawTreeId,
7
+ screenshotId = null,
8
+ role,
9
+ text = null,
10
+ label = null,
11
+ hint,
12
+ errorText,
13
+ bounds = null,
14
+ enabled = true,
15
+ checked = null,
16
+ selected = null,
17
+ clickable = false,
18
+ resourceName,
19
+ editable,
20
+ interaction,
21
+ visible,
22
+ effectiveVisible,
23
+ accessibilityId,
24
+ elementId,
25
+ elementType,
26
+ targetRef,
27
+ }) {
28
+ return {
29
+ nodeId: String(nodeId),
30
+ sourceIndex,
31
+ rawTreeId,
32
+ screenshotId,
33
+ role,
34
+ text,
35
+ label,
36
+ bounds,
37
+ enabled,
38
+ checked,
39
+ selected,
40
+ clickable,
41
+ ...(typeof hint === 'string' ? { hint } : {}),
42
+ ...(typeof errorText === 'string' ? { errorText } : {}),
43
+ ...(typeof resourceName === 'string' ? { resourceName } : {}),
44
+ ...(typeof editable === 'boolean' ? { editable } : {}),
45
+ ...(interaction === undefined ? {} : { interaction }),
46
+ ...(typeof visible === 'boolean' ? { visible } : {}),
47
+ ...(typeof effectiveVisible === 'boolean' ? { effectiveVisible } : {}),
48
+ ...(typeof accessibilityId === 'string' ? { accessibilityId } : {}),
49
+ ...(typeof elementId === 'string' ? { elementId } : {}),
50
+ ...(typeof elementType === 'string' ? { elementType } : {}),
51
+ ...(targetRef === undefined ? {} : { targetRef }),
52
+ };
53
+ }
54
+
55
+ module.exports = { semanticNode };