swipium 1.4.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 +88 -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 +114 -28
  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 +279 -133
  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 +43 -73
  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 +20 -7
  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 +14 -50
  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 +33 -102
  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,18 +1,18 @@
1
- // App Knowledge Map MCP tools. Public v1 reads, builds, queries, scopes, and validates the
1
+ // App Knowledge Map MCP tools. Public v1 reads, builds, queries, and scopes the
2
2
  // durable `.swipium/app-map.json`. Large map data is returned by RESOURCE
3
3
  // URI (swipium://project/<id>/app-map…) rather than flooded into the text channel; compact,
4
4
  // structured data is returned inline. All heavy logic lives in src/appMap/*; these are thin.
5
5
  import { existsSync, readFileSync } from 'node:fs';
6
6
  import { z } from 'zod';
7
7
  import { qaOk, qaError } from '../lib/result.js';
8
+ import { qaNeedsInput } from '../lib/needsInput.js';
8
9
  import { resolveProjectRoot } from '../context/projectRoot.js';
9
10
  import { buildAppMap, summarizeMap } from '../appMap/build.js';
10
11
  import { queryAppMap } from '../appMap/query.js';
11
- import { loadAppMap, loadCodeIndex, saveAppMap, saveIndexes, validateAppMap, appMapResourceUri, appMapPath, projectId } from '../appMap/store.js';
12
+ import { loadAppMap, loadCodeIndex, saveAppMap, saveIndexes, appMapResourceUri, appMapPath, projectId } from '../appMap/store.js';
12
13
  import { addProvenance, makeProvenance, recomputeConfidence } from '../appMap/provenance.js';
13
- import { diffAppMaps } from '../appMap/diff.js';
14
- import { migrateAppMap } from '../appMap/migrations.js';
15
14
  import { rememberProject, lookupRoot } from '../appMap/projectRegistry.js';
15
+ import { resolveFeatureContext } from './featureTesting.js';
16
16
  import { detectFramework } from '../context/detect.js';
17
17
  // projectId(root) is a one-way hash, so resource reads need a reverse lookup. We remember every root
18
18
  // touched this session (in-memory) AND in a DURABLE registry (~/.swipium/projects.json, Fix 8) so a
@@ -55,7 +55,14 @@ async function rootFor(server, sessions, args) {
55
55
  }
56
56
  function fallbackProject(root) {
57
57
  const fw = detectFramework(root);
58
- return { root, gitRemote: null, packageName: null, workspaceTarget: null, framework: fw, platforms: fw === 'native-android' ? ['android'] : fw === 'native-ios' ? ['ios'] : ['android', 'ios'] };
58
+ return {
59
+ root,
60
+ gitRemote: null,
61
+ packageName: null,
62
+ workspaceTarget: null,
63
+ framework: fw,
64
+ platforms: fw === 'native-android' ? ['android'] : fw === 'native-ios' ? ['ios'] : ['android', 'ios'],
65
+ };
59
66
  }
60
67
  /** Read an existing map WITHOUT rescanning. Returns null when no map file exists yet. */
61
68
  function readExistingMap(root) {
@@ -83,15 +90,26 @@ export function registerAppMap(server, sessions) {
83
90
  inputSchema: {
84
91
  projectRoot: z.string().optional().describe('Absolute project root. Omit to use a session root or the MCP workspace root.'),
85
92
  sessionId: z.string().optional().describe('Reuse a session (its root + latest exploration graph).'),
86
- mode: z.enum(['static_only', 'runtime_merge', 'full']).optional().describe('static_only: code scan only; runtime_merge: merge the session exploration graph only; full (default): both.'),
93
+ mode: z
94
+ .enum(['static_only', 'runtime_merge', 'full'])
95
+ .optional()
96
+ .describe('static_only: code scan only; runtime_merge: merge the session exploration graph only; full (default): both.'),
87
97
  includeCodeIndex: z.boolean().optional().describe('Persist a code symbol index for queries (default true).'),
88
98
  forceRescan: z.boolean().optional().describe('Re-run the static scan even if an up-to-date map exists (default false).'),
89
99
  },
90
- outputSchema: { ok: z.boolean(), appMapUri: z.string().optional(), staticScreens: z.number().optional(), runtimeScreens: z.number().optional() },
100
+ // NOTE: no outputSchema — a declared (closed) output schema makes strict MCP clients
101
+ // reject BOTH the rich qaOk payload and the qaError envelope as "additional properties"
102
+ // (caught by test/errorContract.test.ts). structuredContent stays self-describing.
91
103
  }, async ({ projectRoot, sessionId, mode, includeCodeIndex, forceRescan }) => {
92
104
  const { root, session, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
93
105
  if (!root)
94
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot="/abs/path" or a valid sessionId.'], clientHint: hint });
106
+ return qaError({
107
+ what: 'Could not resolve a project root',
108
+ changedState: false,
109
+ retrySafe: true,
110
+ nextSteps: ['Pass projectRoot="/abs/path" or a valid sessionId.'],
111
+ clientHint: hint,
112
+ });
95
113
  remember(root);
96
114
  const m = (mode ?? 'full');
97
115
  const exploreGraph = m === 'static_only' ? null : latestExploreGraph(sessions, session);
@@ -101,7 +119,9 @@ export function registerAppMap(server, sessions) {
101
119
  const merge = res.mergeResult;
102
120
  const text = `🗺️ app map built (${m}) for ${root}\n` +
103
121
  `framework=${res.map.project.framework} router=${res.map.staticTopology.router ?? 'none'} · static screens=${res.map.staticTopology.screens.length} · runtime screens=${res.map.runtimeTopology.screens.length}\n` +
104
- (merge ? `merge: +${merge.newRuntimeScreens} new, ~${merge.updatedRuntimeScreens} updated, ${merge.linkedScreens} linked, ${merge.unmappedRuntimeScreens} unmapped\n` : '') +
122
+ (merge
123
+ ? `merge: +${merge.newRuntimeScreens} new, ~${merge.updatedRuntimeScreens} updated, ${merge.linkedScreens} linked, ${merge.unmappedRuntimeScreens} unmapped\n`
124
+ : '') +
105
125
  `confidence=${res.map.confidence.overall} · features=${res.map.features.length}\n` +
106
126
  (Array.isArray(summary.topGaps) && summary.topGaps.length ? `gaps: ${summary.topGaps.join('; ')}\n` : '') +
107
127
  `appMapUri: ${res.save?.resourceUri}`;
@@ -118,7 +138,12 @@ export function registerAppMap(server, sessions) {
118
138
  }, text);
119
139
  }
120
140
  catch (e) {
121
- return qaError({ what: `App map build failed: ${String(e)}`, changedState: false, retrySafe: true, nextSteps: ['Check the project root is a supported mobile project.'] });
141
+ return qaError({
142
+ what: `App map build failed: ${String(e)}`,
143
+ changedState: false,
144
+ retrySafe: true,
145
+ nextSteps: ['Check the project root is a supported mobile project.'],
146
+ });
122
147
  }
123
148
  });
124
149
  // ------------------------------------------------------------------- read
@@ -132,29 +157,56 @@ export function registerAppMap(server, sessions) {
132
157
  featureId: z.string().optional(),
133
158
  screenId: z.string().optional(),
134
159
  },
135
- outputSchema: { ok: z.boolean(), appMapUri: z.string().optional(), section: z.string().optional() },
136
160
  }, async ({ projectRoot, sessionId, section, featureId, screenId }) => {
137
161
  const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
138
162
  if (!root)
139
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
163
+ return qaError({
164
+ what: 'Could not resolve a project root',
165
+ changedState: false,
166
+ retrySafe: true,
167
+ nextSteps: ['Pass projectRoot or sessionId.'],
168
+ clientHint: hint,
169
+ });
140
170
  remember(root);
141
171
  const map = readExistingMap(root);
142
172
  if (!map)
143
- return qaError({ what: 'No app map yet', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first.'], failureCode: 'NO_APP_MAP' });
173
+ return qaError({
174
+ what: 'No app map yet',
175
+ changedState: false,
176
+ retrySafe: true,
177
+ nextSteps: ['Run qa_app_map_build first.'],
178
+ failureCode: 'NO_APP_MAP',
179
+ });
144
180
  const uri = appMapResourceUri(root);
145
181
  const sec = section ?? 'summary';
146
182
  if (featureId) {
147
183
  const f = map.features.find((x) => x.id === featureId);
148
184
  if (!f)
149
- return qaError({ what: `Unknown featureId ${featureId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_app_map_read { section:"features" } to list ids.'] });
185
+ return qaError({
186
+ what: `Unknown featureId ${featureId}`,
187
+ changedState: false,
188
+ retrySafe: true,
189
+ nextSteps: ['Call qa_app_map_read { section:"features" } to list ids.'],
190
+ });
150
191
  return qaOk({ appMapUri: uri, section: 'feature', feature: f, featureResourceUri: `${uri}/feature/${featureId}` }, `feature ${f.title} — ${f.testCoverage} coverage, ${f.status}, confidence ${f.confidence}`);
151
192
  }
152
193
  if (screenId) {
153
194
  const s = map.staticTopology.screens.find((x) => x.id === screenId);
154
195
  const r = map.runtimeTopology.screens.find((x) => x.id === screenId);
155
196
  if (!s && !r)
156
- return qaError({ what: `Unknown screenId ${screenId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_app_map_read { section:"screens" } to list ids.'] });
157
- return qaOk({ appMapUri: uri, section: 'screen', staticScreen: s ?? null, runtimeScreen: r ?? null, screenResourceUri: `${uri}/screen/${screenId}` }, `screen ${screenId}`);
197
+ return qaError({
198
+ what: `Unknown screenId ${screenId}`,
199
+ changedState: false,
200
+ retrySafe: true,
201
+ nextSteps: ['Call qa_app_map_read { section:"screens" } to list ids.'],
202
+ });
203
+ return qaOk({
204
+ appMapUri: uri,
205
+ section: 'screen',
206
+ staticScreen: s ?? null,
207
+ runtimeScreen: r ?? null,
208
+ screenResourceUri: `${uri}/screen/${screenId}`,
209
+ }, `screen ${screenId}`);
158
210
  }
159
211
  switch (sec) {
160
212
  case 'summary':
@@ -163,8 +215,21 @@ export function registerAppMap(server, sessions) {
163
215
  return qaOk({
164
216
  appMapUri: uri,
165
217
  section: sec,
166
- staticScreens: map.staticTopology.screens.map((s) => ({ id: s.id, name: s.name, route: s.route, kind: s.kind, confidence: s.confidence })),
167
- runtimeScreens: map.runtimeTopology.screens.map((r) => ({ id: r.id, title: r.title, visits: r.visits, linkedStaticScreenId: r.linkedStaticScreenId, unmapped: r.unmapped, locatorReadiness: r.locatorReadiness })),
218
+ staticScreens: map.staticTopology.screens.map((s) => ({
219
+ id: s.id,
220
+ name: s.name,
221
+ route: s.route,
222
+ kind: s.kind,
223
+ confidence: s.confidence,
224
+ })),
225
+ runtimeScreens: map.runtimeTopology.screens.map((r) => ({
226
+ id: r.id,
227
+ title: r.title,
228
+ visits: r.visits,
229
+ linkedStaticScreenId: r.linkedStaticScreenId,
230
+ unmapped: r.unmapped,
231
+ locatorReadiness: r.locatorReadiness,
232
+ })),
168
233
  unvisitedStaticScreens: map.runtimeTopology.unvisitedStaticScreens,
169
234
  }, `${map.staticTopology.screens.length} static / ${map.runtimeTopology.screens.length} runtime screens; ${map.runtimeTopology.unvisitedStaticScreens.length} unvisited`);
170
235
  case 'features':
@@ -178,7 +243,12 @@ export function registerAppMap(server, sessions) {
178
243
  case 'full':
179
244
  default:
180
245
  // Protect context: point at the resource instead of inlining the whole map.
181
- return qaOk({ appMapUri: uri, section: 'full', summary: summarizeMap(map), note: 'Full map omitted from text to protect context — read the appMapUri resource for everything.' }, `Full map at resource: ${uri}`);
246
+ return qaOk({
247
+ appMapUri: uri,
248
+ section: 'full',
249
+ summary: summarizeMap(map),
250
+ note: 'Full map omitted from text to protect context — read the appMapUri resource for everything.',
251
+ }, `Full map at resource: ${uri}`);
182
252
  }
183
253
  });
184
254
  // ------------------------------------------------------------------ query
@@ -192,57 +262,154 @@ export function registerAppMap(server, sessions) {
192
262
  intent: z.enum(['feature', 'screen', 'code', 'test', 'freeform']).optional(),
193
263
  limit: z.number().optional(),
194
264
  },
195
- outputSchema: { ok: z.boolean(), total: z.number().optional() },
196
265
  }, async ({ query, projectRoot, sessionId, intent, limit }) => {
197
266
  const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
198
267
  if (!root)
199
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
268
+ return qaError({
269
+ what: 'Could not resolve a project root',
270
+ changedState: false,
271
+ retrySafe: true,
272
+ nextSteps: ['Pass projectRoot or sessionId.'],
273
+ clientHint: hint,
274
+ });
200
275
  remember(root);
201
276
  const map = readExistingMap(root);
202
277
  if (!map)
203
- return qaError({ what: 'No app map yet', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first.'], failureCode: 'NO_APP_MAP' });
278
+ return qaError({
279
+ what: 'No app map yet',
280
+ changedState: false,
281
+ retrySafe: true,
282
+ nextSteps: ['Run qa_app_map_build first.'],
283
+ failureCode: 'NO_APP_MAP',
284
+ });
204
285
  const codeIndex = loadCodeIndex(root);
205
286
  const out = queryAppMap(map, codeIndex, { query, intent, limit });
206
- const top = out.results.slice(0, 5).map((r, i) => ` ${i + 1}. [${r.type}] ${r.title} (score ${r.score}${r.confidence !== undefined ? `, conf ${r.confidence}` : ''}) → ${r.recommendedNextTool.tool}`).join('\n');
287
+ const top = out.results
288
+ .slice(0, 5)
289
+ .map((r, i) => ` ${i + 1}. [${r.type}] ${r.title} (score ${r.score}${r.confidence !== undefined ? `, conf ${r.confidence}` : ''}) → ${r.recommendedNextTool.tool}`)
290
+ .join('\n');
207
291
  return qaOk({ ...out, appMapUri: appMapResourceUri(root) }, `🔎 "${query}" — ${out.total} result(s)\n${top || ' (no matches)'}`);
208
292
  });
209
293
  // ----------------------------------------------------------- feature_scope
210
294
  server.registerTool('qa_app_map_feature_scope', {
211
295
  title: 'Scope testing to a feature',
212
- description: 'Resolve a feature (by featureId or free-text query) to a focused test scope: its source files, static + runtime screens, current coverage, blockers, and a recommended Swipium plan.',
296
+ description: 'Resolve a feature (by featureId or free-text query) to a focused test scope. READ-ONLY — no device, no mutation. With query (e.g. "weather analysis"), it scopes app-map-FIRST and falls back to a fresh code-index scan + the latest exploration screen graph when no map exists yet — returning ranked code symbols, static + runtime screens, existing tests, an inferred objective model, coverage gaps, a recommended test strategy, and ALL plausible candidates with confidence (asking ONE disambiguation question only when genuinely-different features tie). With featureId, it returns that map feature\'s source files, screens, coverage, blockers, and a recommended plan (requires an existing map). Pass sessionId (preferred — adds runtime evidence) or projectRoot.',
213
297
  inputSchema: {
214
- projectRoot: z.string().optional(),
215
- sessionId: z.string().optional(),
216
- featureId: z.string().optional(),
217
- query: z.string().optional().describe('Free-text feature description, e.g. "checkout flow".'),
298
+ projectRoot: z.string().optional().describe('Project root when no session exists.'),
299
+ sessionId: z.string().optional().describe('Session to scope against (adds runtime screen-graph evidence).'),
300
+ featureId: z.string().optional().describe('Exact app-map feature id (requires an existing map). Provide this OR query.'),
301
+ query: z.string().optional().describe('Free-text feature description, e.g. "checkout flow" or "weather analysis".'),
302
+ platform: z.enum(['android', 'ios']).optional(),
303
+ includeCode: z.boolean().optional().describe('Query mode: scan source for code-aware matches (default true).'),
304
+ limit: z.number().optional().describe('Query mode: max items per list in the scope (default 8).'),
218
305
  },
219
- outputSchema: { ok: z.boolean(), featureId: z.string().optional() },
220
- }, async ({ projectRoot, sessionId, featureId, query }) => {
306
+ }, async ({ projectRoot, sessionId, featureId, query, platform, includeCode, limit }) => {
307
+ if (!featureId && !query) {
308
+ return qaError({
309
+ what: 'Provide one of: featureId or query',
310
+ changedState: false,
311
+ retrySafe: true,
312
+ nextSteps: ['e.g. qa_app_map_feature_scope { query:"login" }'],
313
+ });
314
+ }
315
+ // Free-text QUERY path: full feature scoping (app-map-first, code-index + runtime-graph
316
+ // fallback, objective model, candidate disambiguation). Works without an existing map.
317
+ if (!featureId) {
318
+ const r = await resolveFeatureContext(server, sessions, { sessionId, projectRoot, feature: query, platform, includeCode, limit });
319
+ if (!r.ok)
320
+ return r.result;
321
+ remember(r.ctx.root);
322
+ const { scopeResult, objective, index } = r.ctx;
323
+ const scope = scopeResult.primary;
324
+ if (!scopeResult.found) {
325
+ return qaOk({
326
+ sessionId: sessionId ?? null,
327
+ query,
328
+ found: false,
329
+ scope,
330
+ searched: scopeResult.searched,
331
+ nextRecommendedAction: {
332
+ tool: 'qa_test_this',
333
+ args: { ...(sessionId ? { sessionId } : { projectRoot: r.ctx.root }), goal: 'explore' },
334
+ why: 'Grow the map with an initial run, then re-scope the feature',
335
+ },
336
+ }, `🔎 No feature matched "${query}". Searched ${scopeResult.searched.symbols} symbols, ${scopeResult.searched.routes} routes, ${scopeResult.searched.files} files, ${scopeResult.searched.runtimeScreens} runtime screens with terms: ${scopeResult.searched.terms.slice(0, 12).join(', ')}. Run qa_test_this/qa_explore to grow coverage, or refine the feature name.`);
337
+ }
338
+ if (scopeResult.needsInput) {
339
+ return qaNeedsInput({
340
+ needsInput: true,
341
+ kind: 'monorepo_target',
342
+ question: scopeResult.needsInput.question,
343
+ fields: [{ name: 'query', description: 'The exact feature to test', example: scopeResult.candidates[0]?.title }],
344
+ fallbackOptions: scopeResult.needsInput.options,
345
+ resume: { tool: 'qa_app_map_feature_scope', args: {} },
346
+ attempted: [`scoped "${query}" — matched ${scopeResult.candidates.length} distinct candidates that tie`],
347
+ ifDeclined: 'Swipium scopes the highest-confidence candidate and records the others as alternatives.',
348
+ }, { sessionId: sessionId ?? undefined, candidates: scopeResult.candidates });
349
+ }
350
+ const nextRecommendedAction = scope.recommendedStrategy === 'manual_blocked'
351
+ ? {
352
+ tool: 'qa_test_feature',
353
+ args: { ...(sessionId ? { sessionId } : { projectRoot: r.ctx.root }), feature: query, mode: 'plan' },
354
+ why: 'Review the plan + setup needed before any automated execution',
355
+ }
356
+ : {
357
+ tool: 'qa_test_feature',
358
+ args: { sessionId: sessionId ?? '${sessionId}', feature: query, mode: 'execute' },
359
+ why: 'Run a focused test of this feature',
360
+ };
361
+ return qaOk({
362
+ sessionId: sessionId ?? null,
363
+ query,
364
+ found: true,
365
+ featureId: scope.featureId,
366
+ scope,
367
+ objective,
368
+ appMapUri: r.ctx.appMapUri,
369
+ mapFeatureId: scopeResult.mapFeatureId ?? null,
370
+ ticketRefs: scopeResult.ticketRefs,
371
+ runtimeSource: scopeResult.runtimeSource,
372
+ candidates: scopeResult.candidates,
373
+ searched: scopeResult.searched,
374
+ codeIndex: { scannedFiles: index.scannedFiles, truncated: index.truncated },
375
+ nextRecommendedAction,
376
+ }, `🔎 ${scope.title} (confidence ${Math.round(scope.confidence * 100)}%, strategy ${scope.recommendedStrategy}) — ` +
377
+ `${scope.staticScreens.length} static screen(s), ${scope.runtimeScreens.length} runtime screen(s), ${scope.functions.length} symbol(s), ${scope.existingTests.length} existing test(s).` +
378
+ (scopeResult.candidates.length > 1 ? ` ${scopeResult.candidates.length} candidate(s).` : ''));
379
+ }
380
+ // FEATURE-ID path: exact map lookup (requires an existing map).
221
381
  const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
222
382
  if (!root)
223
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
383
+ return qaError({
384
+ what: 'Could not resolve a project root',
385
+ changedState: false,
386
+ retrySafe: true,
387
+ nextSteps: ['Pass projectRoot or sessionId.'],
388
+ clientHint: hint,
389
+ });
224
390
  remember(root);
225
391
  const map = readExistingMap(root);
226
392
  if (!map)
227
- return qaError({ what: 'No app map yet', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first.'], failureCode: 'NO_APP_MAP' });
228
- let features = map.features;
229
- if (featureId) {
230
- features = map.features.filter((f) => f.id === featureId);
231
- if (!features.length)
232
- return qaError({ what: `Unknown featureId ${featureId}`, changedState: false, retrySafe: true, nextSteps: ['List ids with qa_app_map_read { section:"features" }.'] });
233
- }
234
- else if (query) {
235
- const out = queryAppMap(map, loadCodeIndex(root), { query, intent: 'feature', limit: 5 });
236
- const ids = new Set(out.results.filter((r) => r.type === 'feature').map((r) => r.id));
237
- features = map.features.filter((f) => ids.has(f.id));
238
- if (!features.length)
239
- return qaError({ what: `No feature matched "${query}"`, changedState: false, retrySafe: true, nextSteps: ['Try qa_app_map_query for broader matches, or qa_app_map_build to refresh.'] });
240
- }
241
- else {
242
- return qaError({ what: 'Provide one of: featureId or query', changedState: false, retrySafe: true, nextSteps: ['e.g. qa_app_map_feature_scope { query:"login" }'] });
243
- }
393
+ return qaError({
394
+ what: 'No app map yet',
395
+ changedState: false,
396
+ retrySafe: true,
397
+ nextSteps: ['Run qa_app_map_build first, or pass query= for map-free scoping.'],
398
+ failureCode: 'NO_APP_MAP',
399
+ });
400
+ const features = map.features.filter((f) => f.id === featureId);
401
+ if (!features.length)
402
+ return qaError({
403
+ what: `Unknown featureId ${featureId}`,
404
+ changedState: false,
405
+ retrySafe: true,
406
+ nextSteps: ['List ids with qa_app_map_read { section:"features" }, or pass query= for free-text scoping.'],
407
+ });
244
408
  const scope = features.map((f) => {
245
- const staticScreens = f.staticScreens.map((id) => map.staticTopology.screens.find((s) => s.id === id)).filter(Boolean).map((s) => ({ id: s.id, name: s.name, route: s.route, sourceFiles: s.sourceFiles }));
409
+ const staticScreens = f.staticScreens
410
+ .map((id) => map.staticTopology.screens.find((s) => s.id === id))
411
+ .filter(Boolean)
412
+ .map((s) => ({ id: s.id, name: s.name, route: s.route, sourceFiles: s.sourceFiles }));
246
413
  const needsCreds = f.id === 'feature:auth' || f.blockers.some((b) => /credential/i.test(b));
247
414
  const recommendedGoal = f.id === 'feature:auth' ? 'test_login' : 'reproduce_bug';
248
415
  return {
@@ -259,38 +426,25 @@ export function registerAppMap(server, sessions) {
259
426
  blockers: f.blockers,
260
427
  recommendedPlan: {
261
428
  tool: 'qa_test_this',
262
- args: { mode: 'execute', goal: recommendedGoal, goalText: f.title, explore: true, ...(needsCreds ? { stopOnNeedsInput: true } : {}) },
263
- why: needsCreds ? `Drive "${f.title}"; will stop for test credentials (fixture) — ${f.testCoverage} coverage today` : `Drive "${f.title}" with focused exploration — ${f.testCoverage} coverage today`,
429
+ args: {
430
+ mode: 'execute',
431
+ goal: recommendedGoal,
432
+ goalText: f.title,
433
+ explore: true,
434
+ ...(needsCreds ? { stopOnNeedsInput: true } : {}),
435
+ },
436
+ why: needsCreds
437
+ ? `Drive "${f.title}"; will stop for test credentials (fixture) — ${f.testCoverage} coverage today`
438
+ : `Drive "${f.title}" with focused exploration — ${f.testCoverage} coverage today`,
264
439
  },
265
440
  };
266
441
  });
267
442
  const text = `scope: ${scope.length} feature(s)\n` +
268
- scope.map((s) => ` • ${s.title} [${s.testCoverage}] — ${s.staticScreens.length} screen(s), ${s.sourceFiles.length} file(s)${s.blockers.length ? ` · blockers: ${s.blockers.join(', ')}` : ''}\n → ${s.recommendedPlan.tool} ${JSON.stringify(s.recommendedPlan.args)}`).join('\n');
443
+ scope
444
+ .map((s) => ` • ${s.title} [${s.testCoverage}] — ${s.staticScreens.length} screen(s), ${s.sourceFiles.length} file(s)${s.blockers.length ? ` · blockers: ${s.blockers.join(', ')}` : ''}\n → ${s.recommendedPlan.tool} ${JSON.stringify(s.recommendedPlan.args)}`)
445
+ .join('\n');
269
446
  return qaOk({ appMapUri: appMapResourceUri(root), featureId: scope[0]?.featureId, scope }, text);
270
447
  });
271
- // --------------------------------------------------------------- validate
272
- server.registerTool('qa_app_map_validate', {
273
- title: 'Validate the app knowledge map',
274
- description: 'Validate the app map: schema version, provenance completeness, missing/dangling links, duplicate ids, impossible states (e.g. covered-without-runtime), and (optionally) stale source fingerprints. Returns errors + warnings with codes. Use before trusting an old map for feature-focused testing.',
275
- inputSchema: {
276
- projectRoot: z.string().optional(),
277
- sessionId: z.string().optional(),
278
- checkFingerprint: z.boolean().optional().describe('Also re-hash fingerprinted source files to detect a stale map (default false).'),
279
- },
280
- outputSchema: { ok: z.boolean(), valid: z.boolean().optional(), errors: z.number().optional(), warnings: z.number().optional() },
281
- }, async ({ projectRoot, sessionId, checkFingerprint }) => {
282
- const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
283
- if (!root)
284
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
285
- remember(root);
286
- const map = readExistingMap(root);
287
- if (!map)
288
- return qaError({ what: 'No app map yet', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first.'], failureCode: 'NO_APP_MAP' });
289
- const v = validateAppMap(map, { checkFingerprint, root });
290
- const text = `${v.ok ? '✅' : '❌'} app map ${v.ok ? 'valid' : 'INVALID'} — ${v.errors} error(s), ${v.warnings} warning(s)\n` +
291
- v.issues.slice(0, 12).map((i) => ` ${i.severity === 'error' ? '✗' : '⚠'} ${i.code}: ${i.detail}`).join('\n');
292
- return qaOk({ valid: v.ok, errors: v.errors, warnings: v.warnings, issues: v.issues, appMapUri: appMapResourceUri(root) }, text);
293
- });
294
448
  // ----------------------------------------------------------------- update
295
449
  server.registerTool('qa_app_map_update', {
296
450
  title: 'Update the app knowledge map',
@@ -299,20 +453,49 @@ export function registerAppMap(server, sessions) {
299
453
  projectRoot: z.string().optional(),
300
454
  sessionId: z.string().optional(),
301
455
  note: z.string().optional().describe('A free-text user note added with user_note provenance.'),
302
- testCases: z.array(z.object({ id: z.string(), title: z.string(), featureId: z.string().optional(), screenId: z.string().optional(), status: z.string().optional(), source: z.string().optional(), stale: z.boolean().optional() })).optional(),
303
- automationSuite: z.object({ name: z.string(), path: z.string(), framework: z.string().optional(), linkedFeatureIds: z.array(z.string()).optional(), linkedScreenIds: z.array(z.string()).optional() }).optional(),
456
+ testCases: z
457
+ .array(z.object({
458
+ id: z.string(),
459
+ title: z.string(),
460
+ featureId: z.string().optional(),
461
+ screenId: z.string().optional(),
462
+ status: z.string().optional(),
463
+ source: z.string().optional(),
464
+ stale: z.boolean().optional(),
465
+ }))
466
+ .optional(),
467
+ automationSuite: z
468
+ .object({
469
+ name: z.string(),
470
+ path: z.string(),
471
+ framework: z.string().optional(),
472
+ linkedFeatureIds: z.array(z.string()).optional(),
473
+ linkedScreenIds: z.array(z.string()).optional(),
474
+ })
475
+ .optional(),
304
476
  environment: z.string().optional().describe("App environment, e.g. 'test' | 'staging'."),
305
477
  featureCoverage: z.object({ featureId: z.string(), coverage: z.enum(['none', 'partial', 'covered']) }).optional(),
306
478
  },
307
- outputSchema: { ok: z.boolean(), appMapUri: z.string().optional(), applied: z.array(z.string()).optional() },
308
479
  }, async ({ projectRoot, sessionId, note, testCases, automationSuite, environment, featureCoverage }) => {
309
480
  const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
310
481
  if (!root)
311
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
482
+ return qaError({
483
+ what: 'Could not resolve a project root',
484
+ changedState: false,
485
+ retrySafe: true,
486
+ nextSteps: ['Pass projectRoot or sessionId.'],
487
+ clientHint: hint,
488
+ });
312
489
  remember(root);
313
490
  const map = readExistingMap(root);
314
491
  if (!map)
315
- return qaError({ what: 'No app map yet', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first.'], failureCode: 'NO_APP_MAP' });
492
+ return qaError({
493
+ what: 'No app map yet',
494
+ changedState: false,
495
+ retrySafe: true,
496
+ nextSteps: ['Run qa_app_map_build first.'],
497
+ failureCode: 'NO_APP_MAP',
498
+ });
316
499
  const at = nowIso();
317
500
  const applied = [];
318
501
  if (note) {
@@ -336,7 +519,10 @@ export function registerAppMap(server, sessions) {
336
519
  Object.assign(existing, automationSuite);
337
520
  else
338
521
  map.automation.suites.push(automationSuite);
339
- addProvenance(map, makeProvenance('test_case', at, `Automation suite ${automationSuite.name} linked`, { targetType: 'test', refs: [automationSuite.path] }));
522
+ addProvenance(map, makeProvenance('test_case', at, `Automation suite ${automationSuite.name} linked`, {
523
+ targetType: 'test',
524
+ refs: [automationSuite.path],
525
+ }));
340
526
  applied.push('automationSuite');
341
527
  }
342
528
  if (environment) {
@@ -346,13 +532,23 @@ export function registerAppMap(server, sessions) {
346
532
  if (featureCoverage) {
347
533
  const f = map.features.find((x) => x.id === featureCoverage.featureId);
348
534
  if (!f)
349
- return qaError({ what: `Unknown featureId ${featureCoverage.featureId}`, changedState: false, retrySafe: true, nextSteps: ['List ids with qa_app_map_read { section:"features" }.'] });
535
+ return qaError({
536
+ what: `Unknown featureId ${featureCoverage.featureId}`,
537
+ changedState: false,
538
+ retrySafe: true,
539
+ nextSteps: ['List ids with qa_app_map_read { section:"features" }.'],
540
+ });
350
541
  f.testCoverage = featureCoverage.coverage;
351
542
  addProvenance(map, makeProvenance('user_note', at, `coverage(${f.id})=${featureCoverage.coverage}`, { targetType: 'feature', targetId: f.id }));
352
543
  applied.push('featureCoverage');
353
544
  }
354
545
  if (!applied.length)
355
- return qaError({ what: 'No update fields provided', changedState: false, retrySafe: true, nextSteps: ['Pass at least one of: note, testCases, automationSuite, environment, featureCoverage.'] });
546
+ return qaError({
547
+ what: 'No update fields provided',
548
+ changedState: false,
549
+ retrySafe: true,
550
+ nextSteps: ['Pass at least one of: note, testCases, automationSuite, environment, featureCoverage.'],
551
+ });
356
552
  map.updatedAt = at;
357
553
  map.coverage.staleTests = map.testSuite.cases.filter((c) => c.stale).length;
358
554
  recomputeConfidence(map);
@@ -360,56 +556,6 @@ export function registerAppMap(server, sessions) {
360
556
  saveIndexes(root, loadCodeIndex(root), map.features);
361
557
  return qaOk({ appMapUri: save.resourceUri, applied }, `app map updated: ${applied.join(', ')}\nappMapUri: ${save.resourceUri}`);
362
558
  });
363
- // ------------------------------------------------------------------- diff
364
- server.registerTool('qa_app_map_diff', {
365
- title: 'Diff two app map snapshots',
366
- description: 'Compare two App Knowledge Map snapshots and report added/removed/changed screens, feature coverage changes, locator-readiness changes, stale test cases, and new untested code areas. With no paths, compares the most recent history snapshot (baseline) against the current map.',
367
- inputSchema: {
368
- projectRoot: z.string().optional(),
369
- sessionId: z.string().optional(),
370
- baseline: z.string().optional().describe('Path to a baseline app-map.json (or history snapshot). Defaults to the latest history snapshot.'),
371
- current: z.string().optional().describe('Path to the current app-map.json. Defaults to .swipium/app-map.json.'),
372
- },
373
- outputSchema: { ok: z.boolean(), summary: z.string().optional() },
374
- }, async ({ projectRoot, sessionId, baseline, current }) => {
375
- const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
376
- if (!root)
377
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
378
- remember(root);
379
- const readMap = (p) => {
380
- if (!existsSync(p))
381
- return null;
382
- try {
383
- const raw = JSON.parse(readFileSync(p, 'utf8'));
384
- return migrateAppMap(raw, fallbackProject(root), nowIso()).map;
385
- }
386
- catch {
387
- return null;
388
- }
389
- };
390
- const currentMap = current ? readMap(current) : readExistingMap(root);
391
- if (!currentMap)
392
- return qaError({ what: 'No current app map to diff', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first, or pass current=.'], failureCode: 'NO_APP_MAP' });
393
- let baselineMap = baseline ? readMap(baseline) : null;
394
- if (!baselineMap && !baseline) {
395
- const { appMapHistoryDir } = await import('../appMap/store.js');
396
- const dir = appMapHistoryDir(root);
397
- try {
398
- const { readdirSync } = await import('node:fs');
399
- const snaps = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.json')).sort() : [];
400
- const prev = snaps.length >= 2 ? snaps[snaps.length - 2] : snaps[0];
401
- if (prev)
402
- baselineMap = readMap(`${dir}/${prev}`);
403
- }
404
- catch {
405
- /* none */
406
- }
407
- }
408
- if (!baselineMap)
409
- return qaError({ what: 'No baseline snapshot available to diff against', changedState: false, retrySafe: true, nextSteps: ['Build the map at least twice, or pass baseline=path-to-app-map.json.'] });
410
- const diff = diffAppMaps(baselineMap, currentMap);
411
- return qaOk({ ...diff, appMapUri: appMapResourceUri(root) }, `app map diff: ${diff.summary}`);
412
- });
413
559
  }
414
560
  /** Resolve a feature/screen/test-suite/full app-map resource read for the MCP resource handler.
415
561
  * Routes through loadAppMap() so an older on-disk shape is MIGRATED before it is served (Fix 8). */