swipium 1.5.0 → 2.0.1

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 (498) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/README.md +173 -280
  3. package/THREAT_MODEL.md +223 -47
  4. package/dist/appMap/automationLink.js +30 -27
  5. package/dist/appMap/automationLink.js.map +1 -1
  6. package/dist/appMap/build.js +47 -21
  7. package/dist/appMap/build.js.map +1 -1
  8. package/dist/appMap/codeIndex.js +2 -2
  9. package/dist/appMap/codeIndex.js.map +1 -1
  10. package/dist/appMap/featureIndex.js +3 -3
  11. package/dist/appMap/featureIndex.js.map +1 -1
  12. package/dist/appMap/featureModel.js +2 -2
  13. package/dist/appMap/featureModel.js.map +1 -1
  14. package/dist/appMap/firstRunApply.js +2 -2
  15. package/dist/appMap/firstRunApply.js.map +1 -1
  16. package/dist/appMap/issues.js +10 -10
  17. package/dist/appMap/issues.js.map +1 -1
  18. package/dist/appMap/migrations.js +5 -5
  19. package/dist/appMap/migrations.js.map +1 -1
  20. package/dist/appMap/prelaunch.js +2 -2
  21. package/dist/appMap/prelaunch.js.map +1 -1
  22. package/dist/appMap/projectRegistry.js +77 -11
  23. package/dist/appMap/projectRegistry.js.map +1 -1
  24. package/dist/appMap/provenance.js +2 -2
  25. package/dist/appMap/provenance.js.map +1 -1
  26. package/dist/appMap/query.js +2 -2
  27. package/dist/appMap/query.js.map +1 -1
  28. package/dist/appMap/runtimeMerge.js +3 -3
  29. package/dist/appMap/runtimeMerge.js.map +1 -1
  30. package/dist/appMap/schema.js +1 -1
  31. package/dist/appMap/schema.js.map +1 -1
  32. package/dist/appMap/screenMatch.js +1 -1
  33. package/dist/appMap/screenMatch.js.map +1 -1
  34. package/dist/appMap/staticScan.js +17 -17
  35. package/dist/appMap/staticScan.js.map +1 -1
  36. package/dist/appMap/store.js +55 -7
  37. package/dist/appMap/store.js.map +1 -1
  38. package/dist/appMap/tsAstScan.js +3 -3
  39. package/dist/appMap/tsAstScan.js.map +1 -1
  40. package/dist/artifacts/bundletool.js +5 -5
  41. package/dist/artifacts/bundletool.js.map +1 -1
  42. package/dist/artifacts/resolve.js +10 -10
  43. package/dist/artifacts/resolve.js.map +1 -1
  44. package/dist/automation/capabilities.js +3 -3
  45. package/dist/automation/capabilities.js.map +1 -1
  46. package/dist/automation/gestures.js +1 -1
  47. package/dist/automation/gestures.js.map +1 -1
  48. package/dist/automation/plan.js +7 -7
  49. package/dist/automation/plan.js.map +1 -1
  50. package/dist/automation/report.js +3 -3
  51. package/dist/automation/report.js.map +1 -1
  52. package/dist/automation/selectors.js +14 -25
  53. package/dist/automation/selectors.js.map +1 -1
  54. package/dist/automation/types.js +1 -1
  55. package/dist/automation/types.js.map +1 -1
  56. package/dist/automation/waits.js +5 -5
  57. package/dist/automation/waits.js.map +1 -1
  58. package/dist/automation/webview.js +2 -2
  59. package/dist/automation/webview.js.map +1 -1
  60. package/dist/automationGen/appiumModel.js +13 -7
  61. package/dist/automationGen/appiumModel.js.map +1 -1
  62. package/dist/automationGen/ciEmitter.js +5 -5
  63. package/dist/automationGen/ciEmitter.js.map +1 -1
  64. package/dist/automationGen/identifiers.js +224 -0
  65. package/dist/automationGen/identifiers.js.map +1 -0
  66. package/dist/automationGen/jsEmitter.js +265 -106
  67. package/dist/automationGen/jsEmitter.js.map +1 -1
  68. package/dist/automationGen/packagePatch.js +2 -2
  69. package/dist/automationGen/packagePatch.js.map +1 -1
  70. package/dist/automationGen/platformResolve.js +36 -0
  71. package/dist/automationGen/platformResolve.js.map +1 -0
  72. package/dist/automationGen/projectProfile.js +71 -26
  73. package/dist/automationGen/projectProfile.js.map +1 -1
  74. package/dist/automationGen/pythonEmitter.js +276 -105
  75. package/dist/automationGen/pythonEmitter.js.map +1 -1
  76. package/dist/automationGen/readmeEmitter.js +7 -6
  77. package/dist/automationGen/readmeEmitter.js.map +1 -1
  78. package/dist/{tools/automationGenerate.js → automationGen/run.js} +72 -113
  79. package/dist/automationGen/run.js.map +1 -0
  80. package/dist/automationGen/suitePlan.js +6 -4
  81. package/dist/automationGen/suitePlan.js.map +1 -1
  82. package/dist/automationGen/validation.js +20 -8
  83. package/dist/automationGen/validation.js.map +1 -1
  84. package/dist/build/parseBuildLog.js +2 -2
  85. package/dist/build/parseBuildLog.js.map +1 -1
  86. package/dist/build/plan.js +7 -7
  87. package/dist/build/plan.js.map +1 -1
  88. package/dist/ci/preflight.js +13 -4
  89. package/dist/ci/preflight.js.map +1 -1
  90. package/dist/cli/gc.js +74 -0
  91. package/dist/cli/gc.js.map +1 -0
  92. package/dist/cli/init.js +217 -26
  93. package/dist/cli/init.js.map +1 -1
  94. package/dist/cli/main.js +50 -0
  95. package/dist/cli/main.js.map +1 -0
  96. package/dist/cli/report.js +218 -0
  97. package/dist/cli/report.js.map +1 -0
  98. package/dist/cli/scan.js +35 -14
  99. package/dist/cli/scan.js.map +1 -1
  100. package/dist/cli/suite.js +6 -6
  101. package/dist/cli/suite.js.map +1 -1
  102. package/dist/cli/verify.js +7 -7
  103. package/dist/cli/verify.js.map +1 -1
  104. package/dist/consent/consent.js +162 -5
  105. package/dist/consent/consent.js.map +1 -1
  106. package/dist/context/detect.js +45 -14
  107. package/dist/context/detect.js.map +1 -1
  108. package/dist/context/findApps.js +6 -6
  109. package/dist/context/findApps.js.map +1 -1
  110. package/dist/context/projectRoot.js +174 -19
  111. package/dist/context/projectRoot.js.map +1 -1
  112. package/dist/context/scan.js +1 -1
  113. package/dist/context/scan.js.map +1 -1
  114. package/dist/core/capabilityGroups.js +90 -0
  115. package/dist/core/capabilityGroups.js.map +1 -0
  116. package/dist/core/target.js +3 -3
  117. package/dist/core/target.js.map +1 -1
  118. package/dist/core/targetPlan.js +65 -62
  119. package/dist/core/targetPlan.js.map +1 -1
  120. package/dist/drivers/DirectDriver.js +300 -36
  121. package/dist/drivers/DirectDriver.js.map +1 -1
  122. package/dist/drivers/SimctlDriver.js +120 -7
  123. package/dist/drivers/SimctlDriver.js.map +1 -1
  124. package/dist/drivers/WdaDriver.js +350 -50
  125. package/dist/drivers/WdaDriver.js.map +1 -1
  126. package/dist/explore/candidates.js +2 -2
  127. package/dist/explore/candidates.js.map +1 -1
  128. package/dist/explore/graph.js +5 -5
  129. package/dist/explore/graph.js.map +1 -1
  130. package/dist/explore/policy.js +5 -5
  131. package/dist/explore/policy.js.map +1 -1
  132. package/dist/explore/runner.js +301 -284
  133. package/dist/explore/runner.js.map +1 -1
  134. package/dist/explore/signatures.js +1 -1
  135. package/dist/featureTesting/executionBootstrap.js +29 -30
  136. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  137. package/dist/featureTesting/featureMap.js +2 -2
  138. package/dist/featureTesting/featureMap.js.map +1 -1
  139. package/dist/featureTesting/featureScope.js +12 -12
  140. package/dist/featureTesting/featureScope.js.map +1 -1
  141. package/dist/featureTesting/mapFeatureScope.js +7 -7
  142. package/dist/featureTesting/mapFeatureScope.js.map +1 -1
  143. package/dist/featureTesting/objectiveModel.js +10 -10
  144. package/dist/featureTesting/objectiveModel.js.map +1 -1
  145. package/dist/featureTesting/resultMerge.js +6 -6
  146. package/dist/featureTesting/resultMerge.js.map +1 -1
  147. package/dist/featureTesting/sources.js +4 -4
  148. package/dist/featureTesting/sources.js.map +1 -1
  149. package/dist/featureTesting/suiteBridge.js +1 -1
  150. package/dist/featureTesting/suiteBridge.js.map +1 -1
  151. package/dist/featureTesting/synonyms.js +1 -1
  152. package/dist/featureTesting/synonyms.js.map +1 -1
  153. package/dist/featureTesting/testCaseFactory.js +11 -11
  154. package/dist/featureTesting/testCaseFactory.js.map +1 -1
  155. package/dist/featureTesting/testPlan.js +3 -3
  156. package/dist/featureTesting/testPlan.js.map +1 -1
  157. package/dist/firstRun/authStateMachine.js +1 -1
  158. package/dist/firstRun/authStateMachine.js.map +1 -1
  159. package/dist/firstRun/classifyScreen.js +5 -5
  160. package/dist/firstRun/classifyScreen.js.map +1 -1
  161. package/dist/firstRun/firstRunPlanner.js +11 -11
  162. package/dist/firstRun/firstRunPlanner.js.map +1 -1
  163. package/dist/firstRun/firstRunRunner.js +8 -8
  164. package/dist/firstRun/firstRunRunner.js.map +1 -1
  165. package/dist/firstRun/generatedDataPolicy.js +9 -9
  166. package/dist/firstRun/generatedDataPolicy.js.map +1 -1
  167. package/dist/firstRun/inputPlanner.js +5 -5
  168. package/dist/firstRun/inputPlanner.js.map +1 -1
  169. package/dist/firstRun/onboardingStateMachine.js +1 -1
  170. package/dist/firstRun/onboardingStateMachine.js.map +1 -1
  171. package/dist/firstRun/paywallPolicy.js +3 -3
  172. package/dist/firstRun/paywallPolicy.js.map +1 -1
  173. package/dist/firstRun/types.js +1 -1
  174. package/dist/firstRun/types.js.map +1 -1
  175. package/dist/fixtures/catalog.js +33 -3
  176. package/dist/fixtures/catalog.js.map +1 -1
  177. package/dist/fixtures/load.js +50 -0
  178. package/dist/fixtures/load.js.map +1 -0
  179. package/dist/flows/discover.js +2 -2
  180. package/dist/flows/discover.js.map +1 -1
  181. package/dist/flows/generate.js +20 -10
  182. package/dist/flows/generate.js.map +1 -1
  183. package/dist/flows/pack.js +3 -3
  184. package/dist/flows/pack.js.map +1 -1
  185. package/dist/flows/paths.js +57 -0
  186. package/dist/flows/paths.js.map +1 -0
  187. package/dist/flows/repair.js +111 -23
  188. package/dist/flows/repair.js.map +1 -1
  189. package/dist/flows/run.js +153 -57
  190. package/dist/flows/run.js.map +1 -1
  191. package/dist/flows/schema.js +50 -5
  192. package/dist/flows/schema.js.map +1 -1
  193. package/dist/flows/seedExec.js +4 -4
  194. package/dist/flows/seedExec.js.map +1 -1
  195. package/dist/flows/templates.js +5 -5
  196. package/dist/index.js +50 -8
  197. package/dist/index.js.map +1 -1
  198. package/dist/issues/classify.js +16 -16
  199. package/dist/issues/classify.js.map +1 -1
  200. package/dist/issues/fingerprint.js +24 -4
  201. package/dist/issues/fingerprint.js.map +1 -1
  202. package/dist/issues/index.js +94 -31
  203. package/dist/issues/index.js.map +1 -1
  204. package/dist/issues/metrics.js +4 -4
  205. package/dist/issues/metrics.js.map +1 -1
  206. package/dist/issues/recurrence.js +35 -8
  207. package/dist/issues/recurrence.js.map +1 -1
  208. package/dist/issues/report.js +4 -4
  209. package/dist/issues/report.js.map +1 -1
  210. package/dist/issues/reportBridge.js +4 -4
  211. package/dist/issues/reportBridge.js.map +1 -1
  212. package/dist/issues/schema.js +2 -2
  213. package/dist/issues/schema.js.map +1 -1
  214. package/dist/issues/sourceRevision.js +2 -2
  215. package/dist/issues/sourceRevision.js.map +1 -1
  216. package/dist/issues/store.js +110 -32
  217. package/dist/issues/store.js.map +1 -1
  218. package/dist/lib/abortScope.js +48 -0
  219. package/dist/lib/abortScope.js.map +1 -0
  220. package/dist/lib/android.js +75 -7
  221. package/dist/lib/android.js.map +1 -1
  222. package/dist/lib/coordSpace.js +1 -1
  223. package/dist/lib/coordSpace.js.map +1 -1
  224. package/dist/lib/device.js +32 -7
  225. package/dist/lib/device.js.map +1 -1
  226. package/dist/lib/gestures.js +141 -0
  227. package/dist/lib/gestures.js.map +1 -0
  228. package/dist/lib/gitignore.js +4 -3
  229. package/dist/lib/gitignore.js.map +1 -1
  230. package/dist/lib/image.js +1 -1
  231. package/dist/lib/image.js.map +1 -1
  232. package/dist/lib/lockfile.js +370 -31
  233. package/dist/lib/lockfile.js.map +1 -1
  234. package/dist/lib/needsInput.js +3 -3
  235. package/dist/lib/needsInput.js.map +1 -1
  236. package/dist/lib/png.js +2 -2
  237. package/dist/lib/png.js.map +1 -1
  238. package/dist/lib/redact.js +170 -9
  239. package/dist/lib/redact.js.map +1 -1
  240. package/dist/lib/result.js +79 -10
  241. package/dist/lib/result.js.map +1 -1
  242. package/dist/lib/schemaHash.js +3 -3
  243. package/dist/lib/schemaHash.js.map +1 -1
  244. package/dist/lib/sensitive.js +3 -2
  245. package/dist/lib/sensitive.js.map +1 -1
  246. package/dist/lib/simctl.js +3 -3
  247. package/dist/lib/simctl.js.map +1 -1
  248. package/dist/lib/spawn.js +52 -10
  249. package/dist/lib/spawn.js.map +1 -1
  250. package/dist/lib/toolAnnotations.js +114 -0
  251. package/dist/lib/toolAnnotations.js.map +1 -0
  252. package/dist/lib/wda.js +294 -15
  253. package/dist/lib/wda.js.map +1 -1
  254. package/dist/mobileAudit/checks.js +12 -12
  255. package/dist/mobileAudit/checks.js.map +1 -1
  256. package/dist/mobileAudit/evidence.js +4 -4
  257. package/dist/mobileAudit/evidence.js.map +1 -1
  258. package/dist/mobileAudit/profiles.js +1 -1
  259. package/dist/mobileAudit/profiles.js.map +1 -1
  260. package/dist/mobileAudit/results.js +3 -3
  261. package/dist/mobileAudit/results.js.map +1 -1
  262. package/dist/mobileAudit/runner.js +72 -18
  263. package/dist/mobileAudit/runner.js.map +1 -1
  264. package/dist/oracle/auth.js +1 -1
  265. package/dist/oracle/auth.js.map +1 -1
  266. package/dist/oracle/failures.js +219 -36
  267. package/dist/oracle/failures.js.map +1 -1
  268. package/dist/oracle/health.js +63 -17
  269. package/dist/oracle/health.js.map +1 -1
  270. package/dist/oracle/locator.js +13 -13
  271. package/dist/oracle/locator.js.map +1 -1
  272. package/dist/oracle/record.js +3 -3
  273. package/dist/oracle/record.js.map +1 -1
  274. package/dist/orchestration/envelope.js +2 -2
  275. package/dist/orchestration/envelope.js.map +1 -1
  276. package/dist/orchestration/goal.js +4 -4
  277. package/dist/orchestration/goal.js.map +1 -1
  278. package/dist/orchestration/testThis/execute.js +62 -21
  279. package/dist/orchestration/testThis/execute.js.map +1 -1
  280. package/dist/orchestration/testThis/pipeline.js +119 -30
  281. package/dist/orchestration/testThis/pipeline.js.map +1 -1
  282. package/dist/orchestration/testThis/plan.js +162 -73
  283. package/dist/orchestration/testThis/plan.js.map +1 -1
  284. package/dist/orchestration/testThis/sessionIntent.js +72 -0
  285. package/dist/orchestration/testThis/sessionIntent.js.map +1 -0
  286. package/dist/orchestration/testThis/terminal.js +34 -16
  287. package/dist/orchestration/testThis/terminal.js.map +1 -1
  288. package/dist/orchestration/testThis/types.js +1 -1
  289. package/dist/orchestration/testThis/types.js.map +1 -1
  290. package/dist/plan/plan.js +9 -9
  291. package/dist/plan/plan.js.map +1 -1
  292. package/dist/prompts/index.js +22 -22
  293. package/dist/prompts/index.js.map +1 -1
  294. package/dist/report/export.js +392 -37
  295. package/dist/report/export.js.map +1 -1
  296. package/dist/report/findingsDedupe.js +41 -0
  297. package/dist/report/findingsDedupe.js.map +1 -0
  298. package/dist/report/policy.js +7 -4
  299. package/dist/report/policy.js.map +1 -1
  300. package/dist/report/qaLevel.js +8 -8
  301. package/dist/report/qaLevel.js.map +1 -1
  302. package/dist/report/sarifSources.js +160 -0
  303. package/dist/report/sarifSources.js.map +1 -0
  304. package/dist/report/summary.js +2 -2
  305. package/dist/report/summary.js.map +1 -1
  306. package/dist/report/testCatalog.js +4 -4
  307. package/dist/report/testCatalog.js.map +1 -1
  308. package/dist/report/toolHealth.js +124 -0
  309. package/dist/report/toolHealth.js.map +1 -0
  310. package/dist/server.js +472 -45
  311. package/dist/server.js.map +1 -1
  312. package/dist/services/automationGenerate.js +8 -2
  313. package/dist/services/automationGenerate.js.map +1 -1
  314. package/dist/services/build.js +2 -2
  315. package/dist/services/build.js.map +1 -1
  316. package/dist/{tools → services}/flowGenerate.js +29 -13
  317. package/dist/services/flowGenerate.js.map +1 -0
  318. package/dist/services/preflight.js +4 -4
  319. package/dist/services/preflight.js.map +1 -1
  320. package/dist/services/prepareAndroid.js +46 -8
  321. package/dist/services/prepareAndroid.js.map +1 -1
  322. package/dist/services/prepareIos.js +24 -9
  323. package/dist/services/prepareIos.js.map +1 -1
  324. package/dist/services/report.js +100 -45
  325. package/dist/services/report.js.map +1 -1
  326. package/dist/services/smoke.js +32 -16
  327. package/dist/services/smoke.js.map +1 -1
  328. package/dist/services/suiteGenerate.js +28 -3
  329. package/dist/services/suiteGenerate.js.map +1 -1
  330. package/dist/services/testSuiteKnowledge.js +40 -27
  331. package/dist/services/testSuiteKnowledge.js.map +1 -1
  332. package/dist/session/attach.js +270 -6
  333. package/dist/session/attach.js.map +1 -1
  334. package/dist/session/processRegistry.js +317 -48
  335. package/dist/session/processRegistry.js.map +1 -1
  336. package/dist/session/progress.js +1 -1
  337. package/dist/session/retention.js +201 -0
  338. package/dist/session/retention.js.map +1 -0
  339. package/dist/session/store.js +275 -48
  340. package/dist/session/store.js.map +1 -1
  341. package/dist/snapshot/overlays.js +53 -8
  342. package/dist/snapshot/overlays.js.map +1 -1
  343. package/dist/snapshot/parse.js +28 -4
  344. package/dist/snapshot/parse.js.map +1 -1
  345. package/dist/snapshot/present.js +11 -5
  346. package/dist/snapshot/present.js.map +1 -1
  347. package/dist/snapshot/settle.js +39 -9
  348. package/dist/snapshot/settle.js.map +1 -1
  349. package/dist/state/consent.js +99 -0
  350. package/dist/state/consent.js.map +1 -0
  351. package/dist/state/profile.js +46 -2
  352. package/dist/state/profile.js.map +1 -1
  353. package/dist/suite/compile.js +9 -5
  354. package/dist/suite/compile.js.map +1 -1
  355. package/dist/suite/lint.js +4 -4
  356. package/dist/suite/lint.js.map +1 -1
  357. package/dist/suite/pom.js +50 -41
  358. package/dist/suite/pom.js.map +1 -1
  359. package/dist/suite/secretGuard.js +266 -0
  360. package/dist/suite/secretGuard.js.map +1 -0
  361. package/dist/suite/testcase.js +9 -7
  362. package/dist/suite/testcase.js.map +1 -1
  363. package/dist/testSuite/exporter.js +4 -4
  364. package/dist/testSuite/exporter.js.map +1 -1
  365. package/dist/testSuite/generator.js +13 -9
  366. package/dist/testSuite/generator.js.map +1 -1
  367. package/dist/testSuite/history.js +1 -1
  368. package/dist/testSuite/history.js.map +1 -1
  369. package/dist/testSuite/issueLinks.js +3 -3
  370. package/dist/testSuite/issueLinks.js.map +1 -1
  371. package/dist/testSuite/lint.js +4 -4
  372. package/dist/testSuite/lint.js.map +1 -1
  373. package/dist/testSuite/merge.js +5 -5
  374. package/dist/testSuite/merge.js.map +1 -1
  375. package/dist/testSuite/schema.js +5 -5
  376. package/dist/testSuite/schema.js.map +1 -1
  377. package/dist/testSuite/store.js +3 -3
  378. package/dist/testSuite/store.js.map +1 -1
  379. package/dist/testSuite/traceability.js +2 -2
  380. package/dist/testSuite/traceability.js.map +1 -1
  381. package/dist/tools/act.js +794 -164
  382. package/dist/tools/act.js.map +1 -1
  383. package/dist/tools/agent.js +451 -128
  384. package/dist/tools/agent.js.map +1 -1
  385. package/dist/tools/appControl.js +69 -19
  386. package/dist/tools/appControl.js.map +1 -1
  387. package/dist/tools/appMap.js +188 -130
  388. package/dist/tools/appMap.js.map +1 -1
  389. package/dist/tools/build.js +32 -42
  390. package/dist/tools/build.js.map +1 -1
  391. package/dist/tools/bundletool.js +21 -31
  392. package/dist/tools/bundletool.js.map +1 -1
  393. package/dist/tools/clearOverlay.js +55 -12
  394. package/dist/tools/clearOverlay.js.map +1 -1
  395. package/dist/tools/device.js +76 -24
  396. package/dist/tools/device.js.map +1 -1
  397. package/dist/tools/doctor.js +68 -28
  398. package/dist/tools/doctor.js.map +1 -1
  399. package/dist/tools/explore.js +41 -47
  400. package/dist/tools/explore.js.map +1 -1
  401. package/dist/tools/featureTesting.js +31 -48
  402. package/dist/tools/featureTesting.js.map +1 -1
  403. package/dist/tools/firstRun.js +34 -41
  404. package/dist/tools/firstRun.js.map +1 -1
  405. package/dist/tools/flow.js +183 -103
  406. package/dist/tools/flow.js.map +1 -1
  407. package/dist/tools/flowRepair.js +25 -13
  408. package/dist/tools/flowRepair.js.map +1 -1
  409. package/dist/tools/generate.js +52 -89
  410. package/dist/tools/generate.js.map +1 -1
  411. package/dist/tools/getArtifact.js +16 -5
  412. package/dist/tools/getArtifact.js.map +1 -1
  413. package/dist/tools/health.js +15 -12
  414. package/dist/tools/health.js.map +1 -1
  415. package/dist/tools/ios.js +45 -223
  416. package/dist/tools/ios.js.map +1 -1
  417. package/dist/tools/issues.js +251 -35
  418. package/dist/tools/issues.js.map +1 -1
  419. package/dist/tools/jobs.js +41 -16
  420. package/dist/tools/jobs.js.map +1 -1
  421. package/dist/tools/metro.js +36 -35
  422. package/dist/tools/metro.js.map +1 -1
  423. package/dist/tools/mobileAudit.js +50 -45
  424. package/dist/tools/mobileAudit.js.map +1 -1
  425. package/dist/tools/network.js +31 -14
  426. package/dist/tools/network.js.map +1 -1
  427. package/dist/tools/note.js +26 -26
  428. package/dist/tools/note.js.map +1 -1
  429. package/dist/tools/prepareIosTarget.js +10 -16
  430. package/dist/tools/prepareIosTarget.js.map +1 -1
  431. package/dist/tools/prepareTarget.js +117 -50
  432. package/dist/tools/prepareTarget.js.map +1 -1
  433. package/dist/tools/report.js +11 -22
  434. package/dist/tools/report.js.map +1 -1
  435. package/dist/tools/resolveArtifact.js +11 -15
  436. package/dist/tools/resolveArtifact.js.map +1 -1
  437. package/dist/tools/resolveTarget.js +78 -24
  438. package/dist/tools/resolveTarget.js.map +1 -1
  439. package/dist/tools/screenRecord.js +23 -21
  440. package/dist/tools/screenRecord.js.map +1 -1
  441. package/dist/tools/screenshot.js +23 -16
  442. package/dist/tools/screenshot.js.map +1 -1
  443. package/dist/tools/smoke.js +20 -16
  444. package/dist/tools/smoke.js.map +1 -1
  445. package/dist/tools/snapshot.js +155 -118
  446. package/dist/tools/snapshot.js.map +1 -1
  447. package/dist/tools/startSession.js +104 -116
  448. package/dist/tools/startSession.js.map +1 -1
  449. package/dist/tools/suite.js +72 -43
  450. package/dist/tools/suite.js.map +1 -1
  451. package/dist/tools/testSuite.js +30 -20
  452. package/dist/tools/testSuite.js.map +1 -1
  453. package/dist/tools/testThis.js +24 -28
  454. package/dist/tools/testThis.js.map +1 -1
  455. package/dist/tools/visual.js +686 -210
  456. package/dist/tools/visual.js.map +1 -1
  457. package/dist/tools/wait.js +9 -38
  458. package/dist/tools/wait.js.map +1 -1
  459. package/dist/tools/wda.js +187 -57
  460. package/dist/tools/wda.js.map +1 -1
  461. package/dist/version.js +31 -22
  462. package/dist/version.js.map +1 -1
  463. package/dist/visual/ocr.js +53 -6
  464. package/dist/visual/ocr.js.map +1 -1
  465. package/dist/visual/provider.js +80 -12
  466. package/dist/visual/provider.js.map +1 -1
  467. package/docs/README.md +24 -10
  468. package/docs/ci-reports.md +328 -0
  469. package/docs/concepts.md +227 -0
  470. package/docs/flows.md +153 -0
  471. package/docs/mcp-server.md +194 -128
  472. package/docs/physical-devices.md +84 -0
  473. package/docs/tools.md +812 -152
  474. package/package.json +3 -2
  475. package/dist/tools/assertVisual.js +0 -84
  476. package/dist/tools/assertVisual.js.map +0 -1
  477. package/dist/tools/automationGenerate.js.map +0 -1
  478. package/dist/tools/capabilities.js +0 -179
  479. package/dist/tools/capabilities.js.map +0 -1
  480. package/dist/tools/detectContext.js +0 -41
  481. package/dist/tools/detectContext.js.map +0 -1
  482. package/dist/tools/flowGenerate.js.map +0 -1
  483. package/dist/tools/history.js +0 -86
  484. package/dist/tools/history.js.map +0 -1
  485. package/dist/tools/locator.js +0 -85
  486. package/dist/tools/locator.js.map +0 -1
  487. package/dist/tools/permissions.js +0 -174
  488. package/dist/tools/permissions.js.map +0 -1
  489. package/dist/tools/plan.js +0 -52
  490. package/dist/tools/plan.js.map +0 -1
  491. package/dist/tools/screenInfo.js +0 -76
  492. package/dist/tools/screenInfo.js.map +0 -1
  493. package/dist/tools/seed.js +0 -133
  494. package/dist/tools/seed.js.map +0 -1
  495. package/dist/tools/state.js +0 -217
  496. package/dist/tools/state.js.map +0 -1
  497. package/dist/tools/visualText.js +0 -106
  498. package/dist/tools/visualText.js.map +0 -1
@@ -1,12 +1,13 @@
1
1
  // Guided exploration runner (Phase 3.3 Milestones D/E/G). Bounded, safe-by-default screen crawl:
2
- // observe → signature → rank safe candidates → act → health-check → record edge, until a budget is
2
+ // observe > signature > rank safe candidates > act > health-check > record edge, until a budget is
3
3
  // reached. Visual-only screens (poor UI tree) are verified with a screenshot + visual assertion
4
4
  // instead of blind coordinate crawling. Honest: unsafe actions are skipped with a note; an auth
5
- // wall with no credentials returns NeedsInput. Drives only the Driver interface — no MCP-over-MCP.
5
+ // wall with no credentials returns NeedsInput. Drives only the Driver interface, no MCP-over-MCP.
6
6
  import { parseSnapshot } from '../snapshot/parse.js';
7
7
  import { settle } from '../snapshot/settle.js';
8
8
  import { checkHealth } from '../oracle/health.js';
9
9
  import { recordHealthFindings } from '../oracle/record.js';
10
+ import { CancelledError, isAbortError } from '../lib/abortScope.js';
10
11
  import { ExploreGraph } from './graph.js';
11
12
  import { structuredSignature, visualSignature } from './signatures.js';
12
13
  import { actionLikeNonInteractive, rankCandidates, locatorQuality } from './candidates.js';
@@ -55,7 +56,7 @@ function valueForField(session, label, locatorValue, role, secure, gen) {
55
56
  const resolved = resolveFixtureValue(session, label, locatorValue, { role });
56
57
  if (resolved)
57
58
  return resolved;
58
- // Built-in safe generator — only when generation is explicitly allowed for this environment.
59
+ // Built-in safe generator, only when generation is explicitly allowed for this environment.
59
60
  if (gen?.allowed) {
60
61
  const field = fieldKindFromHints(label, locatorValue, undefined, secure);
61
62
  const g = generateFieldValue(field, gen.policy);
@@ -83,7 +84,7 @@ function strategyToKind(strategy) {
83
84
  case 'accessibility':
84
85
  return 'accessibility_id';
85
86
  case 'id':
86
- return 'resource_id'; // Android resource-id → durable id= selector (not a text match)
87
+ return 'resource_id'; // Android resource-id > durable id= selector (not a text match)
87
88
  case 'text':
88
89
  return 'text';
89
90
  default:
@@ -130,7 +131,7 @@ export async function runExplore(sessions, session, driver, opts, ctx = {}) {
130
131
  const maxScreens = opts.maxScreens ?? 12;
131
132
  const depth = opts.depth ?? 3;
132
133
  const stopOnAuth = opts.stopOnAuth ?? true;
133
- // SWIPIUM-REQ-02: resolve the generated-data decision once. Only "allowed" when policy + the
134
+ // Resolve the generated-data decision once. Only "allowed" when policy + the
134
135
  // classified environment (test/staging) permit safe throwaway data; otherwise generation is off.
135
136
  const genCtx = opts.allowGeneratedData
136
137
  ? (() => {
@@ -155,7 +156,7 @@ export async function runExplore(sessions, session, driver, opts, ctx = {}) {
155
156
  promotedSuiteCandidates: 0,
156
157
  testabilityBlockers: [],
157
158
  };
158
- const exploredPerScreen = new Map(); // nodeId → explored signatureKeys
159
+ const exploredPerScreen = new Map(); // nodeId > explored signatureKeys
159
160
  const skippedNotedScreens = new Set();
160
161
  const testabilityNotedScreens = new Set();
161
162
  const destructiveCandidates = new Map();
@@ -164,7 +165,7 @@ export async function runExplore(sessions, session, driver, opts, ctx = {}) {
164
165
  let sameScreenEdges = 0;
165
166
  let planned = false;
166
167
  const note = (n) => sessions.addNote(session, { at: Date.now(), ...n });
167
- /** Observe the current screen → a graph node (structured or visual-only). */
168
+ /** Observe the current screen as a graph node (structured or visual-only). */
168
169
  const observe = async () => {
169
170
  const foreground = await driver.foregroundOwner().catch(() => 'unknown');
170
171
  let xml = '';
@@ -176,10 +177,17 @@ export async function runExplore(sessions, session, driver, opts, ctx = {}) {
176
177
  elements = parsed.elements;
177
178
  webviewDominance = parsed.quality.signals.webviewDominance;
178
179
  }
179
- catch {
180
- /* snapshot failed → treated as visual-only below */
180
+ catch (e) {
181
+ // Cancelled job: an aborted dump is not a visual-only screen, a finding or a blocker.
182
+ if (isAbortError(e, signal))
183
+ throw new CancelledError('exploration cancelled');
184
+ /* snapshot failed, so treated as visual-only below */
181
185
  }
186
+ if (signal?.aborted)
187
+ throw new CancelledError('exploration cancelled');
182
188
  const health = await checkHealth(driver, session.appId, xml || undefined);
189
+ if (health.cancelled || signal?.aborted)
190
+ throw new CancelledError('exploration cancelled');
183
191
  await recordHealthFindings(sessions, session, health.findings, driver, health.foreground);
184
192
  if (!health.nativeHealthy)
185
193
  summary.appErrors += 0; // native handled separately below
@@ -201,7 +209,7 @@ export async function runExplore(sessions, session, driver, opts, ctx = {}) {
201
209
  /* best-effort */
202
210
  }
203
211
  }
204
- // Visual-only (Milestone E): no usable structured elements (canvas/map/webview/empty) → visual
212
+ // Visual-only (Milestone E): no usable structured elements (canvas/map/webview/empty) means visual
205
213
  // node + assertion, no blind coordinate crawl. A low automation-readiness verdict on a screen
206
214
  // that DOES expose identified elements is still structured (we can act on it).
207
215
  const visual = elements.length === 0 || webviewDominance > 0.5;
@@ -257,7 +265,7 @@ export async function runExplore(sessions, session, driver, opts, ctx = {}) {
257
265
  workflow: 'guided_exploration',
258
266
  outcome: 'pass',
259
267
  category: undefined,
260
- reason: `visual-only screen "${foreground}" — verified by screenshot`,
268
+ reason: `visual-only screen "${foreground}", verified by screenshot`,
261
269
  verifiedVisually: true,
262
270
  method: 'visual',
263
271
  artifactUris: screenshotUri ? [screenshotUri] : undefined,
@@ -268,7 +276,7 @@ export async function runExplore(sessions, session, driver, opts, ctx = {}) {
268
276
  const allowedCandidate = (candidate, node) => {
269
277
  if (candidate.risk !== 'destructive')
270
278
  return allowedUnder(candidate.risk, safeMode);
271
- // Controlled account-cycle exception (REQ-07): logout — and only logout — is an expected step
279
+ // Controlled account-cycle exception: logout (and only logout) is an expected step
272
280
  // on a disposable generated account inside the mobile-audit account-cycle workflow.
273
281
  if (opts.accountCycle?.enabled &&
274
282
  allowsControlledLogout({ label: candidate.label, id: candidate.locator?.value, role: candidate.locator?.strategy }, { accountCycle: true, disposableAccount: opts.accountCycle.disposableAccount })) {
@@ -307,319 +315,328 @@ export async function runExplore(sessions, session, driver, opts, ctx = {}) {
307
315
  };
308
316
  // ---- main loop ----
309
317
  let stoppedReason = 'exploration complete';
310
- while (true) {
311
- if (signal?.aborted) {
312
- stoppedReason = 'cancelled';
313
- break;
314
- }
315
- if (summary.actionsTried >= maxActions) {
316
- stoppedReason = `action budget reached (${maxActions})`;
317
- break;
318
- }
319
- if (graph.nodeCount() >= maxScreens) {
320
- stoppedReason = `screen budget reached (${maxScreens})`;
321
- break;
322
- }
323
- if (Date.now() >= deadline) {
324
- stoppedReason = 'time budget reached';
325
- break;
326
- }
327
- const budgetStop = sessions.budgetStop(session);
328
- if (budgetStop) {
329
- stoppedReason = `session budget: ${budgetStop}`;
330
- break;
331
- }
332
- const obs = await observe();
333
- progress(`exploring: ${summary.screensVisited} screens, ${summary.actionsTried} actions${obs.visual ? ', visual-only' : ''}`);
334
- if (!planned && (opts.strategy === 'task_planner' || opts.strategy === 'hybrid')) {
335
- planned = true;
336
- const screenText = obs.candidates.map((c) => `${c.label ?? ''} ${c.locator?.value ?? ''}`).join(' ');
337
- const domain = inferAppDomain({
338
- packageId: session.appId,
339
- goal: opts.goal,
340
- screenText,
341
- fixtures: session.fixtures.map((f) => f.name),
342
- });
343
- const tasks = proposeTasks({
344
- packageId: session.appId,
345
- goal: opts.goal,
346
- screenText,
347
- fixtures: session.fixtures.map((f) => f.name),
348
- candidates: obs.candidates,
349
- });
350
- graph.setTasks(tasks);
351
- graph.setHypotheses([`domain:${domain.domain}`, ...tasks.map((t) => `${t.feature}:${t.title}`)]);
352
- graph.setBlockedPreconditions([...new Set(tasks.flatMap((t) => t.preconditions))]);
353
- }
354
- // App error → record + stop this path (do not hide bugs; do not keep crawling a broken screen).
355
- if (obs.node.health.app === 'error') {
356
- note({
357
- workflow: 'guided_exploration',
358
- outcome: 'fail',
359
- category: 'app_bug',
360
- reason: `app-layer error on "${obs.node.title}"`,
361
- artifactUris: obs.node.screenshotUri ? [obs.node.screenshotUri] : undefined,
362
- });
363
- summary.blockers++;
364
- if (lastNodeId)
365
- graph.addEdge({
366
- from: lastNodeId,
367
- to: obs.node.id,
368
- action: { type: 'tap', targetDescription: 'prev action' },
369
- outcome: 'app_error',
370
- evidenceUris: obs.node.screenshotUri ? [obs.node.screenshotUri] : [],
318
+ // A cancelled job unwinds via CancelledError (thrown by observe). It's never recorded as a finding,
319
+ // blocker or visual-only screen; the partial graph is still finalized below.
320
+ try {
321
+ while (true) {
322
+ if (signal?.aborted) {
323
+ stoppedReason = 'cancelled';
324
+ break;
325
+ }
326
+ if (summary.actionsTried >= maxActions) {
327
+ stoppedReason = `action budget reached (${maxActions})`;
328
+ break;
329
+ }
330
+ if (graph.nodeCount() >= maxScreens) {
331
+ stoppedReason = `screen budget reached (${maxScreens})`;
332
+ break;
333
+ }
334
+ if (Date.now() >= deadline) {
335
+ stoppedReason = 'time budget reached';
336
+ break;
337
+ }
338
+ const budgetStop = sessions.budgetStop(session);
339
+ if (budgetStop) {
340
+ stoppedReason = `session budget: ${budgetStop}`;
341
+ break;
342
+ }
343
+ const obs = await observe();
344
+ progress(`exploring: ${summary.screensVisited} screens, ${summary.actionsTried} actions${obs.visual ? ', visual-only' : ''}`);
345
+ if (!planned && (opts.strategy === 'task_planner' || opts.strategy === 'hybrid')) {
346
+ planned = true;
347
+ const screenText = obs.candidates.map((c) => `${c.label ?? ''} ${c.locator?.value ?? ''}`).join(' ');
348
+ const domain = inferAppDomain({
349
+ packageId: session.appId,
350
+ goal: opts.goal,
351
+ screenText,
352
+ fixtures: session.fixtures.map((f) => f.name),
371
353
  });
372
- stoppedReason = 'app-layer error encountered';
373
- break;
374
- }
375
- if (obs.node.health.native === 'error') {
376
- summary.blockers++;
377
- stoppedReason = 'native error encountered';
378
- break;
379
- }
380
- // Auth wall with no credentials.
381
- if (obs.authWall && !hasCredentials(session)) {
382
- note({
383
- workflow: 'guided_exploration',
384
- outcome: 'blocked',
385
- category: 'missing_test_data',
386
- reason: 'login required, no credentials available',
387
- missingPrecondition: 'test account credentials',
388
- recommendedSetup: 'Provide TEST_EMAIL/TEST_PASSWORD (qa_continue_from_blocker) or accept pre-login coverage.',
389
- });
390
- summary.blockers++;
391
- if (stopOnAuth) {
392
- const suitePromotion = finalizeGraph(graph, summary, sameScreenEdges, destructiveCandidates, ['auth wall blocked exploration']);
393
- return {
394
- graph,
395
- summary,
396
- needsInput: NeedsInput.credentials('Exploration hit a login screen.'),
397
- state: 'needs_input',
398
- stoppedReason: 'auth required (credentials missing)',
399
- suitePromotion,
400
- destructiveCandidates: [...destructiveCandidates.values()],
401
- };
354
+ const tasks = proposeTasks({
355
+ packageId: session.appId,
356
+ goal: opts.goal,
357
+ screenText,
358
+ fixtures: session.fixtures.map((f) => f.name),
359
+ candidates: obs.candidates,
360
+ });
361
+ graph.setTasks(tasks);
362
+ graph.setHypotheses([`domain:${domain.domain}`, ...tasks.map((t) => `${t.feature}:${t.title}`)]);
363
+ graph.setBlockedPreconditions([...new Set(tasks.flatMap((t) => t.preconditions))]);
402
364
  }
403
- // stopOnAuth:false → record blocked and stop crawling further (only public screens were reachable).
404
- stoppedReason = 'auth required — recorded blocked, no credentials to proceed';
405
- break;
406
- }
407
- const explored = exploredPerScreen.get(obs.node.id) ?? new Set();
408
- exploredPerScreen.set(obs.node.id, explored);
409
- // Visual-only screens: no safe structured target → step back to keep the graph connected.
410
- const pick = obs.candidates.find((c) => !explored.has(c.signatureKey) && allowedCandidate(c, obs.node));
411
- const blockedCandidates = obs.candidates.filter((c) => !allowedCandidate(c, obs.node));
412
- if (blockedCandidates.length && !skippedNotedScreens.has(obs.node.id)) {
413
- skippedNotedScreens.add(obs.node.id);
414
- const ex = blockedCandidates[0];
415
- note({
416
- workflow: 'guided_exploration',
417
- outcome: 'skipped',
418
- category: ex.risk === 'destructive' ? 'destructive_refused' : 'other',
419
- reason: `Skipped ${ex.risk} action "${ex.label ?? ex.locator?.value}": ${ex.reason}`,
420
- recommendedSetup: ex.risk === 'destructive'
421
- ? 'Run dry_run_destructive to list candidate-bound approvals, then approve one exact candidate with disposable test state.'
422
- : 'Add accessibilityLabel/testID so the action can be judged + automated.',
423
- });
424
- summary.unsafeActionsSkipped += blockedCandidates.length;
425
- }
426
- if (!pick) {
427
- if (obs.actionLikeSkipped.length && !testabilityNotedScreens.has(obs.node.id)) {
428
- testabilityNotedScreens.add(obs.node.id);
429
- for (const s of obs.actionLikeSkipped.slice(0, 8)) {
430
- summary.testabilityBlockers.push({
431
- screen: obs.node.title ?? 'unknown',
432
- visibleText: s.visibleText,
433
- role: s.role,
434
- bounds: s.bounds,
435
- clickable: s.clickable,
436
- recommendation: `"${s.visibleText}" looks tappable but the UI tree marks it non-interactive. Add accessibilityRole="button" and a stable testID or accessibility identifier.`,
437
- manualCandidate: { x: Math.round(s.bounds.x + s.bounds.w / 2), y: Math.round(s.bounds.y + s.bounds.h / 2) },
438
- });
439
- }
365
+ // App error: record + stop this path (do not hide bugs; do not keep crawling a broken screen).
366
+ if (obs.node.health.app === 'error') {
440
367
  note({
441
368
  workflow: 'guided_exploration',
442
- outcome: 'blocked',
443
- category: 'mcp_limitation',
444
- reason: `No safe actionable elements on "${obs.node.title}". ${obs.actionLikeSkipped.length} visible action-like text element(s) are not clickable in the UI tree.`,
445
- missingPrecondition: 'tappable controls with accessibilityRole and testID',
446
- recommendedSetup: `Add accessibilityRole="button" and a stable testID to primary buttons such as "${obs.actionLikeSkipped[0].visibleText}".`,
369
+ outcome: 'fail',
370
+ category: 'app_bug',
371
+ reason: `app-layer error on "${obs.node.title}"`,
372
+ artifactUris: obs.node.screenshotUri ? [obs.node.screenshotUri] : undefined,
447
373
  });
374
+ summary.blockers++;
375
+ if (lastNodeId)
376
+ graph.addEdge({
377
+ from: lastNodeId,
378
+ to: obs.node.id,
379
+ action: { type: 'tap', targetDescription: 'prev action' },
380
+ outcome: 'app_error',
381
+ evidenceUris: obs.node.screenshotUri ? [obs.node.screenshotUri] : [],
382
+ });
383
+ stoppedReason = 'app-layer error encountered';
384
+ break;
448
385
  }
449
- // Nothing safe+new here. Go back to keep exploring; if we're at the root with nothing, stop.
450
- if (lastNodeId && obs.node.id !== graph.rootId && depth > 1) {
451
- await driver.pressKey('back').catch(() => { });
452
- graph.addEdge({
453
- from: obs.node.id,
454
- action: { type: 'back', targetDescription: 'back' },
455
- outcome: 'changed_screen',
456
- evidenceUris: [],
457
- });
458
- await settle(driver, { timeoutMs: 4000 }).catch(() => { });
459
- lastNodeId = obs.node.id;
460
- continue;
386
+ if (obs.node.health.native === 'error') {
387
+ summary.blockers++;
388
+ stoppedReason = 'native error encountered';
389
+ break;
461
390
  }
462
- stoppedReason = 'no further safe, unexplored actions';
463
- break;
464
- }
465
- // ---- act on the chosen candidate ----
466
- explored.add(pick.signatureKey);
467
- const cx = (pick.bounds?.x ?? 0) + (pick.bounds?.w ?? 0) / 2;
468
- const cy = (pick.bounds?.y ?? 0) + (pick.bounds?.h ?? 0) / 2;
469
- const fromId = obs.node.id;
470
- // Text entry only with a value source (P1 fix): never type blindly. If none, skip honestly.
471
- if (pick.actionType === 'type') {
472
- const v = valueForField(session, pick.label, pick.locator?.value, pick.role, pick.secure, genCtx);
473
- if (!v) {
391
+ // Auth wall with no credentials.
392
+ if (obs.authWall && !hasCredentials(session)) {
474
393
  note({
475
394
  workflow: 'guided_exploration',
476
- outcome: 'skipped',
395
+ outcome: 'blocked',
477
396
  category: 'missing_test_data',
478
- reason: `text field "${pick.label ?? pick.locator?.value}" needs a value to exercise`,
479
- missingPrecondition: 'a fixture value or credential for this field',
480
- recommendedSetup: 'Provide credentials (qa_continue_from_blocker) or a fixture { name, value }.',
397
+ reason: 'login required, no credentials available',
398
+ missingPrecondition: 'test account credentials',
399
+ recommendedSetup: 'Provide SWIPIUM_TEST_EMAIL/SWIPIUM_TEST_PASSWORD (qa_continue_from_blocker) or accept pre-login coverage.',
481
400
  });
482
401
  summary.blockers++;
402
+ if (stopOnAuth) {
403
+ const suitePromotion = finalizeGraph(graph, summary, sameScreenEdges, destructiveCandidates, ['auth wall blocked exploration']);
404
+ return {
405
+ graph,
406
+ summary,
407
+ needsInput: NeedsInput.credentials('Exploration hit a login screen.'),
408
+ state: 'needs_input',
409
+ stoppedReason: 'auth required (credentials missing)',
410
+ suitePromotion,
411
+ destructiveCandidates: [...destructiveCandidates.values()],
412
+ };
413
+ }
414
+ // stopOnAuth:false: record blocked and stop crawling further (only public screens were reachable).
415
+ stoppedReason = 'auth required: recorded blocked, no credentials to proceed';
416
+ break;
417
+ }
418
+ const explored = exploredPerScreen.get(obs.node.id) ?? new Set();
419
+ exploredPerScreen.set(obs.node.id, explored);
420
+ // Visual-only screens: no safe structured target, so step back to keep the graph connected.
421
+ const pick = obs.candidates.find((c) => !explored.has(c.signatureKey) && allowedCandidate(c, obs.node));
422
+ const blockedCandidates = obs.candidates.filter((c) => !allowedCandidate(c, obs.node));
423
+ if (blockedCandidates.length && !skippedNotedScreens.has(obs.node.id)) {
424
+ skippedNotedScreens.add(obs.node.id);
425
+ const ex = blockedCandidates[0];
426
+ note({
427
+ workflow: 'guided_exploration',
428
+ outcome: 'skipped',
429
+ category: ex.risk === 'destructive' ? 'destructive_refused' : 'other',
430
+ reason: `Skipped ${ex.risk} action "${ex.label ?? ex.locator?.value}": ${ex.reason}`,
431
+ recommendedSetup: ex.risk === 'destructive'
432
+ ? 'Run dry_run_destructive to list candidate-bound approvals, then approve one exact candidate with disposable test state.'
433
+ : 'Add accessibilityLabel/testID so the action can be judged + automated.',
434
+ });
435
+ summary.unsafeActionsSkipped += blockedCandidates.length;
436
+ }
437
+ if (!pick) {
438
+ if (obs.actionLikeSkipped.length && !testabilityNotedScreens.has(obs.node.id)) {
439
+ testabilityNotedScreens.add(obs.node.id);
440
+ for (const s of obs.actionLikeSkipped.slice(0, 8)) {
441
+ summary.testabilityBlockers.push({
442
+ screen: obs.node.title ?? 'unknown',
443
+ visibleText: s.visibleText,
444
+ role: s.role,
445
+ bounds: s.bounds,
446
+ clickable: s.clickable,
447
+ recommendation: `"${s.visibleText}" looks tappable but the UI tree marks it non-interactive. Add accessibilityRole="button" and a stable testID or accessibility identifier.`,
448
+ manualCandidate: { x: Math.round(s.bounds.x + s.bounds.w / 2), y: Math.round(s.bounds.y + s.bounds.h / 2) },
449
+ });
450
+ }
451
+ note({
452
+ workflow: 'guided_exploration',
453
+ outcome: 'blocked',
454
+ category: 'mcp_limitation',
455
+ reason: `No safe actionable elements on "${obs.node.title}". ${obs.actionLikeSkipped.length} visible action-like text element(s) are not clickable in the UI tree.`,
456
+ missingPrecondition: 'tappable controls with accessibilityRole and testID',
457
+ recommendedSetup: `Add accessibilityRole="button" and a stable testID to primary buttons such as "${obs.actionLikeSkipped[0].visibleText}".`,
458
+ });
459
+ }
460
+ // Nothing safe+new here. Go back to keep exploring; if we're at the root with nothing, stop.
461
+ if (lastNodeId && obs.node.id !== graph.rootId && depth > 1) {
462
+ await driver.pressKey('back').catch(() => { });
463
+ graph.addEdge({
464
+ from: obs.node.id,
465
+ action: { type: 'back', targetDescription: 'back' },
466
+ outcome: 'changed_screen',
467
+ evidenceUris: [],
468
+ });
469
+ await settle(driver, { timeoutMs: 4000 }).catch(() => { });
470
+ lastNodeId = obs.node.id;
471
+ continue;
472
+ }
473
+ stoppedReason = 'no further safe, unexplored actions';
474
+ break;
475
+ }
476
+ // ---- act on the chosen candidate ----
477
+ explored.add(pick.signatureKey);
478
+ const cx = (pick.bounds?.x ?? 0) + (pick.bounds?.w ?? 0) / 2;
479
+ const cy = (pick.bounds?.y ?? 0) + (pick.bounds?.h ?? 0) / 2;
480
+ const fromId = obs.node.id;
481
+ // Text entry only with a value source (P1 fix): never type blindly. If none, skip honestly.
482
+ if (pick.actionType === 'type') {
483
+ const v = valueForField(session, pick.label, pick.locator?.value, pick.role, pick.secure, genCtx);
484
+ if (!v) {
485
+ note({
486
+ workflow: 'guided_exploration',
487
+ outcome: 'skipped',
488
+ category: 'missing_test_data',
489
+ reason: `text field "${pick.label ?? pick.locator?.value}" needs a value to exercise`,
490
+ missingPrecondition: 'a fixture value or credential for this field',
491
+ recommendedSetup: 'Provide credentials (qa_continue_from_blocker) or a fixture { name, value }.',
492
+ });
493
+ summary.blockers++;
494
+ continue;
495
+ }
496
+ try {
497
+ await driver.tapXY(Math.round(cx), Math.round(cy)); // focus the field
498
+ await driver.inputText(v.value);
499
+ }
500
+ catch {
501
+ graph.addEdge({
502
+ from: fromId,
503
+ action: { type: 'type', targetDescription: pick.label ?? 'field' },
504
+ outcome: 'blocked',
505
+ evidenceUris: [],
506
+ });
507
+ continue;
508
+ }
509
+ summary.actionsTried++;
510
+ sessions.bump(session, 'actions');
511
+ if (v.secret)
512
+ session.secrets.add(v.value);
513
+ if (v.source === 'generator') {
514
+ const rec = session.generatedValues.find((g) => g.varName === v.varName && g.fixture === v.fixture && g.field === v.field);
515
+ if (rec && !rec.artifactUri) {
516
+ rec.artifactUri = sessions.saveArtifact(session, 'generated_data', `generated-${v.varName.toLowerCase()}-${Date.now()}.json`, JSON.stringify({
517
+ schema: 'swipium.generated_value.v1',
518
+ fixture: rec.fixture,
519
+ field: rec.field,
520
+ varName: rec.varName,
521
+ generator: rec.generator,
522
+ value: rec.secret ? '<redacted>' : rec.value,
523
+ secret: rec.secret,
524
+ }, null, 2), 'application/json', `generated fixture value ${rec.fixture}.${rec.field}`);
525
+ sessions.persist(session);
526
+ }
527
+ }
528
+ if (pick.locator && pick.locator.strategy !== 'coordinate') {
529
+ sessions.addRecordedAction(session, {
530
+ at: Date.now(),
531
+ action: 'type',
532
+ selector: pick.locator.value,
533
+ selectorKind: strategyToKind(pick.locator.strategy),
534
+ text: `\${${v.varName}}`,
535
+ secret: v.secret,
536
+ exportability: v.secret ? 'needs-human-data' : 'semantic',
537
+ screen: obs.node.title,
538
+ provenance: provenanceForCandidate(obs.node, pick),
539
+ });
540
+ }
541
+ await settle(driver, { timeoutMs: 5000 }).catch(() => { });
542
+ const afterType = await observe();
543
+ graph.addEdge({
544
+ from: fromId,
545
+ to: afterType.node.id,
546
+ action: { type: 'type', targetDescription: pick.label ?? 'field' },
547
+ outcome: afterType.node.id === fromId ? 'same_screen' : 'changed_screen',
548
+ evidenceUris: [],
549
+ });
550
+ lastNodeId = afterType.node.id;
483
551
  continue;
484
552
  }
485
553
  try {
486
- await driver.tapXY(Math.round(cx), Math.round(cy)); // focus the field
487
- await driver.inputText(v.value);
554
+ await driver.tapXY(Math.round(cx), Math.round(cy));
488
555
  }
489
556
  catch {
557
+ if (pick.risk === 'destructive') {
558
+ sessions.recordMutation(session, {
559
+ tool: 'qa_explore',
560
+ action: 'destructive_ui_candidate',
561
+ risk: 'high',
562
+ target: destructiveMutationTarget(session, obs.node, pick, opts.destructiveApproval),
563
+ consent: { required: true, consentId: opts.destructiveApproval?.consentId, approved: true },
564
+ status: 'blocked',
565
+ detail: 'Approved destructive exploration candidate could not be tapped.',
566
+ });
567
+ }
490
568
  graph.addEdge({
491
569
  from: fromId,
492
- action: { type: 'type', targetDescription: pick.label ?? 'field' },
570
+ action: { type: 'tap', targetDescription: pick.label ?? pick.locator?.value ?? 'control' },
493
571
  outcome: 'blocked',
494
572
  evidenceUris: [],
495
573
  });
496
574
  continue;
497
575
  }
576
+ if (pick.risk === 'destructive') {
577
+ sessions.recordMutation(session, {
578
+ tool: 'qa_explore',
579
+ action: 'destructive_ui_candidate',
580
+ risk: 'high',
581
+ target: destructiveMutationTarget(session, obs.node, pick, opts.destructiveApproval),
582
+ consent: { required: true, consentId: opts.destructiveApproval?.consentId, approved: true },
583
+ status: 'executed',
584
+ detail: 'Approved destructive exploration candidate tapped.',
585
+ });
586
+ }
498
587
  summary.actionsTried++;
499
588
  sessions.bump(session, 'actions');
500
- if (v.secret)
501
- session.secrets.add(v.value);
502
- if (v.source === 'generator') {
503
- const rec = session.generatedValues.find((g) => g.varName === v.varName && g.fixture === v.fixture && g.field === v.field);
504
- if (rec && !rec.artifactUri) {
505
- rec.artifactUri = sessions.saveArtifact(session, 'generated_data', `generated-${v.varName.toLowerCase()}-${Date.now()}.json`, JSON.stringify({
506
- schema: 'swipium.generated_value.v1',
507
- fixture: rec.fixture,
508
- field: rec.field,
509
- varName: rec.varName,
510
- generator: rec.generator,
511
- value: rec.secret ? '<redacted>' : rec.value,
512
- secret: rec.secret,
513
- }, null, 2), 'application/json', `generated fixture value ${rec.fixture}.${rec.field}`);
514
- sessions.persist(session);
515
- }
516
- }
589
+ // Record durable taps so qa_generate target:"suite" can promote the path (§9.3).
517
590
  if (pick.locator && pick.locator.strategy !== 'coordinate') {
518
591
  sessions.addRecordedAction(session, {
519
592
  at: Date.now(),
520
- action: 'type',
593
+ action: 'tap',
521
594
  selector: pick.locator.value,
522
595
  selectorKind: strategyToKind(pick.locator.strategy),
523
- text: `\${${v.varName}}`,
524
- secret: v.secret,
525
- exportability: v.secret ? 'needs-human-data' : 'semantic',
596
+ exportability: 'semantic',
526
597
  screen: obs.node.title,
527
598
  provenance: provenanceForCandidate(obs.node, pick),
528
599
  });
529
600
  }
530
601
  await settle(driver, { timeoutMs: 5000 }).catch(() => { });
531
- const afterType = await observe();
532
- graph.addEdge({
533
- from: fromId,
534
- to: afterType.node.id,
535
- action: { type: 'type', targetDescription: pick.label ?? 'field' },
536
- outcome: afterType.node.id === fromId ? 'same_screen' : 'changed_screen',
537
- evidenceUris: [],
538
- });
539
- lastNodeId = afterType.node.id;
540
- continue;
541
- }
542
- try {
543
- await driver.tapXY(Math.round(cx), Math.round(cy));
544
- }
545
- catch {
546
- if (pick.risk === 'destructive') {
547
- sessions.recordMutation(session, {
548
- tool: 'qa_explore',
549
- action: 'destructive_ui_candidate',
550
- risk: 'high',
551
- target: destructiveMutationTarget(session, obs.node, pick, opts.destructiveApproval),
552
- consent: { required: true, consentId: opts.destructiveApproval?.consentId, approved: true },
553
- status: 'blocked',
554
- detail: 'Approved destructive exploration candidate could not be tapped.',
555
- });
602
+ // ---- observe outcome to classify the transition ----
603
+ const after = await observe();
604
+ const outcome = after.node.id === fromId ? 'same_screen' : 'changed_screen';
605
+ if (outcome === 'changed_screen') {
606
+ summary.workflowsFound++;
607
+ consecutiveNoChange = 0;
556
608
  }
609
+ else {
610
+ consecutiveNoChange++;
611
+ }
612
+ if (outcome === 'same_screen')
613
+ sameScreenEdges++;
557
614
  graph.addEdge({
558
615
  from: fromId,
559
- action: { type: 'tap', targetDescription: pick.label ?? pick.locator?.value ?? 'control' },
560
- outcome: 'blocked',
561
- evidenceUris: [],
562
- });
563
- continue;
564
- }
565
- if (pick.risk === 'destructive') {
566
- sessions.recordMutation(session, {
567
- tool: 'qa_explore',
568
- action: 'destructive_ui_candidate',
569
- risk: 'high',
570
- target: destructiveMutationTarget(session, obs.node, pick, opts.destructiveApproval),
571
- consent: { required: true, consentId: opts.destructiveApproval?.consentId, approved: true },
572
- status: 'executed',
573
- detail: 'Approved destructive exploration candidate tapped.',
616
+ to: after.node.id,
617
+ action: {
618
+ type: 'tap',
619
+ targetDescription: pick.label ?? pick.locator?.value ?? 'control',
620
+ locator: pick.locator,
621
+ },
622
+ outcome,
623
+ evidenceUris: after.node.screenshotUri ? [after.node.screenshotUri] : [],
624
+ riskDecision: pick.riskClass ?? pick.risk,
625
+ preActionState: obs.node.signature,
626
+ postActionState: after.node.signature,
627
+ oracle: outcome === 'changed_screen' ? 'screen_signature_changed' : 'screen_signature_same',
574
628
  });
629
+ lastNodeId = after.node.id;
630
+ if (consecutiveNoChange >= 3) {
631
+ stoppedReason = 'repeated no-change actions';
632
+ break;
633
+ }
575
634
  }
576
- summary.actionsTried++;
577
- sessions.bump(session, 'actions');
578
- // Record durable taps so qa_generate target:"suite" can promote the path (§9.3).
579
- if (pick.locator && pick.locator.strategy !== 'coordinate') {
580
- sessions.addRecordedAction(session, {
581
- at: Date.now(),
582
- action: 'tap',
583
- selector: pick.locator.value,
584
- selectorKind: strategyToKind(pick.locator.strategy),
585
- exportability: 'semantic',
586
- screen: obs.node.title,
587
- provenance: provenanceForCandidate(obs.node, pick),
588
- });
589
- }
590
- await settle(driver, { timeoutMs: 5000 }).catch(() => { });
591
- // ---- observe outcome to classify the transition ----
592
- const after = await observe();
593
- const outcome = after.node.id === fromId ? 'same_screen' : 'changed_screen';
594
- if (outcome === 'changed_screen') {
595
- summary.workflowsFound++;
596
- consecutiveNoChange = 0;
597
- }
598
- else {
599
- consecutiveNoChange++;
600
- }
601
- if (outcome === 'same_screen')
602
- sameScreenEdges++;
603
- graph.addEdge({
604
- from: fromId,
605
- to: after.node.id,
606
- action: {
607
- type: 'tap',
608
- targetDescription: pick.label ?? pick.locator?.value ?? 'control',
609
- locator: pick.locator,
610
- },
611
- outcome,
612
- evidenceUris: after.node.screenshotUri ? [after.node.screenshotUri] : [],
613
- riskDecision: pick.riskClass ?? pick.risk,
614
- preActionState: obs.node.signature,
615
- postActionState: after.node.signature,
616
- oracle: outcome === 'changed_screen' ? 'screen_signature_changed' : 'screen_signature_same',
617
- });
618
- lastNodeId = after.node.id;
619
- if (consecutiveNoChange >= 3) {
620
- stoppedReason = 'repeated no-change actions';
621
- break;
622
- }
635
+ }
636
+ catch (e) {
637
+ if (!(e instanceof CancelledError) && !(signal?.aborted && isAbortError(e, signal)))
638
+ throw e;
639
+ stoppedReason = 'cancelled';
623
640
  }
624
641
  const suitePromotion = finalizeGraph(graph, summary, sameScreenEdges, destructiveCandidates, []);
625
642
  return {