swipium 1.3.0 → 1.5.0

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 (438) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +89 -25
  3. package/THREAT_MODEL.md +1 -1
  4. package/dist/appMap/automationLink.js +13 -3
  5. package/dist/appMap/automationLink.js.map +1 -1
  6. package/dist/appMap/build.js +28 -6
  7. package/dist/appMap/build.js.map +1 -1
  8. package/dist/appMap/codeIndex.js +29 -3
  9. package/dist/appMap/codeIndex.js.map +1 -1
  10. package/dist/appMap/featureIndex.js +20 -2
  11. package/dist/appMap/featureIndex.js.map +1 -1
  12. package/dist/appMap/featureModel.js +42 -6
  13. package/dist/appMap/featureModel.js.map +1 -1
  14. package/dist/appMap/firstRunApply.js +22 -4
  15. package/dist/appMap/firstRunApply.js.map +1 -1
  16. package/dist/appMap/fsWalk.js +19 -2
  17. package/dist/appMap/fsWalk.js.map +1 -1
  18. package/dist/appMap/issues.js +0 -20
  19. package/dist/appMap/issues.js.map +1 -1
  20. package/dist/appMap/migrations.js +1 -1
  21. package/dist/appMap/migrations.js.map +1 -1
  22. package/dist/appMap/prelaunch.js.map +1 -1
  23. package/dist/appMap/projectRegistry.js +8 -2
  24. package/dist/appMap/projectRegistry.js.map +1 -1
  25. package/dist/appMap/query.js +20 -4
  26. package/dist/appMap/query.js.map +1 -1
  27. package/dist/appMap/runtimeMerge.js +9 -2
  28. package/dist/appMap/runtimeMerge.js.map +1 -1
  29. package/dist/appMap/schema.js.map +1 -1
  30. package/dist/appMap/screenMatch.js +5 -1
  31. package/dist/appMap/screenMatch.js.map +1 -1
  32. package/dist/appMap/staticScan.js +134 -22
  33. package/dist/appMap/staticScan.js.map +1 -1
  34. package/dist/appMap/store.js +4 -79
  35. package/dist/appMap/store.js.map +1 -1
  36. package/dist/appMap/tsAstScan.js +31 -9
  37. package/dist/appMap/tsAstScan.js.map +1 -1
  38. package/dist/artifacts/bundletool.js +44 -7
  39. package/dist/artifacts/bundletool.js.map +1 -1
  40. package/dist/artifacts/resolve.js +33 -6
  41. package/dist/artifacts/resolve.js.map +1 -1
  42. package/dist/automation/capabilities.js +109 -27
  43. package/dist/automation/capabilities.js.map +1 -1
  44. package/dist/automation/gestures.js +5 -1
  45. package/dist/automation/gestures.js.map +1 -1
  46. package/dist/automation/plan.js +65 -17
  47. package/dist/automation/plan.js.map +1 -1
  48. package/dist/automation/report.js +12 -2
  49. package/dist/automation/report.js.map +1 -1
  50. package/dist/automation/selectors.js +8 -3
  51. package/dist/automation/selectors.js.map +1 -1
  52. package/dist/automation/types.js.map +1 -1
  53. package/dist/automation/waits.js +9 -3
  54. package/dist/automation/waits.js.map +1 -1
  55. package/dist/automation/webview.js +7 -1
  56. package/dist/automation/webview.js.map +1 -1
  57. package/dist/automationGen/appiumModel.js +3 -1
  58. package/dist/automationGen/appiumModel.js.map +1 -1
  59. package/dist/automationGen/ciEmitter.js +1 -1
  60. package/dist/automationGen/ciEmitter.js.map +1 -1
  61. package/dist/automationGen/jsEmitter.js +29 -21
  62. package/dist/automationGen/jsEmitter.js.map +1 -1
  63. package/dist/automationGen/projectProfile.js +5 -1
  64. package/dist/automationGen/projectProfile.js.map +1 -1
  65. package/dist/automationGen/pythonEmitter.js +3 -3
  66. package/dist/automationGen/pythonEmitter.js.map +1 -1
  67. package/dist/automationGen/readmeEmitter.js.map +1 -1
  68. package/dist/automationGen/suitePlan.js +11 -6
  69. package/dist/automationGen/suitePlan.js.map +1 -1
  70. package/dist/automationGen/validation.js +33 -5
  71. package/dist/automationGen/validation.js.map +1 -1
  72. package/dist/build/parseBuildLog.js +234 -30
  73. package/dist/build/parseBuildLog.js.map +1 -1
  74. package/dist/build/plan.js +33 -12
  75. package/dist/build/plan.js.map +1 -1
  76. package/dist/ci/preflight.js.map +1 -1
  77. package/dist/cli/init.js.map +1 -1
  78. package/dist/cli/scan.js +1 -1
  79. package/dist/cli/scan.js.map +1 -1
  80. package/dist/cli/suite.js +2 -2
  81. package/dist/cli/suite.js.map +1 -1
  82. package/dist/cli/verify.js.map +1 -1
  83. package/dist/context/detect.js +10 -4
  84. package/dist/context/detect.js.map +1 -1
  85. package/dist/context/findApps.js +16 -2
  86. package/dist/context/findApps.js.map +1 -1
  87. package/dist/context/scan.js.map +1 -1
  88. package/dist/core/target.js +7 -5
  89. package/dist/core/target.js.map +1 -1
  90. package/dist/core/targetPlan.js +135 -17
  91. package/dist/core/targetPlan.js.map +1 -1
  92. package/dist/drivers/DirectDriver.js +17 -7
  93. package/dist/drivers/DirectDriver.js.map +1 -1
  94. package/dist/drivers/SimctlDriver.js +4 -1
  95. package/dist/drivers/SimctlDriver.js.map +1 -1
  96. package/dist/drivers/WdaDriver.js +15 -5
  97. package/dist/drivers/WdaDriver.js.map +1 -1
  98. package/dist/explore/candidates.js +5 -1
  99. package/dist/explore/candidates.js.map +1 -1
  100. package/dist/explore/graph.js +11 -2
  101. package/dist/explore/graph.js.map +1 -1
  102. package/dist/explore/memory.js +4 -1
  103. package/dist/explore/memory.js.map +1 -1
  104. package/dist/explore/planner.js +54 -9
  105. package/dist/explore/planner.js.map +1 -1
  106. package/dist/explore/policy.js +6 -1
  107. package/dist/explore/policy.js.map +1 -1
  108. package/dist/explore/runner.js +179 -23
  109. package/dist/explore/runner.js.map +1 -1
  110. package/dist/explore/signatures.js +3 -1
  111. package/dist/explore/signatures.js.map +1 -1
  112. package/dist/explore/suite.js +3 -1
  113. package/dist/explore/suite.js.map +1 -1
  114. package/dist/featureTesting/executionBootstrap.js +128 -19
  115. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  116. package/dist/featureTesting/featureMap.js +4 -1
  117. package/dist/featureTesting/featureMap.js.map +1 -1
  118. package/dist/featureTesting/featureScope.js +94 -13
  119. package/dist/featureTesting/featureScope.js.map +1 -1
  120. package/dist/featureTesting/mapFeatureScope.js +37 -8
  121. package/dist/featureTesting/mapFeatureScope.js.map +1 -1
  122. package/dist/featureTesting/objectiveModel.js +5 -1
  123. package/dist/featureTesting/objectiveModel.js.map +1 -1
  124. package/dist/featureTesting/resultMerge.js +10 -2
  125. package/dist/featureTesting/resultMerge.js.map +1 -1
  126. package/dist/featureTesting/sources.js +4 -1
  127. package/dist/featureTesting/sources.js.map +1 -1
  128. package/dist/featureTesting/suiteBridge.js +7 -1
  129. package/dist/featureTesting/suiteBridge.js.map +1 -1
  130. package/dist/featureTesting/synonyms.js +31 -3
  131. package/dist/featureTesting/synonyms.js.map +1 -1
  132. package/dist/featureTesting/testCaseFactory.js +16 -4
  133. package/dist/featureTesting/testCaseFactory.js.map +1 -1
  134. package/dist/featureTesting/testPlan.js +32 -10
  135. package/dist/featureTesting/testPlan.js.map +1 -1
  136. package/dist/firstRun/classifyScreen.js +4 -1
  137. package/dist/firstRun/classifyScreen.js.map +1 -1
  138. package/dist/firstRun/firstRunPlanner.js +5 -2
  139. package/dist/firstRun/firstRunPlanner.js.map +1 -1
  140. package/dist/firstRun/firstRunRunner.js +84 -23
  141. package/dist/firstRun/firstRunRunner.js.map +1 -1
  142. package/dist/firstRun/generatedDataPolicy.js +16 -6
  143. package/dist/firstRun/generatedDataPolicy.js.map +1 -1
  144. package/dist/firstRun/inputPlanner.js +40 -6
  145. package/dist/firstRun/inputPlanner.js.map +1 -1
  146. package/dist/firstRun/paywallPolicy.js.map +1 -1
  147. package/dist/fixtures/catalog.js +39 -6
  148. package/dist/fixtures/catalog.js.map +1 -1
  149. package/dist/flows/generate.js +53 -12
  150. package/dist/flows/generate.js.map +1 -1
  151. package/dist/flows/lint.js +88 -14
  152. package/dist/flows/lint.js.map +1 -1
  153. package/dist/flows/pack.js +18 -2
  154. package/dist/flows/pack.js.map +1 -1
  155. package/dist/flows/repair.js +71 -20
  156. package/dist/flows/repair.js.map +1 -1
  157. package/dist/flows/run.js +164 -51
  158. package/dist/flows/run.js.map +1 -1
  159. package/dist/flows/schema.js +57 -20
  160. package/dist/flows/schema.js.map +1 -1
  161. package/dist/flows/seedExec.js +5 -1
  162. package/dist/flows/seedExec.js.map +1 -1
  163. package/dist/ios/signing.js +32 -3
  164. package/dist/ios/signing.js.map +1 -1
  165. package/dist/issues/classify.js +12 -4
  166. package/dist/issues/classify.js.map +1 -1
  167. package/dist/issues/fingerprint.js +11 -5
  168. package/dist/issues/fingerprint.js.map +1 -1
  169. package/dist/issues/index.js +4 -20
  170. package/dist/issues/index.js.map +1 -1
  171. package/dist/issues/metrics.js +2 -1
  172. package/dist/issues/metrics.js.map +1 -1
  173. package/dist/issues/recurrence.js.map +1 -1
  174. package/dist/issues/report.js +8 -4
  175. package/dist/issues/report.js.map +1 -1
  176. package/dist/issues/reportBridge.js +36 -5
  177. package/dist/issues/reportBridge.js.map +1 -1
  178. package/dist/issues/schema.js.map +1 -1
  179. package/dist/issues/store.js.map +1 -1
  180. package/dist/lib/android.js +9 -1
  181. package/dist/lib/android.js.map +1 -1
  182. package/dist/lib/device.js +21 -5
  183. package/dist/lib/device.js.map +1 -1
  184. package/dist/lib/image.js +11 -2
  185. package/dist/lib/image.js.map +1 -1
  186. package/dist/lib/lockfile.js +60 -0
  187. package/dist/lib/lockfile.js.map +1 -0
  188. package/dist/lib/needsInput.js +6 -3
  189. package/dist/lib/needsInput.js.map +1 -1
  190. package/dist/lib/png.js +2 -1
  191. package/dist/lib/png.js.map +1 -1
  192. package/dist/lib/result.js +16 -1
  193. package/dist/lib/result.js.map +1 -1
  194. package/dist/lib/schemaHash.js +4 -2
  195. package/dist/lib/schemaHash.js.map +1 -1
  196. package/dist/lib/sensitive.js +3 -1
  197. package/dist/lib/sensitive.js.map +1 -1
  198. package/dist/lib/simctl.js.map +1 -1
  199. package/dist/lib/spawn.js +29 -10
  200. package/dist/lib/spawn.js.map +1 -1
  201. package/dist/lib/wda.js +27 -10
  202. package/dist/lib/wda.js.map +1 -1
  203. package/dist/lib/wdaConfig.js +12 -5
  204. package/dist/lib/wdaConfig.js.map +1 -1
  205. package/dist/lib/wdaTune.js +52 -23
  206. package/dist/lib/wdaTune.js.map +1 -1
  207. package/dist/mobileAudit/checks.js +141 -19
  208. package/dist/mobileAudit/checks.js.map +1 -1
  209. package/dist/mobileAudit/profiles.js +120 -19
  210. package/dist/mobileAudit/profiles.js.map +1 -1
  211. package/dist/mobileAudit/results.js +7 -2
  212. package/dist/mobileAudit/results.js.map +1 -1
  213. package/dist/mobileAudit/runner.js +144 -19
  214. package/dist/mobileAudit/runner.js.map +1 -1
  215. package/dist/oracle/auth.js.map +1 -1
  216. package/dist/oracle/failures.js +888 -131
  217. package/dist/oracle/failures.js.map +1 -1
  218. package/dist/oracle/health.js +90 -14
  219. package/dist/oracle/health.js.map +1 -1
  220. package/dist/oracle/locator.js +147 -18
  221. package/dist/oracle/locator.js.map +1 -1
  222. package/dist/oracle/record.js.map +1 -1
  223. package/dist/orchestration/goal.js +3 -9
  224. package/dist/orchestration/goal.js.map +1 -1
  225. package/dist/orchestration/testThis/execute.js +126 -0
  226. package/dist/orchestration/testThis/execute.js.map +1 -0
  227. package/dist/orchestration/testThis/pipeline.js +324 -0
  228. package/dist/orchestration/testThis/pipeline.js.map +1 -0
  229. package/dist/orchestration/testThis/plan.js +430 -0
  230. package/dist/orchestration/testThis/plan.js.map +1 -0
  231. package/dist/orchestration/testThis/terminal.js +118 -0
  232. package/dist/orchestration/testThis/terminal.js.map +1 -0
  233. package/dist/orchestration/testThis/types.js +12 -0
  234. package/dist/orchestration/testThis/types.js.map +1 -0
  235. package/dist/plan/plan.js.map +1 -1
  236. package/dist/prompts/index.js +3 -3
  237. package/dist/prompts/index.js.map +1 -1
  238. package/dist/report/coverage.js +0 -1
  239. package/dist/report/coverage.js.map +1 -1
  240. package/dist/report/evidence.js.map +1 -1
  241. package/dist/report/export.js +59 -37
  242. package/dist/report/export.js.map +1 -1
  243. package/dist/report/flake.js +13 -14
  244. package/dist/report/flake.js.map +1 -1
  245. package/dist/report/history.js +72 -38
  246. package/dist/report/history.js.map +1 -1
  247. package/dist/report/policy.js +11 -3
  248. package/dist/report/policy.js.map +1 -1
  249. package/dist/report/qaLevel.js +2 -8
  250. package/dist/report/qaLevel.js.map +1 -1
  251. package/dist/report/readiness.js +42 -12
  252. package/dist/report/readiness.js.map +1 -1
  253. package/dist/report/summary.js.map +1 -1
  254. package/dist/report/timing.js +23 -23
  255. package/dist/report/timing.js.map +1 -1
  256. package/dist/server.js +42 -37
  257. package/dist/server.js.map +1 -1
  258. package/dist/services/automationGenerate.js.map +1 -1
  259. package/dist/services/build.js +8 -2
  260. package/dist/services/build.js.map +1 -1
  261. package/dist/services/preflight.js +38 -6
  262. package/dist/services/preflight.js.map +1 -1
  263. package/dist/services/prepareAndroid.js +46 -10
  264. package/dist/services/prepareAndroid.js.map +1 -1
  265. package/dist/services/prepareIos.js +72 -10
  266. package/dist/services/prepareIos.js.map +1 -1
  267. package/dist/services/report.js +173 -37
  268. package/dist/services/report.js.map +1 -1
  269. package/dist/services/smoke.js +36 -6
  270. package/dist/services/smoke.js.map +1 -1
  271. package/dist/services/suiteGenerate.js +16 -4
  272. package/dist/services/suiteGenerate.js.map +1 -1
  273. package/dist/services/testSuiteKnowledge.js +38 -6
  274. package/dist/services/testSuiteKnowledge.js.map +1 -1
  275. package/dist/session/attach.js +16 -0
  276. package/dist/session/attach.js.map +1 -1
  277. package/dist/session/processRegistry.js +164 -0
  278. package/dist/session/processRegistry.js.map +1 -0
  279. package/dist/session/progress.js +1 -1
  280. package/dist/session/progress.js.map +1 -1
  281. package/dist/session/store.js +216 -43
  282. package/dist/session/store.js.map +1 -1
  283. package/dist/snapshot/overlays.js +7 -2
  284. package/dist/snapshot/overlays.js.map +1 -1
  285. package/dist/snapshot/parse.js +2 -8
  286. package/dist/snapshot/parse.js.map +1 -1
  287. package/dist/snapshot/present.js +27 -3
  288. package/dist/snapshot/present.js.map +1 -1
  289. package/dist/state/profile.js +36 -9
  290. package/dist/state/profile.js.map +1 -1
  291. package/dist/suite/compile.js +7 -2
  292. package/dist/suite/compile.js.map +1 -1
  293. package/dist/suite/lint.js +21 -3
  294. package/dist/suite/lint.js.map +1 -1
  295. package/dist/suite/pom.js +26 -4
  296. package/dist/suite/pom.js.map +1 -1
  297. package/dist/suite/testcase.js +6 -4
  298. package/dist/suite/testcase.js.map +1 -1
  299. package/dist/testSuite/exporter.js +6 -2
  300. package/dist/testSuite/exporter.js.map +1 -1
  301. package/dist/testSuite/generator.js +31 -6
  302. package/dist/testSuite/generator.js.map +1 -1
  303. package/dist/testSuite/issueLinks.js.map +1 -1
  304. package/dist/testSuite/lint.js +1 -1
  305. package/dist/testSuite/lint.js.map +1 -1
  306. package/dist/testSuite/merge.js +8 -2
  307. package/dist/testSuite/merge.js.map +1 -1
  308. package/dist/testSuite/schema.js +16 -11
  309. package/dist/testSuite/schema.js.map +1 -1
  310. package/dist/testSuite/store.js +9 -1
  311. package/dist/testSuite/store.js.map +1 -1
  312. package/dist/tools/act.js +107 -43
  313. package/dist/tools/act.js.map +1 -1
  314. package/dist/tools/agent.js +123 -25
  315. package/dist/tools/agent.js.map +1 -1
  316. package/dist/tools/appControl.js +16 -3
  317. package/dist/tools/appControl.js.map +1 -1
  318. package/dist/tools/appMap.js +331 -63
  319. package/dist/tools/appMap.js.map +1 -1
  320. package/dist/tools/assertVisual.js +12 -2
  321. package/dist/tools/assertVisual.js.map +1 -1
  322. package/dist/tools/automationGenerate.js +276 -241
  323. package/dist/tools/automationGenerate.js.map +1 -1
  324. package/dist/tools/build.js +92 -41
  325. package/dist/tools/build.js.map +1 -1
  326. package/dist/tools/bundletool.js +139 -28
  327. package/dist/tools/bundletool.js.map +1 -1
  328. package/dist/tools/capabilities.js +45 -71
  329. package/dist/tools/capabilities.js.map +1 -1
  330. package/dist/tools/clearOverlay.js +45 -8
  331. package/dist/tools/clearOverlay.js.map +1 -1
  332. package/dist/tools/detectContext.js +1 -1
  333. package/dist/tools/detectContext.js.map +1 -1
  334. package/dist/tools/device.js +44 -7
  335. package/dist/tools/device.js.map +1 -1
  336. package/dist/tools/doctor.js +38 -15
  337. package/dist/tools/doctor.js.map +1 -1
  338. package/dist/tools/explore.js +96 -22
  339. package/dist/tools/explore.js.map +1 -1
  340. package/dist/tools/featureTesting.js +153 -114
  341. package/dist/tools/featureTesting.js.map +1 -1
  342. package/dist/tools/firstRun.js +95 -60
  343. package/dist/tools/firstRun.js.map +1 -1
  344. package/dist/tools/flow.js +236 -101
  345. package/dist/tools/flow.js.map +1 -1
  346. package/dist/tools/flowGenerate.js +51 -47
  347. package/dist/tools/flowGenerate.js.map +1 -1
  348. package/dist/tools/flowRepair.js +24 -4
  349. package/dist/tools/flowRepair.js.map +1 -1
  350. package/dist/tools/generate.js +236 -0
  351. package/dist/tools/generate.js.map +1 -0
  352. package/dist/tools/getArtifact.js +21 -3
  353. package/dist/tools/getArtifact.js.map +1 -1
  354. package/dist/tools/health.js +3 -1
  355. package/dist/tools/health.js.map +1 -1
  356. package/dist/tools/history.js +40 -10
  357. package/dist/tools/history.js.map +1 -1
  358. package/dist/tools/ios.js +157 -25
  359. package/dist/tools/ios.js.map +1 -1
  360. package/dist/tools/issues.js +19 -150
  361. package/dist/tools/issues.js.map +1 -1
  362. package/dist/tools/jobs.js +23 -4
  363. package/dist/tools/jobs.js.map +1 -1
  364. package/dist/tools/locator.js +36 -8
  365. package/dist/tools/locator.js.map +1 -1
  366. package/dist/tools/metro.js +61 -16
  367. package/dist/tools/metro.js.map +1 -1
  368. package/dist/tools/mobileAudit.js +67 -15
  369. package/dist/tools/mobileAudit.js.map +1 -1
  370. package/dist/tools/network.js +6 -1
  371. package/dist/tools/network.js.map +1 -1
  372. package/dist/tools/note.js +20 -6
  373. package/dist/tools/note.js.map +1 -1
  374. package/dist/tools/permissions.js +31 -6
  375. package/dist/tools/permissions.js.map +1 -1
  376. package/dist/tools/plan.js +5 -2
  377. package/dist/tools/plan.js.map +1 -1
  378. package/dist/tools/prepareIosTarget.js +31 -8
  379. package/dist/tools/prepareIosTarget.js.map +1 -1
  380. package/dist/tools/prepareTarget.js +97 -18
  381. package/dist/tools/prepareTarget.js.map +1 -1
  382. package/dist/tools/report.js +24 -5
  383. package/dist/tools/report.js.map +1 -1
  384. package/dist/tools/resolveArtifact.js +9 -5
  385. package/dist/tools/resolveArtifact.js.map +1 -1
  386. package/dist/tools/resolveTarget.js +11 -2
  387. package/dist/tools/resolveTarget.js.map +1 -1
  388. package/dist/tools/screenInfo.js +8 -2
  389. package/dist/tools/screenInfo.js.map +1 -1
  390. package/dist/tools/screenRecord.js +74 -11
  391. package/dist/tools/screenRecord.js.map +1 -1
  392. package/dist/tools/screenshot.js +23 -6
  393. package/dist/tools/screenshot.js.map +1 -1
  394. package/dist/tools/seed.js +38 -6
  395. package/dist/tools/seed.js.map +1 -1
  396. package/dist/tools/smoke.js +16 -3
  397. package/dist/tools/smoke.js.map +1 -1
  398. package/dist/tools/snapshot.js +59 -6
  399. package/dist/tools/snapshot.js.map +1 -1
  400. package/dist/tools/startSession.js +54 -9
  401. package/dist/tools/startSession.js.map +1 -1
  402. package/dist/tools/state.js +62 -15
  403. package/dist/tools/state.js.map +1 -1
  404. package/dist/tools/suite.js +350 -298
  405. package/dist/tools/suite.js.map +1 -1
  406. package/dist/tools/testSuite.js +94 -28
  407. package/dist/tools/testSuite.js.map +1 -1
  408. package/dist/tools/testThis.js +30 -793
  409. package/dist/tools/testThis.js.map +1 -1
  410. package/dist/tools/visual.js +124 -18
  411. package/dist/tools/visual.js.map +1 -1
  412. package/dist/tools/visualText.js +51 -6
  413. package/dist/tools/visualText.js.map +1 -1
  414. package/dist/tools/wait.js +18 -3
  415. package/dist/tools/wait.js.map +1 -1
  416. package/dist/tools/wda.js +157 -29
  417. package/dist/tools/wda.js.map +1 -1
  418. package/dist/version.js +17 -49
  419. package/dist/version.js.map +1 -1
  420. package/dist/visual/ocr.js +12 -2
  421. package/dist/visual/ocr.js.map +1 -1
  422. package/dist/visual/provider.js +7 -3
  423. package/dist/visual/provider.js.map +1 -1
  424. package/docs/mcp-server.md +44 -4
  425. package/docs/tools.md +42 -100
  426. package/package.json +14 -5
  427. package/dist/appMap/diff.js +0 -60
  428. package/dist/appMap/diff.js.map +0 -1
  429. package/dist/automation/maestroIr.js +0 -282
  430. package/dist/automation/maestroIr.js.map +0 -1
  431. package/dist/interop/maestro.js +0 -151
  432. package/dist/interop/maestro.js.map +0 -1
  433. package/dist/tools/idling.js +0 -104
  434. package/dist/tools/idling.js.map +0 -1
  435. package/dist/tools/inputCapabilities.js +0 -30
  436. package/dist/tools/inputCapabilities.js.map +0 -1
  437. package/dist/tools/maestro.js +0 -78
  438. package/dist/tools/maestro.js.map +0 -1
@@ -1,11 +1,13 @@
1
1
  // qa_flow_check + qa_flow_run (PHASE3-PLAN §3.4) — turn exploration into repeatable QA.
2
2
  // Flows live as .swipium/flows/*.yaml. check = parse + static validation (no device).
3
- // run = the orchestrator (src/flows/run.ts), reporting the exact failing step + evidence.
3
+ // run (mode:"run", default) = the orchestrator (src/flows/run.ts), reporting the exact failing
4
+ // step + evidence. run (mode:"plan", 1.5.0 consolidation) = read-only execution preview:
5
+ // compile the flow against backend capabilities without touching a device.
4
6
  import { z } from 'zod';
5
7
  import { createHash } from 'node:crypto';
6
8
  import { existsSync, readFileSync } from 'node:fs';
7
9
  import { join, isAbsolute } from 'node:path';
8
- import { qaOk, qaError, qaStop } from '../lib/result.js';
10
+ import { qaOk, qaError, qaStop, qaAnnotate } from '../lib/result.js';
9
11
  import { parseFlow } from '../flows/schema.js';
10
12
  import { lintFlowObjectWithOptions } from '../flows/lint.js';
11
13
  import { runFlow } from '../flows/run.js';
@@ -50,16 +52,19 @@ function loadFlowSource(root, flow, flowYaml) {
50
52
  return { error: `Flow not found. Looked for: ${candidates.join(', ') || '(no project root)'}` };
51
53
  }
52
54
  const MUTATING_FLOW_STEPS = new Set(['networkOffline', 'networkOnline', 'seed', 'restartApp']);
53
- const EXTERNAL_PROVIDER_FLOW_STEPS = new Set(['tapOcrText', 'assertOcrText']);
54
55
  function stable(value) {
55
56
  if (Array.isArray(value))
56
57
  return value.map(stable);
57
58
  if (value && typeof value === 'object')
58
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, stable(v)]));
59
+ return Object.fromEntries(Object.entries(value)
60
+ .sort(([a], [b]) => a.localeCompare(b))
61
+ .map(([k, v]) => [k, stable(v)]));
59
62
  return value;
60
63
  }
61
64
  function fullHash(value) {
62
- return createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
65
+ return createHash('sha256')
66
+ .update(JSON.stringify(stable(value)))
67
+ .digest('hex');
63
68
  }
64
69
  function hash(value) {
65
70
  return fullHash(value).slice(0, 16);
@@ -92,7 +97,8 @@ function flowMutationAffects(flow, session, source, repeat, externalProviders =
92
97
  function flowMutationRisk(affects) {
93
98
  if (affects.mutations.some((m) => m.kind === 'seed' && m.seedType === 'script'))
94
99
  return 'high';
95
- if (affects.externalProviders.length || affects.mutations.some((m) => m.kind === 'networkOffline' || m.kind === 'networkOnline' || m.kind === 'seed'))
100
+ if (affects.externalProviders.length ||
101
+ affects.mutations.some((m) => m.kind === 'networkOffline' || m.kind === 'networkOnline' || m.kind === 'seed'))
96
102
  return 'medium';
97
103
  return 'low';
98
104
  }
@@ -108,28 +114,44 @@ function ocrSteps(flow) {
108
114
  export function registerFlow(server, sessions) {
109
115
  server.registerTool('qa_flow_check', {
110
116
  title: 'Check a flow',
111
- description: 'Parse and statically validate a Swipium flow (a .swipium/flows/*.yaml authored as name + steps) WITHOUT running it. Reports syntax/schema errors with the offending step, plus warnings (e.g. selector resolvability is only known at run time). Provide `flow` (a name under .swipium/flows, or a path) or `flowYaml` (inline). A sessionId lets it resolve names against that session\'s project root.',
117
+ description: 'Parse and statically validate a Swipium flow (a .swipium/flows/*.yaml authored as name + steps) WITHOUT running it. Reports syntax/schema errors with the offending step, plus warnings (e.g. selector resolvability is only known at run time). Provide `flow` (a name under .swipium/flows, or a path) or `flowYaml` (inline). A sessionId lets it resolve names against that session\'s project root. Related: qa_flow_check is the STATIC LINT of the YAML; qa_flow_run mode:"plan" is the read-only EXECUTION PREVIEW (which backends can run it, step-by-step support); qa_flow_run mode:"run" executes.',
112
118
  inputSchema: {
113
119
  sessionId: z.string().optional(),
114
120
  flow: z.string().optional().describe('Flow name under .swipium/flows, or a path to a .yaml file.'),
115
121
  flowYaml: z.string().optional().describe('Inline flow YAML (instead of a file).'),
116
- platform: z.enum(['android', 'ios', 'cross-platform']).optional().describe('Optional authoring target for platform-aware warnings.'),
117
- ci: z.boolean().optional().describe('When true, add CI preflight warnings such as missing variables and mutating steps not allowed by policy.'),
122
+ platform: z
123
+ .enum(['android', 'ios', 'cross-platform'])
124
+ .optional()
125
+ .describe('Optional authoring target for platform-aware warnings.'),
126
+ ci: z
127
+ .boolean()
128
+ .optional()
129
+ .describe('When true, add CI preflight warnings such as missing variables and mutating steps not allowed by policy.'),
118
130
  },
119
131
  }, async ({ sessionId, flow, flowYaml, platform, ci }) => {
120
132
  const root = sessionId ? sessions.get(sessionId)?.root : undefined;
121
133
  const src = loadFlowSource(root, flow, flowYaml);
122
134
  if (src.error)
123
- return qaError({ what: src.error, changedState: false, retrySafe: true, nextSteps: ['Pass an existing flow name/path or inline flowYaml.'] });
135
+ return qaError({
136
+ what: src.error,
137
+ changedState: false,
138
+ retrySafe: true,
139
+ nextSteps: ['Pass an existing flow name/path or inline flowYaml.'],
140
+ });
124
141
  const { flow: parsed, errors } = parseFlow(src.yamlText);
125
142
  if (errors.length || !parsed) {
126
- return qaError({ what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'})`, changedState: false, retrySafe: true, nextSteps: ['Fix the listed errors and re-check.'] }, { source: src.source, errors });
143
+ return qaError({
144
+ what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'})`,
145
+ changedState: false,
146
+ retrySafe: true,
147
+ nextSteps: ['Fix the listed errors and re-check.'],
148
+ }, { source: src.source, errors });
127
149
  }
128
150
  const allSteps = [...parsed.setup, ...parsed.steps, ...parsed.teardown];
129
151
  const STRUCTURED_KINDS = new Set(['tap', 'assertVisible', 'assertNotVisible', 'scrollTo', 'waitForVisible', 'inputText']);
130
152
  const warnings = [];
131
153
  if (!parsed.appId)
132
- warnings.push('No appId — a prepareTarget step will rely on the session\'s prepared appId.');
154
+ warnings.push("No appId — a prepareTarget step will rely on the session's prepared appId.");
133
155
  if (allSteps.some((s) => s.kind === 'tap' && s.selector.startsWith('@')))
134
156
  warnings.push('Uses @ref selectors — refs are run-time only; prefer text/id selectors for durable flows.');
135
157
  if (src.yamlText.includes('${'))
@@ -151,14 +173,21 @@ export function registerFlow(server, sessions) {
151
173
  warnings.push('mode:visual but the flow uses structured steps (tap/assertVisible/…) — those need a UI tree and will fail on a visual-only screen.');
152
174
  }
153
175
  for (const s of allSteps) {
154
- const text = s.kind === 'tap' ? s.selector : s.kind === 'inputText' ? s.into ?? s.value : 'query' in s && typeof s.query === 'string' ? s.query : '';
176
+ const text = s.kind === 'tap'
177
+ ? s.selector
178
+ : s.kind === 'inputText'
179
+ ? (s.into ?? s.value)
180
+ : 'query' in s && typeof s.query === 'string'
181
+ ? s.query
182
+ : '';
155
183
  if (!text)
156
184
  continue;
157
185
  if (inferredPlatform === 'ios' && /resource-id=|uiautomator|android\./i.test(text))
158
186
  warnings.push(`Android-specific locator in iOS flow: "${text}". Prefer accessibilityIdentifier/name/label on iOS.`);
159
187
  if (inferredPlatform === 'android' && /class chain|predicate string|XCUIElementType|accessibility id=/i.test(text))
160
188
  warnings.push(`iOS-specific locator in Android flow: "${text}". Prefer resource-id/content-desc/text on Android.`);
161
- if (inferredPlatform === 'cross-platform' && /(resource-id=|uiautomator|android\.|accessibility id=|class chain|predicate string|XCUIElementType|xpath=|^\/\/)/i.test(text)) {
189
+ if (inferredPlatform === 'cross-platform' &&
190
+ /(resource-id=|uiautomator|android\.|accessibility id=|class chain|predicate string|XCUIElementType|xpath=|^\/\/)/i.test(text)) {
162
191
  warnings.push(`Platform-specific or brittle locator in cross-platform flow: "${text}". Prefer shared text/testID/accessibility labels.`);
163
192
  }
164
193
  }
@@ -171,7 +200,10 @@ export function registerFlow(server, sessions) {
171
200
  for (const v of missingVars)
172
201
  warnings.push(`CI variable: ${v.variable} is required by ${v.flow} step ${v.step} but is not set in the environment.`);
173
202
  }
174
- const lintFindings = lintFlowObjectWithOptions(src.source ?? 'inline', parsed, { platform, policy: ci ? (root ? loadPolicy(root) : null) : undefined });
203
+ const lintFindings = lintFlowObjectWithOptions(src.source ?? 'inline', parsed, {
204
+ platform,
205
+ policy: ci ? (root ? loadPolicy(root) : null) : undefined,
206
+ });
175
207
  for (const f of lintFindings) {
176
208
  const prefix = f.severity === 'error' ? 'Lint error' : 'Lint warning';
177
209
  warnings.push(`${prefix} ${f.code}${f.step ? ` step ${f.step}` : ''}: ${f.message}`);
@@ -179,23 +211,65 @@ export function registerFlow(server, sessions) {
179
211
  // Image-template steps: verify the referenced files exist (best-effort, when a root is known).
180
212
  if (root) {
181
213
  for (const s of allSteps) {
182
- if ((s.kind === 'tapImage' || s.kind === 'assertImage')) {
214
+ if (s.kind === 'tapImage' || s.kind === 'assertImage') {
183
215
  const p = isAbsolute(s.template) ? s.template : join(root, s.template);
184
216
  if (!existsSync(p))
185
217
  warnings.push(`image template not found: ${s.template} (resolve relative to the project root).`);
186
218
  }
187
219
  }
188
220
  }
189
- return qaOk({ valid: true, source: src.source, name: parsed.name, appId: parsed.appId ?? null, mode: parsed.mode, budgetProfile: parsed.budgetProfile ?? null, stepCount: parsed.steps.length, setupCount: parsed.setup.length, teardownCount: parsed.teardown.length, fixtures: parsed.fixtures, warnings, lintFindings }, `✅ flow "${parsed.name}" is valid — ${parsed.steps.length} steps (mode=${parsed.mode}${parsed.setup.length ? `, ${parsed.setup.length} setup` : ''}${parsed.teardown.length ? `, ${parsed.teardown.length} teardown` : ''})${warnings.length ? `\nwarnings:\n - ${warnings.join('\n - ')}` : ''}`);
221
+ return qaOk({
222
+ valid: true,
223
+ source: src.source,
224
+ name: parsed.name,
225
+ appId: parsed.appId ?? null,
226
+ mode: parsed.mode,
227
+ budgetProfile: parsed.budgetProfile ?? null,
228
+ stepCount: parsed.steps.length,
229
+ setupCount: parsed.setup.length,
230
+ teardownCount: parsed.teardown.length,
231
+ fixtures: parsed.fixtures,
232
+ warnings,
233
+ lintFindings,
234
+ }, `✅ flow "${parsed.name}" is valid — ${parsed.steps.length} steps (mode=${parsed.mode}${parsed.setup.length ? `, ${parsed.setup.length} setup` : ''}${parsed.teardown.length ? `, ${parsed.teardown.length} teardown` : ''})${warnings.length ? `\nwarnings:\n - ${warnings.join('\n - ')}` : ''}`);
190
235
  });
191
- server.registerTool('qa_flow_plan', {
192
- title: 'Plan a flow for a backend',
193
- description: 'Compile a Swipium flow into a deterministic, backend-specific automation plan WITHOUT touching a device (Automation Kernel V2). For each backend it answers: can this flow run, which exact selectors/gestures/waits will it use, and what to fix if it cannot. Each step is classified native | supported_with_fallback | visual_only | unsupported, with the exact missing capability and a concrete next step named for anything blocked. Returns structured data plus a natural-language message a coding agent can repeat to the user (ready | candidate | blocked). Provide `flow` (name/path) or `flowYaml`. Without `backend`, it plans across Android DirectDriver, iOS raw simulator, iOS WDA, Appium UiAutomator2, and Appium XCUITest; with a sessionId it also reports the attached backend.',
236
+ server.registerTool('qa_flow_run', {
237
+ title: 'Run a flow (or preview its execution plan)',
238
+ description: 'Run a Swipium flow against the prepared app and report the result — or, with mode:"plan", preview the execution WITHOUT touching a device. mode:"run" (default) drives steps server-side: tap/tapAt/tapImage, inputText (incl. { into, text } to focus a named field), assertVisible/assertNotVisible/assertImage/assertVisual/assertDiff, swipe (device-relative { direction, area, distance }), scrollTo, press, openUrl, wait/waitForIdle/waitForVisible, clearOverlay, networkOffline/Online, restartApp, seed, note, screenshot — with optional setup/teardown (teardown always runs) and flow mode structured|visual|auto. Fail-fast, NO auto-retry of mutating steps; on failure returns the exact failing step + screenshot + typed failureCode + health. mode:"plan" (read-only) compiles the flow into a deterministic backend-specific automation plan: per backend, can it run, each step classified native | supported_with_fallback | visual_only | unsupported with the missing capability, and an agent-repeatable ready | candidate | blocked message. Provide `flow` (name/path) or `flowYaml`, plus `variables` for any ${VAR}. Related: qa_flow_check is the STATIC LINT of the YAML; mode:"plan" here is the read-only execution preview.',
194
239
  inputSchema: {
195
- sessionId: z.string().optional(),
240
+ mode: z
241
+ .enum(['plan', 'run'])
242
+ .optional()
243
+ .describe("run (default): execute the flow on the session's prepared device. plan: READ-ONLY execution preview — no device is touched; compiles the flow against backend capabilities (Android direct, iOS raw simulator, iOS WDA, Appium UiAutomator2/XCUITest) and reports what would run, what falls back, and what is blocked."),
244
+ sessionId: z
245
+ .string()
246
+ .optional()
247
+ .describe('REQUIRED for mode:"run" (needs a prepared device). Optional for mode:"plan" — it resolves flow names against the session\'s project root and marks the attached backend.'),
196
248
  flow: z.string().optional().describe('Flow name under .swipium/flows, or a path to a .yaml file.'),
197
249
  flowYaml: z.string().optional().describe('Inline flow YAML (instead of a file).'),
198
- backend: z.enum(['android-direct', 'ios-raw-simulator', 'ios-wda', 'appium-uiautomator2', 'appium-xcuitest']).optional().describe('Plan only for this backend. Omit to plan across all five (the pre-run "can this run on Android and iOS?" answer).'),
250
+ variables: z
251
+ .record(z.string())
252
+ .optional()
253
+ .describe('(mode:"run" only) Values for ${VAR} placeholders (merged over process.env). Credential-looking names are auto-redacted.'),
254
+ repeat: z
255
+ .number()
256
+ .int()
257
+ .min(1)
258
+ .max(10)
259
+ .optional()
260
+ .describe('(mode:"run" only) Run the flow N times for flake detection (default 1). Reports pass-rate + a deterministic-pass | deterministic-fail | flaky classification.'),
261
+ consentId: z
262
+ .string()
263
+ .optional()
264
+ .describe('(mode:"run" only) Consent token returned when the flow contains mutating steps or external visual-provider steps such as OCR.'),
265
+ approve: z
266
+ .boolean()
267
+ .optional()
268
+ .describe('(mode:"run" only) Set true with consentId to execute after reviewing the exact flow hash, mutation list, and external provider command if present.'),
269
+ backend: z
270
+ .enum(['android-direct', 'ios-raw-simulator', 'ios-wda', 'appium-uiautomator2', 'appium-xcuitest'])
271
+ .optional()
272
+ .describe('(mode:"plan" only) Plan only for this backend. Omit to plan across all five (the pre-run "can this run on Android and iOS?" answer).'),
199
273
  appium: z
200
274
  .object({
201
275
  automationName: z.string().optional(),
@@ -203,113 +277,152 @@ export function registerFlow(server, sessions) {
203
277
  webviewContextsAvailable: z.boolean().optional(),
204
278
  })
205
279
  .optional()
206
- .describe('Appium session hints that refine the Appium backend capability picture (driver family, WebView contexts).'),
280
+ .describe('(mode:"plan" only) Appium session hints that refine the Appium backend capability picture (driver family, WebView contexts).'),
207
281
  },
208
- }, async ({ sessionId, flow, flowYaml, backend, appium }) => {
209
- const session = sessionId ? sessions.get(sessionId) : undefined;
210
- const root = session?.root;
211
- const src = loadFlowSource(root, flow, flowYaml);
212
- if (src.error)
213
- return qaError({ what: src.error, changedState: false, retrySafe: true, nextSteps: ['Pass an existing flow name/path or inline flowYaml.'] });
214
- const { flow: parsed, errors } = parseFlow(src.yamlText);
215
- if (errors.length || !parsed) {
216
- return qaError({ what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'}) — run qa_flow_check`, changedState: false, retrySafe: true, nextSteps: ['Fix the listed errors and re-plan.'] }, { source: src.source, errors });
282
+ }, async ({ mode, sessionId, flow, flowYaml, variables, repeat, consentId, approve, backend, appium }) => {
283
+ const effectiveMode = mode ?? 'run';
284
+ const notes = [];
285
+ // ---- mode:"plan" — read-only execution preview (merged twin, 1.5.0). ----
286
+ if (effectiveMode === 'plan') {
287
+ const ignored = [
288
+ variables !== undefined && 'variables',
289
+ repeat !== undefined && 'repeat',
290
+ consentId !== undefined && 'consentId',
291
+ approve !== undefined && 'approve',
292
+ ].filter((x) => !!x);
293
+ if (ignored.length)
294
+ notes.push(`ignored parameter(s) not applicable to mode:"plan": ${ignored.join(', ')} — re-run with mode:"run" to execute`);
295
+ const session = sessionId ? sessions.get(sessionId) : undefined;
296
+ const root = session?.root;
297
+ const src = loadFlowSource(root, flow, flowYaml);
298
+ if (src.error)
299
+ return qaAnnotate(qaError({
300
+ what: src.error,
301
+ changedState: false,
302
+ retrySafe: true,
303
+ nextSteps: ['Pass an existing flow name/path or inline flowYaml.'],
304
+ }), notes);
305
+ const { flow: parsed, errors } = parseFlow(src.yamlText);
306
+ if (errors.length || !parsed) {
307
+ return qaAnnotate(qaError({
308
+ what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'}) — run qa_flow_check`,
309
+ changedState: false,
310
+ retrySafe: true,
311
+ nextSteps: ['Fix the listed errors and re-plan.'],
312
+ }, { source: src.source, errors }), notes);
313
+ }
314
+ const appiumHints = appium ? { ...appium } : undefined;
315
+ const attachedBackend = session?.driver?.kind ? backendForDriverKind(session.driver.kind, appiumHints) : undefined;
316
+ // Which backends to plan for: an explicit one, else all concrete backends (so the answer covers
317
+ // Android + iOS even with no device attached — the pre-run "can this run?" product question).
318
+ const targets = backend ? [backend] : [...ALL_BACKENDS];
319
+ if (attachedBackend && attachedBackend !== 'unknown' && !targets.includes(attachedBackend))
320
+ targets.unshift(attachedBackend);
321
+ const plans = targets.map((b) => {
322
+ const caps = backendCapabilities(b, appiumHints);
323
+ const plan = compileAutomationPlan(parsed, caps);
324
+ const readiness = buildReadiness(plan);
325
+ return {
326
+ backend: b,
327
+ attached: b === attachedBackend,
328
+ status: readiness.status,
329
+ headline: readiness.headline,
330
+ executable: plan.executable,
331
+ supportedSteps: readiness.supportedSteps,
332
+ unsupportedSteps: readiness.unsupportedSteps,
333
+ fallbackUsed: readiness.fallbackUsed,
334
+ agentMessage: readiness.agentMessage,
335
+ developerMessage: readiness.developerMessage,
336
+ blockers: readiness.blockers,
337
+ warnings: readiness.warnings,
338
+ steps: plan.steps.map((s) => ({
339
+ index: s.index,
340
+ kind: s.action.kind,
341
+ target: s.action.note,
342
+ support: s.support,
343
+ reason: s.reason,
344
+ requiredCapability: s.requiredCapability,
345
+ })),
346
+ };
347
+ });
348
+ const summaryLines = plans.map((p) => {
349
+ const head = `${STATUS_ICON[p.status] ?? '•'} ${p.backend}${p.attached ? ' (attached)' : ''}: ${p.status} — ${p.supportedSteps} supported, ${p.unsupportedSteps} unsupported`;
350
+ return `${head}\n ${p.agentMessage}`;
351
+ });
352
+ const summary = `flow "${parsed.name}" — automation plan across ${plans.length} backend(s) (mode:"plan" — read-only, nothing executed):\n${summaryLines.join('\n')}`;
353
+ return qaAnnotate(qaOk({ flow: parsed.name, appId: parsed.appId ?? null, mode: parsed.mode, source: src.source, plans }, summary), notes);
217
354
  }
218
- const appiumHints = appium ? { ...appium } : undefined;
219
- const attachedBackend = session?.driver?.kind ? backendForDriverKind(session.driver.kind, appiumHints) : undefined;
220
- // Which backends to plan for: an explicit one, else all concrete backends (so the answer covers
221
- // Android + iOS even with no device attached — the pre-run "can this run?" product question).
222
- const targets = backend ? [backend] : [...ALL_BACKENDS];
223
- if (attachedBackend && attachedBackend !== 'unknown' && !targets.includes(attachedBackend))
224
- targets.unshift(attachedBackend);
225
- const plans = targets.map((b) => {
226
- const caps = backendCapabilities(b, appiumHints);
227
- const plan = compileAutomationPlan(parsed, caps);
228
- const readiness = buildReadiness(plan);
229
- return {
230
- backend: b,
231
- attached: b === attachedBackend,
232
- status: readiness.status,
233
- headline: readiness.headline,
234
- executable: plan.executable,
235
- supportedSteps: readiness.supportedSteps,
236
- unsupportedSteps: readiness.unsupportedSteps,
237
- fallbackUsed: readiness.fallbackUsed,
238
- agentMessage: readiness.agentMessage,
239
- developerMessage: readiness.developerMessage,
240
- blockers: readiness.blockers,
241
- warnings: readiness.warnings,
242
- steps: plan.steps.map((s) => ({ index: s.index, kind: s.action.kind, target: s.action.note, support: s.support, reason: s.reason, requiredCapability: s.requiredCapability })),
243
- };
244
- });
245
- const summaryLines = plans.map((p) => {
246
- const head = `${STATUS_ICON[p.status] ?? '•'} ${p.backend}${p.attached ? ' (attached)' : ''}: ${p.status} — ${p.supportedSteps} supported, ${p.unsupportedSteps} unsupported`;
247
- return `${head}\n ${p.agentMessage}`;
248
- });
249
- const summary = `flow "${parsed.name}" — automation plan across ${plans.length} backend(s):\n${summaryLines.join('\n')}`;
250
- return qaOk({ flow: parsed.name, appId: parsed.appId ?? null, mode: parsed.mode, source: src.source, plans }, summary);
251
- });
252
- server.registerTool('qa_flow_run', {
253
- title: 'Run a flow',
254
- description: 'Run a Swipium flow against the prepared app and report the result. Drives steps server-side: tap/tapAt/tapImage, inputText (incl. { into, text } to focus a named field), assertVisible/assertNotVisible/assertImage/assertVisual/assertDiff, swipe (device-relative { direction, area, distance }), scrollTo, press, openUrl, wait/waitForIdle/waitForVisible, clearOverlay, networkOffline/Online, restartApp, seed, note, screenshot — with optional setup/teardown (teardown always runs) and mode structured|visual|auto. Fail-fast, NO auto-retry of mutating steps; on failure returns the exact failing step + screenshot + typed failureCode + health. Provide `flow` (name/path) or `flowYaml`, plus `variables` for any ${VAR}.',
255
- inputSchema: {
256
- sessionId: z.string(),
257
- flow: z.string().optional(),
258
- flowYaml: z.string().optional(),
259
- variables: z.record(z.string()).optional().describe('Values for ${VAR} placeholders (merged over process.env). Credential-looking names are auto-redacted.'),
260
- repeat: z.number().int().min(1).max(10).optional().describe('Run the flow N times for flake detection (default 1). Reports pass-rate + a deterministic-pass | deterministic-fail | flaky classification.'),
261
- consentId: z.string().optional().describe('Consent token returned when the flow contains mutating steps or external visual-provider steps such as OCR.'),
262
- approve: z.boolean().optional().describe('Set true with consentId to execute after reviewing the exact flow hash, mutation list, and external provider command if present.'),
263
- },
264
- }, async ({ sessionId, flow, flowYaml, variables, repeat, consentId, approve }) => {
265
- const session = sessions.get(sessionId);
355
+ // ---- mode:"run" — execute on the prepared device. ----
356
+ const runIgnored = [backend !== undefined && 'backend', appium !== undefined && 'appium'].filter((x) => !!x);
357
+ if (runIgnored.length)
358
+ notes.push(`ignored parameter(s) not applicable to mode:"run": ${runIgnored.join(', ')} — they refine the mode:"plan" preview`);
359
+ const session = sessionId ? sessions.get(sessionId) : undefined;
266
360
  const { driver } = session ? await getDriver(session) : { driver: undefined };
267
361
  if (!session || !driver) {
268
- return qaError({ what: 'No device attached to this session', changedState: false, retrySafe: true, nextSteps: ['Call qa_prepare_target / qa_ios boot first, then qa_flow_run.'] });
362
+ return qaAnnotate(qaError({
363
+ what: sessionId
364
+ ? 'No device attached to this session'
365
+ : 'qa_flow_run mode:"run" needs a sessionId with a prepared device (mode:"plan" works without one)',
366
+ changedState: false,
367
+ retrySafe: true,
368
+ nextSteps: ['Call qa_prepare_target / qa_ios boot first, then qa_flow_run.'],
369
+ }), notes);
269
370
  }
270
371
  const src = loadFlowSource(session.root, flow, flowYaml);
271
372
  if (src.error)
272
- return qaError({ what: src.error, changedState: false, retrySafe: true, nextSteps: ['Pass an existing flow name/path or inline flowYaml.'] });
373
+ return qaAnnotate(qaError({
374
+ what: src.error,
375
+ changedState: false,
376
+ retrySafe: true,
377
+ nextSteps: ['Pass an existing flow name/path or inline flowYaml.'],
378
+ }), notes);
273
379
  const { flow: parsed, errors } = parseFlow(src.yamlText);
274
380
  if (errors.length || !parsed) {
275
- return qaError({ what: `Flow is invalid — run qa_flow_check first`, changedState: false, retrySafe: true, nextSteps: ['Fix the flow and re-run.'] }, { errors });
381
+ return qaAnnotate(qaError({
382
+ what: `Flow is invalid — run qa_flow_check first`,
383
+ changedState: false,
384
+ retrySafe: true,
385
+ nextSteps: ['Fix the flow and re-run.'],
386
+ }, { errors }), notes);
276
387
  }
277
388
  // Backend/mode gate (NEXT-PLAN: catch unsupported combos before running). A structured-mode
278
389
  // flow needs a UI tree, which the iOS simulator and a visual-fallback session don't have.
279
390
  if (parsed.mode === 'structured' && (driver.kind === 'simulator' || session.mode === 'visual-fallback')) {
280
- return qaError({
391
+ return qaAnnotate(qaError({
281
392
  what: `This flow is mode:structured but ${driver.kind === 'simulator' ? 'the iOS simulator backend has no UI tree' : 'the session is in visual-fallback'}`,
282
393
  changedState: false,
283
394
  retrySafe: false,
284
395
  failureCode: 'BACKEND_UNSUPPORTED',
285
- nextSteps: ['Author the flow with `mode: visual` (or auto) and use image/visual steps (tapImage/assertImage/assertDiff/assertVisual).'],
286
- });
396
+ nextSteps: [
397
+ 'Author the flow with `mode: visual` (or auto) and use image/visual steps (tapImage/assertImage/assertDiff/assertVisual).',
398
+ ],
399
+ }), notes);
287
400
  }
288
401
  const budget = sessions.budgetStop(session);
289
402
  if (budget)
290
- return qaStop(budget, { counters: session.counters });
403
+ return qaAnnotate(qaStop(budget, { counters: session.counters }), notes);
291
404
  const runs = repeat ?? 1;
292
405
  const externalProviderSteps = ocrSteps(parsed);
293
406
  const externalProviders = [];
294
407
  if (externalProviderSteps.length) {
295
408
  if (session.sensitive) {
296
- return qaError({
409
+ return qaAnnotate(qaError({
297
410
  what: 'Sensitive mode refuses OCR flow steps because they would pass screenshots to an external provider',
298
411
  changedState: false,
299
412
  retrySafe: false,
300
413
  failureCode: 'UNSAFE_ACTION_REFUSED',
301
414
  nextSteps: ['Disable sensitive mode only for a safe, masked QA screen, or remove tapOcrText/assertOcrText from the flow.'],
302
- });
415
+ }), notes);
303
416
  }
304
417
  const command = configuredOcrCommand(session.root);
305
418
  if (!command) {
306
- return qaError({
419
+ return qaAnnotate(qaError({
307
420
  what: 'Flow uses OCR text steps, but OCR is not configured',
308
421
  changedState: false,
309
422
  retrySafe: true,
310
423
  failureCode: 'VISUAL_ONLY_SCREEN',
311
424
  nextSteps: ['Configure ocrCommand in .swipium/config.json or remove tapOcrText/assertOcrText from the flow.'],
312
- });
425
+ }), notes);
313
426
  }
314
427
  try {
315
428
  const preview = resolveVisualProvider(command, { image: '<screenshot>' }, 30000);
@@ -325,18 +438,24 @@ export function registerFlow(server, sessions) {
325
438
  });
326
439
  }
327
440
  catch (e) {
328
- return qaError({
441
+ return qaAnnotate(qaError({
329
442
  what: e instanceof GitScopeForbiddenError ? e.message : `Invalid OCR command template: ${String(e)}`,
330
443
  changedState: false,
331
444
  retrySafe: !(e instanceof GitScopeForbiddenError),
332
445
  failureCode: e instanceof GitScopeForbiddenError ? 'GIT_SCOPE_FORBIDDEN' : 'INVALID_FLOW',
333
- nextSteps: e instanceof GitScopeForbiddenError ? ['Run Git yourself outside Swipium; configure ocrCommand to use a non-Git executable.'] : ['Use an argv array in .swipium/config.json, e.g. ["node","ocr.js","{image}"].'],
334
- });
446
+ nextSteps: e instanceof GitScopeForbiddenError
447
+ ? ['Run Git yourself outside Swipium; configure ocrCommand to use a non-Git executable.']
448
+ : ['Use an argv array in .swipium/config.json, e.g. ["node","ocr.js","{image}"].'],
449
+ }), notes);
335
450
  }
336
451
  }
337
452
  const affects = flowMutationAffects(parsed, session, src.source, runs, externalProviders);
338
453
  const mutationRisk = flowMutationRisk(affects);
339
- let mutationConsent = { required: false, approved: true, payloadHash: affects.flowHash };
454
+ let mutationConsent = {
455
+ required: false,
456
+ approved: true,
457
+ payloadHash: affects.flowHash,
458
+ };
340
459
  const privileged = affects.mutations.length > 0 || affects.externalProviders.length > 0;
341
460
  if (privileged) {
342
461
  const payloadHash = fullHash(affects);
@@ -353,7 +472,7 @@ export function registerFlow(server, sessions) {
353
472
  const externalCommand = affects.externalProviders.length
354
473
  ? `; external provider: ${displayArgv(affects.externalProviders[0].argv ?? [])}`
355
474
  : '';
356
- return requireConsent({
475
+ return qaAnnotate(requireConsent({
357
476
  action: 'flow_mutation_run',
358
477
  risk: mutationRisk,
359
478
  exactCommand: `qa_flow_run ${parsed.name} (${[...affects.mutations.map((m) => `${m.step}:${m.kind}`), ...externalProviderSteps.map((s) => `${s.step}:${s.kind}`)].join(', ')})${externalCommand}`,
@@ -361,7 +480,7 @@ export function registerFlow(server, sessions) {
361
480
  explain: affects.externalProviders.length
362
481
  ? `Run flow "${parsed.name}" with external visual provider steps? OCR steps pass ${runs * externalProviderSteps.length} screenshot(s) to the configured provider and any mutating steps can change app/device/test state.`
363
482
  : `Run mutating flow "${parsed.name}"? Mutating steps can change app/device/test state and will be recorded in the mutation ledger.`,
364
- });
483
+ }), notes);
365
484
  }
366
485
  mutationConsent = { required: true, consentId, approved: true, payloadHash };
367
486
  sessions.recordMutation(session, {
@@ -383,15 +502,31 @@ export function registerFlow(server, sessions) {
383
502
  results.push(await runFlow(sessions, session, driver, parsed, { variables, mutationConsent }));
384
503
  }
385
504
  const flake = classifyFlakeResults(results, runs);
386
- return qaOk({ flow: parsed.name, runs: results.length, passes: flake.passed, fails: flake.failed, passRate: flake.passRate, classification: flake.classification, triage: flake.triage, results: results.map((r) => ({ passed: r.passed, failedAtStep: r.failedAtStep, reason: r.reason, failureCode: r.failureCode })) }, `flow "${parsed.name}" × ${results.length}: ${flake.passed} passed, ${flake.failed} failed → ${flake.classification === 'flaky' ? '⚠ FLAKY' : flake.classification === 'deterministic-pass' ? '✅ deterministic-pass' : '❌ deterministic-fail'} (${flake.passRate}% pass-rate); triage=${flake.triage.likelyCause}`);
505
+ return qaAnnotate(qaOk({
506
+ flow: parsed.name,
507
+ runs: results.length,
508
+ passes: flake.passed,
509
+ fails: flake.failed,
510
+ passRate: flake.passRate,
511
+ classification: flake.classification,
512
+ triage: flake.triage,
513
+ results: results.map((r) => ({
514
+ passed: r.passed,
515
+ failedAtStep: r.failedAtStep,
516
+ reason: r.reason,
517
+ failureCode: r.failureCode,
518
+ })),
519
+ }, `flow "${parsed.name}" × ${results.length}: ${flake.passed} passed, ${flake.failed} failed → ${flake.classification === 'flaky' ? '⚠ FLAKY' : flake.classification === 'deterministic-pass' ? '✅ deterministic-pass' : '❌ deterministic-fail'} (${flake.passRate}% pass-rate); triage=${flake.triage.likelyCause}`), notes);
387
520
  }
388
521
  const result = await runFlow(sessions, session, driver, parsed, { variables, mutationConsent });
389
522
  const head = `flow "${result.name}" ${result.passed ? '✅ PASSED' : `❌ FAILED at step ${result.failedAtStep} (${result.reason})`} ` +
390
- `— ${result.steps.filter((s) => s.ok).length}/${result.steps.length} steps in ${result.durationSec}s` +
523
+ `— ${result.steps.filter((s) => s.ok).length}/${result.steps.length} steps in ${Math.round(result.durationMs / 100) / 10}s` +
391
524
  (result.appHealth ? `\nhealth: native=${result.nativeHealth} app=${result.appHealth}` : '');
392
- const stepLines = result.steps.map((s) => `${s.ok ? '✓' : '✗'} ${s.index}. ${s.summary}${s.detail ? ` — ${s.detail}` : ''}${s.screenshotUri ? `\n evidence: ${s.screenshotUri}` : ''}`).join('\n');
525
+ const stepLines = result.steps
526
+ .map((s) => `${s.ok ? '✓' : '✗'} ${s.index}. ${s.summary}${s.detail ? ` — ${s.detail}` : ''}${s.screenshotUri ? `\n evidence: ${s.screenshotUri}` : ''}`)
527
+ .join('\n');
393
528
  // A failed flow is a structured result, not a protocol error — the agent should record it.
394
- return qaOk({ ...result }, `${head}\n${stepLines}`);
529
+ return qaAnnotate(qaOk({ ...result }, `${head}\n${stepLines}`), notes);
395
530
  });
396
531
  }
397
532
  //# sourceMappingURL=flow.js.map