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,15 +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, validateAppMap, appMapResourceUri, appMapPath, projectId } from '../appMap/store.js';
12
+ import { loadAppMap, loadCodeIndex, saveAppMap, saveIndexes, appMapResourceUri, appMapPath, projectId } from '../appMap/store.js';
13
+ import { addProvenance, makeProvenance, recomputeConfidence } from '../appMap/provenance.js';
12
14
  import { rememberProject, lookupRoot } from '../appMap/projectRegistry.js';
15
+ import { resolveFeatureContext } from './featureTesting.js';
13
16
  import { detectFramework } from '../context/detect.js';
14
17
  // projectId(root) is a one-way hash, so resource reads need a reverse lookup. We remember every root
15
18
  // touched this session (in-memory) AND in a DURABLE registry (~/.swipium/projects.json, Fix 8) so a
@@ -52,7 +55,14 @@ async function rootFor(server, sessions, args) {
52
55
  }
53
56
  function fallbackProject(root) {
54
57
  const fw = detectFramework(root);
55
- 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
+ };
56
66
  }
57
67
  /** Read an existing map WITHOUT rescanning. Returns null when no map file exists yet. */
58
68
  function readExistingMap(root) {
@@ -80,15 +90,26 @@ export function registerAppMap(server, sessions) {
80
90
  inputSchema: {
81
91
  projectRoot: z.string().optional().describe('Absolute project root. Omit to use a session root or the MCP workspace root.'),
82
92
  sessionId: z.string().optional().describe('Reuse a session (its root + latest exploration graph).'),
83
- 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.'),
84
97
  includeCodeIndex: z.boolean().optional().describe('Persist a code symbol index for queries (default true).'),
85
98
  forceRescan: z.boolean().optional().describe('Re-run the static scan even if an up-to-date map exists (default false).'),
86
99
  },
87
- 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.
88
103
  }, async ({ projectRoot, sessionId, mode, includeCodeIndex, forceRescan }) => {
89
104
  const { root, session, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
90
105
  if (!root)
91
- 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
+ });
92
113
  remember(root);
93
114
  const m = (mode ?? 'full');
94
115
  const exploreGraph = m === 'static_only' ? null : latestExploreGraph(sessions, session);
@@ -98,7 +119,9 @@ export function registerAppMap(server, sessions) {
98
119
  const merge = res.mergeResult;
99
120
  const text = `🗺️ app map built (${m}) for ${root}\n` +
100
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` +
101
- (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
+ : '') +
102
125
  `confidence=${res.map.confidence.overall} · features=${res.map.features.length}\n` +
103
126
  (Array.isArray(summary.topGaps) && summary.topGaps.length ? `gaps: ${summary.topGaps.join('; ')}\n` : '') +
104
127
  `appMapUri: ${res.save?.resourceUri}`;
@@ -115,7 +138,12 @@ export function registerAppMap(server, sessions) {
115
138
  }, text);
116
139
  }
117
140
  catch (e) {
118
- 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
+ });
119
147
  }
120
148
  });
121
149
  // ------------------------------------------------------------------- read
@@ -129,29 +157,56 @@ export function registerAppMap(server, sessions) {
129
157
  featureId: z.string().optional(),
130
158
  screenId: z.string().optional(),
131
159
  },
132
- outputSchema: { ok: z.boolean(), appMapUri: z.string().optional(), section: z.string().optional() },
133
160
  }, async ({ projectRoot, sessionId, section, featureId, screenId }) => {
134
161
  const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
135
162
  if (!root)
136
- 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
+ });
137
170
  remember(root);
138
171
  const map = readExistingMap(root);
139
172
  if (!map)
140
- 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
+ });
141
180
  const uri = appMapResourceUri(root);
142
181
  const sec = section ?? 'summary';
143
182
  if (featureId) {
144
183
  const f = map.features.find((x) => x.id === featureId);
145
184
  if (!f)
146
- 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
+ });
147
191
  return qaOk({ appMapUri: uri, section: 'feature', feature: f, featureResourceUri: `${uri}/feature/${featureId}` }, `feature ${f.title} — ${f.testCoverage} coverage, ${f.status}, confidence ${f.confidence}`);
148
192
  }
149
193
  if (screenId) {
150
194
  const s = map.staticTopology.screens.find((x) => x.id === screenId);
151
195
  const r = map.runtimeTopology.screens.find((x) => x.id === screenId);
152
196
  if (!s && !r)
153
- return qaError({ what: `Unknown screenId ${screenId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_app_map_read { section:"screens" } to list ids.'] });
154
- 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}`);
155
210
  }
156
211
  switch (sec) {
157
212
  case 'summary':
@@ -160,8 +215,21 @@ export function registerAppMap(server, sessions) {
160
215
  return qaOk({
161
216
  appMapUri: uri,
162
217
  section: sec,
163
- staticScreens: map.staticTopology.screens.map((s) => ({ id: s.id, name: s.name, route: s.route, kind: s.kind, confidence: s.confidence })),
164
- 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
+ })),
165
233
  unvisitedStaticScreens: map.runtimeTopology.unvisitedStaticScreens,
166
234
  }, `${map.staticTopology.screens.length} static / ${map.runtimeTopology.screens.length} runtime screens; ${map.runtimeTopology.unvisitedStaticScreens.length} unvisited`);
167
235
  case 'features':
@@ -175,7 +243,12 @@ export function registerAppMap(server, sessions) {
175
243
  case 'full':
176
244
  default:
177
245
  // Protect context: point at the resource instead of inlining the whole map.
178
- 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}`);
179
252
  }
180
253
  });
181
254
  // ------------------------------------------------------------------ query
@@ -189,57 +262,154 @@ export function registerAppMap(server, sessions) {
189
262
  intent: z.enum(['feature', 'screen', 'code', 'test', 'freeform']).optional(),
190
263
  limit: z.number().optional(),
191
264
  },
192
- outputSchema: { ok: z.boolean(), total: z.number().optional() },
193
265
  }, async ({ query, projectRoot, sessionId, intent, limit }) => {
194
266
  const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
195
267
  if (!root)
196
- 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
+ });
197
275
  remember(root);
198
276
  const map = readExistingMap(root);
199
277
  if (!map)
200
- 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
+ });
201
285
  const codeIndex = loadCodeIndex(root);
202
286
  const out = queryAppMap(map, codeIndex, { query, intent, limit });
203
- 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');
204
291
  return qaOk({ ...out, appMapUri: appMapResourceUri(root) }, `🔎 "${query}" — ${out.total} result(s)\n${top || ' (no matches)'}`);
205
292
  });
206
293
  // ----------------------------------------------------------- feature_scope
207
294
  server.registerTool('qa_app_map_feature_scope', {
208
295
  title: 'Scope testing to a feature',
209
- 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.',
210
297
  inputSchema: {
211
- projectRoot: z.string().optional(),
212
- sessionId: z.string().optional(),
213
- featureId: z.string().optional(),
214
- 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).'),
215
305
  },
216
- outputSchema: { ok: z.boolean(), featureId: z.string().optional() },
217
- }, 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).
218
381
  const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
219
382
  if (!root)
220
- 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
+ });
221
390
  remember(root);
222
391
  const map = readExistingMap(root);
223
392
  if (!map)
224
- return qaError({ what: 'No app map yet', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first.'], failureCode: 'NO_APP_MAP' });
225
- let features = map.features;
226
- if (featureId) {
227
- features = map.features.filter((f) => f.id === featureId);
228
- if (!features.length)
229
- return qaError({ what: `Unknown featureId ${featureId}`, changedState: false, retrySafe: true, nextSteps: ['List ids with qa_app_map_read { section:"features" }.'] });
230
- }
231
- else if (query) {
232
- const out = queryAppMap(map, loadCodeIndex(root), { query, intent: 'feature', limit: 5 });
233
- const ids = new Set(out.results.filter((r) => r.type === 'feature').map((r) => r.id));
234
- features = map.features.filter((f) => ids.has(f.id));
235
- if (!features.length)
236
- 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.'] });
237
- }
238
- else {
239
- return qaError({ what: 'Provide one of: featureId or query', changedState: false, retrySafe: true, nextSteps: ['e.g. qa_app_map_feature_scope { query:"login" }'] });
240
- }
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
+ });
241
408
  const scope = features.map((f) => {
242
- 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 }));
243
413
  const needsCreds = f.id === 'feature:auth' || f.blockers.some((b) => /credential/i.test(b));
244
414
  const recommendedGoal = f.id === 'feature:auth' ? 'test_login' : 'reproduce_bug';
245
415
  return {
@@ -256,37 +426,135 @@ export function registerAppMap(server, sessions) {
256
426
  blockers: f.blockers,
257
427
  recommendedPlan: {
258
428
  tool: 'qa_test_this',
259
- args: { mode: 'execute', goal: recommendedGoal, goalText: f.title, explore: true, ...(needsCreds ? { stopOnNeedsInput: true } : {}) },
260
- 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`,
261
439
  },
262
440
  };
263
441
  });
264
442
  const text = `scope: ${scope.length} feature(s)\n` +
265
- 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');
266
446
  return qaOk({ appMapUri: appMapResourceUri(root), featureId: scope[0]?.featureId, scope }, text);
267
447
  });
268
- // --------------------------------------------------------------- validate
269
- server.registerTool('qa_app_map_validate', {
270
- title: 'Validate the app knowledge map',
271
- 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.',
448
+ // ----------------------------------------------------------------- update
449
+ server.registerTool('qa_app_map_update', {
450
+ title: 'Update the app knowledge map',
451
+ description: "Apply targeted, provenance-tracked updates to the app map without a full rebuild: attach a user note, add test cases, link an automation suite to feature/screen ids, set the app environment, or override a feature's coverage. Recomputes confidence + coverage and persists.",
272
452
  inputSchema: {
273
453
  projectRoot: z.string().optional(),
274
454
  sessionId: z.string().optional(),
275
- checkFingerprint: z.boolean().optional().describe('Also re-hash fingerprinted source files to detect a stale map (default false).'),
455
+ note: z.string().optional().describe('A free-text user note added with user_note provenance.'),
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(),
476
+ environment: z.string().optional().describe("App environment, e.g. 'test' | 'staging'."),
477
+ featureCoverage: z.object({ featureId: z.string(), coverage: z.enum(['none', 'partial', 'covered']) }).optional(),
276
478
  },
277
- outputSchema: { ok: z.boolean(), valid: z.boolean().optional(), errors: z.number().optional(), warnings: z.number().optional() },
278
- }, async ({ projectRoot, sessionId, checkFingerprint }) => {
479
+ }, async ({ projectRoot, sessionId, note, testCases, automationSuite, environment, featureCoverage }) => {
279
480
  const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
280
481
  if (!root)
281
- 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
+ });
282
489
  remember(root);
283
490
  const map = readExistingMap(root);
284
491
  if (!map)
285
- return qaError({ what: 'No app map yet', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first.'], failureCode: 'NO_APP_MAP' });
286
- const v = validateAppMap(map, { checkFingerprint, root });
287
- const text = `${v.ok ? '✅' : '❌'} app map ${v.ok ? 'valid' : 'INVALID'} — ${v.errors} error(s), ${v.warnings} warning(s)\n` +
288
- v.issues.slice(0, 12).map((i) => ` ${i.severity === 'error' ? '✗' : '⚠'} ${i.code}: ${i.detail}`).join('\n');
289
- return qaOk({ valid: v.ok, errors: v.errors, warnings: v.warnings, issues: v.issues, appMapUri: appMapResourceUri(root) }, text);
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
+ });
499
+ const at = nowIso();
500
+ const applied = [];
501
+ if (note) {
502
+ addProvenance(map, makeProvenance('user_note', at, note, { targetType: 'map' }));
503
+ applied.push('note');
504
+ }
505
+ if (testCases?.length) {
506
+ for (const c of testCases) {
507
+ const existing = map.testSuite.cases.find((x) => x.id === c.id);
508
+ if (existing)
509
+ Object.assign(existing, c);
510
+ else
511
+ map.testSuite.cases.push(c);
512
+ }
513
+ addProvenance(map, makeProvenance('test_case', at, `${testCases.length} test case(s) registered`, { targetType: 'test' }));
514
+ applied.push(`testCases(${testCases.length})`);
515
+ }
516
+ if (automationSuite) {
517
+ const existing = map.automation.suites.find((s) => s.path === automationSuite.path);
518
+ if (existing)
519
+ Object.assign(existing, automationSuite);
520
+ else
521
+ map.automation.suites.push(automationSuite);
522
+ addProvenance(map, makeProvenance('test_case', at, `Automation suite ${automationSuite.name} linked`, {
523
+ targetType: 'test',
524
+ refs: [automationSuite.path],
525
+ }));
526
+ applied.push('automationSuite');
527
+ }
528
+ if (environment) {
529
+ map.appIdentity.environment = environment;
530
+ applied.push('environment');
531
+ }
532
+ if (featureCoverage) {
533
+ const f = map.features.find((x) => x.id === featureCoverage.featureId);
534
+ if (!f)
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
+ });
541
+ f.testCoverage = featureCoverage.coverage;
542
+ addProvenance(map, makeProvenance('user_note', at, `coverage(${f.id})=${featureCoverage.coverage}`, { targetType: 'feature', targetId: f.id }));
543
+ applied.push('featureCoverage');
544
+ }
545
+ if (!applied.length)
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
+ });
552
+ map.updatedAt = at;
553
+ map.coverage.staleTests = map.testSuite.cases.filter((c) => c.stale).length;
554
+ recomputeConfidence(map);
555
+ const save = saveAppMap(root, map);
556
+ saveIndexes(root, loadCodeIndex(root), map.features);
557
+ return qaOk({ appMapUri: save.resourceUri, applied }, `app map updated: ${applied.join(', ')}\nappMapUri: ${save.resourceUri}`);
290
558
  });
291
559
  }
292
560
  /** Resolve a feature/screen/test-suite/full app-map resource read for the MCP resource handler.