swipium 1.4.0 → 2.0.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 (541) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +175 -219
  3. package/THREAT_MODEL.md +223 -47
  4. package/dist/appMap/automationLink.js +39 -26
  5. package/dist/appMap/automationLink.js.map +1 -1
  6. package/dist/appMap/build.js +68 -20
  7. package/dist/appMap/build.js.map +1 -1
  8. package/dist/appMap/codeIndex.js +31 -5
  9. package/dist/appMap/codeIndex.js.map +1 -1
  10. package/dist/appMap/featureIndex.js +23 -5
  11. package/dist/appMap/featureIndex.js.map +1 -1
  12. package/dist/appMap/featureModel.js +44 -8
  13. package/dist/appMap/featureModel.js.map +1 -1
  14. package/dist/appMap/firstRunApply.js +24 -6
  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 +10 -30
  19. package/dist/appMap/issues.js.map +1 -1
  20. package/dist/appMap/migrations.js +6 -6
  21. package/dist/appMap/migrations.js.map +1 -1
  22. package/dist/appMap/prelaunch.js +2 -2
  23. package/dist/appMap/prelaunch.js.map +1 -1
  24. package/dist/appMap/projectRegistry.js +83 -11
  25. package/dist/appMap/projectRegistry.js.map +1 -1
  26. package/dist/appMap/provenance.js +2 -2
  27. package/dist/appMap/provenance.js.map +1 -1
  28. package/dist/appMap/query.js +21 -5
  29. package/dist/appMap/query.js.map +1 -1
  30. package/dist/appMap/runtimeMerge.js +12 -5
  31. package/dist/appMap/runtimeMerge.js.map +1 -1
  32. package/dist/appMap/schema.js +1 -1
  33. package/dist/appMap/schema.js.map +1 -1
  34. package/dist/appMap/screenMatch.js +6 -2
  35. package/dist/appMap/screenMatch.js.map +1 -1
  36. package/dist/appMap/staticScan.js +151 -39
  37. package/dist/appMap/staticScan.js.map +1 -1
  38. package/dist/appMap/store.js +58 -85
  39. package/dist/appMap/store.js.map +1 -1
  40. package/dist/appMap/tsAstScan.js +34 -12
  41. package/dist/appMap/tsAstScan.js.map +1 -1
  42. package/dist/artifacts/bundletool.js +49 -12
  43. package/dist/artifacts/bundletool.js.map +1 -1
  44. package/dist/artifacts/resolve.js +41 -14
  45. package/dist/artifacts/resolve.js.map +1 -1
  46. package/dist/automation/capabilities.js +112 -30
  47. package/dist/automation/capabilities.js.map +1 -1
  48. package/dist/automation/gestures.js +6 -2
  49. package/dist/automation/gestures.js.map +1 -1
  50. package/dist/automation/plan.js +71 -23
  51. package/dist/automation/plan.js.map +1 -1
  52. package/dist/automation/report.js +15 -5
  53. package/dist/automation/report.js.map +1 -1
  54. package/dist/automation/selectors.js +22 -28
  55. package/dist/automation/selectors.js.map +1 -1
  56. package/dist/automation/types.js +1 -1
  57. package/dist/automation/types.js.map +1 -1
  58. package/dist/automation/waits.js +14 -8
  59. package/dist/automation/waits.js.map +1 -1
  60. package/dist/automation/webview.js +9 -3
  61. package/dist/automation/webview.js.map +1 -1
  62. package/dist/automationGen/appiumModel.js +16 -8
  63. package/dist/automationGen/appiumModel.js.map +1 -1
  64. package/dist/automationGen/ciEmitter.js +6 -6
  65. package/dist/automationGen/ciEmitter.js.map +1 -1
  66. package/dist/automationGen/identifiers.js +224 -0
  67. package/dist/automationGen/identifiers.js.map +1 -0
  68. package/dist/automationGen/jsEmitter.js +288 -121
  69. package/dist/automationGen/jsEmitter.js.map +1 -1
  70. package/dist/automationGen/packagePatch.js +2 -2
  71. package/dist/automationGen/packagePatch.js.map +1 -1
  72. package/dist/automationGen/platformResolve.js +36 -0
  73. package/dist/automationGen/platformResolve.js.map +1 -0
  74. package/dist/automationGen/projectProfile.js +76 -27
  75. package/dist/automationGen/projectProfile.js.map +1 -1
  76. package/dist/automationGen/pythonEmitter.js +278 -107
  77. package/dist/automationGen/pythonEmitter.js.map +1 -1
  78. package/dist/automationGen/readmeEmitter.js +7 -6
  79. package/dist/automationGen/readmeEmitter.js.map +1 -1
  80. package/dist/automationGen/run.js +388 -0
  81. package/dist/automationGen/run.js.map +1 -0
  82. package/dist/automationGen/suitePlan.js +17 -10
  83. package/dist/automationGen/suitePlan.js.map +1 -1
  84. package/dist/automationGen/validation.js +52 -12
  85. package/dist/automationGen/validation.js.map +1 -1
  86. package/dist/build/parseBuildLog.js +235 -31
  87. package/dist/build/parseBuildLog.js.map +1 -1
  88. package/dist/build/plan.js +39 -18
  89. package/dist/build/plan.js.map +1 -1
  90. package/dist/ci/preflight.js +13 -4
  91. package/dist/ci/preflight.js.map +1 -1
  92. package/dist/cli/gc.js +74 -0
  93. package/dist/cli/gc.js.map +1 -0
  94. package/dist/cli/init.js +217 -26
  95. package/dist/cli/init.js.map +1 -1
  96. package/dist/cli/main.js +50 -0
  97. package/dist/cli/main.js.map +1 -0
  98. package/dist/cli/report.js +218 -0
  99. package/dist/cli/report.js.map +1 -0
  100. package/dist/cli/scan.js +36 -15
  101. package/dist/cli/scan.js.map +1 -1
  102. package/dist/cli/suite.js +7 -7
  103. package/dist/cli/suite.js.map +1 -1
  104. package/dist/cli/verify.js +7 -7
  105. package/dist/cli/verify.js.map +1 -1
  106. package/dist/consent/consent.js +162 -5
  107. package/dist/consent/consent.js.map +1 -1
  108. package/dist/context/detect.js +55 -18
  109. package/dist/context/detect.js.map +1 -1
  110. package/dist/context/findApps.js +22 -8
  111. package/dist/context/findApps.js.map +1 -1
  112. package/dist/context/projectRoot.js +174 -19
  113. package/dist/context/projectRoot.js.map +1 -1
  114. package/dist/context/scan.js +1 -1
  115. package/dist/context/scan.js.map +1 -1
  116. package/dist/core/capabilityGroups.js +90 -0
  117. package/dist/core/capabilityGroups.js.map +1 -0
  118. package/dist/core/target.js +10 -8
  119. package/dist/core/target.js.map +1 -1
  120. package/dist/core/targetPlan.js +160 -39
  121. package/dist/core/targetPlan.js.map +1 -1
  122. package/dist/drivers/DirectDriver.js +317 -43
  123. package/dist/drivers/DirectDriver.js.map +1 -1
  124. package/dist/drivers/SimctlDriver.js +124 -8
  125. package/dist/drivers/SimctlDriver.js.map +1 -1
  126. package/dist/drivers/WdaDriver.js +365 -55
  127. package/dist/drivers/WdaDriver.js.map +1 -1
  128. package/dist/explore/candidates.js +7 -3
  129. package/dist/explore/candidates.js.map +1 -1
  130. package/dist/explore/graph.js +15 -6
  131. package/dist/explore/graph.js.map +1 -1
  132. package/dist/explore/memory.js +4 -1
  133. package/dist/explore/memory.js.map +1 -1
  134. package/dist/explore/planner.js +54 -9
  135. package/dist/explore/planner.js.map +1 -1
  136. package/dist/explore/policy.js +11 -6
  137. package/dist/explore/policy.js.map +1 -1
  138. package/dist/explore/runner.js +363 -190
  139. package/dist/explore/runner.js.map +1 -1
  140. package/dist/explore/signatures.js +4 -2
  141. package/dist/explore/signatures.js.map +1 -1
  142. package/dist/explore/suite.js +3 -1
  143. package/dist/explore/suite.js.map +1 -1
  144. package/dist/featureTesting/executionBootstrap.js +137 -29
  145. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  146. package/dist/featureTesting/featureMap.js +6 -3
  147. package/dist/featureTesting/featureMap.js.map +1 -1
  148. package/dist/featureTesting/featureScope.js +101 -20
  149. package/dist/featureTesting/featureScope.js.map +1 -1
  150. package/dist/featureTesting/mapFeatureScope.js +44 -15
  151. package/dist/featureTesting/mapFeatureScope.js.map +1 -1
  152. package/dist/featureTesting/objectiveModel.js +15 -11
  153. package/dist/featureTesting/objectiveModel.js.map +1 -1
  154. package/dist/featureTesting/resultMerge.js +16 -8
  155. package/dist/featureTesting/resultMerge.js.map +1 -1
  156. package/dist/featureTesting/sources.js +8 -5
  157. package/dist/featureTesting/sources.js.map +1 -1
  158. package/dist/featureTesting/suiteBridge.js +8 -2
  159. package/dist/featureTesting/suiteBridge.js.map +1 -1
  160. package/dist/featureTesting/synonyms.js +32 -4
  161. package/dist/featureTesting/synonyms.js.map +1 -1
  162. package/dist/featureTesting/testCaseFactory.js +27 -15
  163. package/dist/featureTesting/testCaseFactory.js.map +1 -1
  164. package/dist/featureTesting/testPlan.js +34 -12
  165. package/dist/featureTesting/testPlan.js.map +1 -1
  166. package/dist/firstRun/authStateMachine.js +1 -1
  167. package/dist/firstRun/authStateMachine.js.map +1 -1
  168. package/dist/firstRun/classifyScreen.js +9 -6
  169. package/dist/firstRun/classifyScreen.js.map +1 -1
  170. package/dist/firstRun/firstRunPlanner.js +16 -13
  171. package/dist/firstRun/firstRunPlanner.js.map +1 -1
  172. package/dist/firstRun/firstRunRunner.js +92 -31
  173. package/dist/firstRun/firstRunRunner.js.map +1 -1
  174. package/dist/firstRun/generatedDataPolicy.js +25 -15
  175. package/dist/firstRun/generatedDataPolicy.js.map +1 -1
  176. package/dist/firstRun/inputPlanner.js +45 -11
  177. package/dist/firstRun/inputPlanner.js.map +1 -1
  178. package/dist/firstRun/onboardingStateMachine.js +1 -1
  179. package/dist/firstRun/onboardingStateMachine.js.map +1 -1
  180. package/dist/firstRun/paywallPolicy.js +3 -3
  181. package/dist/firstRun/paywallPolicy.js.map +1 -1
  182. package/dist/firstRun/types.js +1 -1
  183. package/dist/firstRun/types.js.map +1 -1
  184. package/dist/fixtures/catalog.js +72 -9
  185. package/dist/fixtures/catalog.js.map +1 -1
  186. package/dist/fixtures/load.js +50 -0
  187. package/dist/fixtures/load.js.map +1 -0
  188. package/dist/flows/discover.js +2 -2
  189. package/dist/flows/discover.js.map +1 -1
  190. package/dist/flows/generate.js +68 -17
  191. package/dist/flows/generate.js.map +1 -1
  192. package/dist/flows/lint.js +88 -14
  193. package/dist/flows/lint.js.map +1 -1
  194. package/dist/flows/pack.js +21 -5
  195. package/dist/flows/pack.js.map +1 -1
  196. package/dist/flows/paths.js +57 -0
  197. package/dist/flows/paths.js.map +1 -0
  198. package/dist/flows/repair.js +176 -37
  199. package/dist/flows/repair.js.map +1 -1
  200. package/dist/flows/run.js +308 -99
  201. package/dist/flows/run.js.map +1 -1
  202. package/dist/flows/schema.js +107 -25
  203. package/dist/flows/schema.js.map +1 -1
  204. package/dist/flows/seedExec.js +8 -4
  205. package/dist/flows/seedExec.js.map +1 -1
  206. package/dist/flows/templates.js +5 -5
  207. package/dist/index.js +50 -8
  208. package/dist/index.js.map +1 -1
  209. package/dist/ios/signing.js +32 -3
  210. package/dist/ios/signing.js.map +1 -1
  211. package/dist/issues/classify.js +28 -20
  212. package/dist/issues/classify.js.map +1 -1
  213. package/dist/issues/fingerprint.js +35 -9
  214. package/dist/issues/fingerprint.js.map +1 -1
  215. package/dist/issues/index.js +93 -46
  216. package/dist/issues/index.js.map +1 -1
  217. package/dist/issues/metrics.js +6 -5
  218. package/dist/issues/metrics.js.map +1 -1
  219. package/dist/issues/recurrence.js +35 -8
  220. package/dist/issues/recurrence.js.map +1 -1
  221. package/dist/issues/report.js +12 -8
  222. package/dist/issues/report.js.map +1 -1
  223. package/dist/issues/reportBridge.js +40 -9
  224. package/dist/issues/reportBridge.js.map +1 -1
  225. package/dist/issues/schema.js +2 -2
  226. package/dist/issues/schema.js.map +1 -1
  227. package/dist/issues/sourceRevision.js +2 -2
  228. package/dist/issues/sourceRevision.js.map +1 -1
  229. package/dist/issues/store.js +110 -32
  230. package/dist/issues/store.js.map +1 -1
  231. package/dist/lib/abortScope.js +48 -0
  232. package/dist/lib/abortScope.js.map +1 -0
  233. package/dist/lib/android.js +84 -8
  234. package/dist/lib/android.js.map +1 -1
  235. package/dist/lib/coordSpace.js +1 -1
  236. package/dist/lib/coordSpace.js.map +1 -1
  237. package/dist/lib/device.js +53 -12
  238. package/dist/lib/device.js.map +1 -1
  239. package/dist/lib/gestures.js +141 -0
  240. package/dist/lib/gestures.js.map +1 -0
  241. package/dist/lib/gitignore.js +4 -3
  242. package/dist/lib/gitignore.js.map +1 -1
  243. package/dist/lib/image.js +12 -3
  244. package/dist/lib/image.js.map +1 -1
  245. package/dist/lib/lockfile.js +399 -0
  246. package/dist/lib/lockfile.js.map +1 -0
  247. package/dist/lib/needsInput.js +9 -6
  248. package/dist/lib/needsInput.js.map +1 -1
  249. package/dist/lib/png.js +4 -3
  250. package/dist/lib/png.js.map +1 -1
  251. package/dist/lib/redact.js +170 -9
  252. package/dist/lib/redact.js.map +1 -1
  253. package/dist/lib/result.js +94 -10
  254. package/dist/lib/result.js.map +1 -1
  255. package/dist/lib/schemaHash.js +7 -5
  256. package/dist/lib/schemaHash.js.map +1 -1
  257. package/dist/lib/sensitive.js +6 -3
  258. package/dist/lib/sensitive.js.map +1 -1
  259. package/dist/lib/simctl.js +3 -3
  260. package/dist/lib/simctl.js.map +1 -1
  261. package/dist/lib/spawn.js +78 -17
  262. package/dist/lib/spawn.js.map +1 -1
  263. package/dist/lib/toolAnnotations.js +114 -0
  264. package/dist/lib/toolAnnotations.js.map +1 -0
  265. package/dist/lib/wda.js +321 -25
  266. package/dist/lib/wda.js.map +1 -1
  267. package/dist/lib/wdaConfig.js +12 -5
  268. package/dist/lib/wdaConfig.js.map +1 -1
  269. package/dist/lib/wdaTune.js +52 -23
  270. package/dist/lib/wdaTune.js.map +1 -1
  271. package/dist/mobileAudit/checks.js +150 -28
  272. package/dist/mobileAudit/checks.js.map +1 -1
  273. package/dist/mobileAudit/evidence.js +4 -4
  274. package/dist/mobileAudit/evidence.js.map +1 -1
  275. package/dist/mobileAudit/profiles.js +121 -20
  276. package/dist/mobileAudit/profiles.js.map +1 -1
  277. package/dist/mobileAudit/results.js +9 -4
  278. package/dist/mobileAudit/results.js.map +1 -1
  279. package/dist/mobileAudit/runner.js +214 -35
  280. package/dist/mobileAudit/runner.js.map +1 -1
  281. package/dist/oracle/auth.js +1 -1
  282. package/dist/oracle/auth.js.map +1 -1
  283. package/dist/oracle/failures.js +1085 -145
  284. package/dist/oracle/failures.js.map +1 -1
  285. package/dist/oracle/health.js +148 -26
  286. package/dist/oracle/health.js.map +1 -1
  287. package/dist/oracle/locator.js +148 -19
  288. package/dist/oracle/locator.js.map +1 -1
  289. package/dist/oracle/record.js +3 -3
  290. package/dist/oracle/record.js.map +1 -1
  291. package/dist/orchestration/envelope.js +2 -2
  292. package/dist/orchestration/envelope.js.map +1 -1
  293. package/dist/orchestration/goal.js +7 -13
  294. package/dist/orchestration/goal.js.map +1 -1
  295. package/dist/orchestration/testThis/execute.js +167 -0
  296. package/dist/orchestration/testThis/execute.js.map +1 -0
  297. package/dist/orchestration/testThis/pipeline.js +413 -0
  298. package/dist/orchestration/testThis/pipeline.js.map +1 -0
  299. package/dist/orchestration/testThis/plan.js +519 -0
  300. package/dist/orchestration/testThis/plan.js.map +1 -0
  301. package/dist/orchestration/testThis/sessionIntent.js +72 -0
  302. package/dist/orchestration/testThis/sessionIntent.js.map +1 -0
  303. package/dist/orchestration/testThis/terminal.js +136 -0
  304. package/dist/orchestration/testThis/terminal.js.map +1 -0
  305. package/dist/orchestration/testThis/types.js +12 -0
  306. package/dist/orchestration/testThis/types.js.map +1 -0
  307. package/dist/plan/plan.js +9 -9
  308. package/dist/plan/plan.js.map +1 -1
  309. package/dist/prompts/index.js +24 -24
  310. package/dist/prompts/index.js.map +1 -1
  311. package/dist/report/coverage.js +0 -1
  312. package/dist/report/coverage.js.map +1 -1
  313. package/dist/report/evidence.js.map +1 -1
  314. package/dist/report/export.js +449 -72
  315. package/dist/report/export.js.map +1 -1
  316. package/dist/report/findingsDedupe.js +41 -0
  317. package/dist/report/findingsDedupe.js.map +1 -0
  318. package/dist/report/flake.js +13 -14
  319. package/dist/report/flake.js.map +1 -1
  320. package/dist/report/history.js +72 -38
  321. package/dist/report/history.js.map +1 -1
  322. package/dist/report/policy.js +18 -7
  323. package/dist/report/policy.js.map +1 -1
  324. package/dist/report/qaLevel.js +10 -16
  325. package/dist/report/qaLevel.js.map +1 -1
  326. package/dist/report/readiness.js +42 -12
  327. package/dist/report/readiness.js.map +1 -1
  328. package/dist/report/sarifSources.js +160 -0
  329. package/dist/report/sarifSources.js.map +1 -0
  330. package/dist/report/summary.js +2 -2
  331. package/dist/report/summary.js.map +1 -1
  332. package/dist/report/testCatalog.js +4 -4
  333. package/dist/report/testCatalog.js.map +1 -1
  334. package/dist/report/timing.js +23 -23
  335. package/dist/report/timing.js.map +1 -1
  336. package/dist/report/toolHealth.js +124 -0
  337. package/dist/report/toolHealth.js.map +1 -0
  338. package/dist/server.js +501 -69
  339. package/dist/server.js.map +1 -1
  340. package/dist/services/automationGenerate.js +8 -2
  341. package/dist/services/automationGenerate.js.map +1 -1
  342. package/dist/services/build.js +10 -4
  343. package/dist/services/build.js.map +1 -1
  344. package/dist/services/flowGenerate.js +75 -0
  345. package/dist/services/flowGenerate.js.map +1 -0
  346. package/dist/services/preflight.js +40 -8
  347. package/dist/services/preflight.js.map +1 -1
  348. package/dist/services/prepareAndroid.js +92 -18
  349. package/dist/services/prepareAndroid.js.map +1 -1
  350. package/dist/services/prepareIos.js +94 -17
  351. package/dist/services/prepareIos.js.map +1 -1
  352. package/dist/services/report.js +271 -80
  353. package/dist/services/report.js.map +1 -1
  354. package/dist/services/smoke.js +68 -22
  355. package/dist/services/smoke.js.map +1 -1
  356. package/dist/services/suiteGenerate.js +43 -6
  357. package/dist/services/suiteGenerate.js.map +1 -1
  358. package/dist/services/testSuiteKnowledge.js +78 -33
  359. package/dist/services/testSuiteKnowledge.js.map +1 -1
  360. package/dist/session/attach.js +284 -4
  361. package/dist/session/attach.js.map +1 -1
  362. package/dist/session/processRegistry.js +433 -0
  363. package/dist/session/processRegistry.js.map +1 -0
  364. package/dist/session/progress.js +1 -1
  365. package/dist/session/progress.js.map +1 -1
  366. package/dist/session/retention.js +201 -0
  367. package/dist/session/retention.js.map +1 -0
  368. package/dist/session/store.js +459 -59
  369. package/dist/session/store.js.map +1 -1
  370. package/dist/snapshot/overlays.js +60 -10
  371. package/dist/snapshot/overlays.js.map +1 -1
  372. package/dist/snapshot/parse.js +30 -12
  373. package/dist/snapshot/parse.js.map +1 -1
  374. package/dist/snapshot/present.js +36 -6
  375. package/dist/snapshot/present.js.map +1 -1
  376. package/dist/snapshot/settle.js +39 -9
  377. package/dist/snapshot/settle.js.map +1 -1
  378. package/dist/state/consent.js +99 -0
  379. package/dist/state/consent.js.map +1 -0
  380. package/dist/state/profile.js +82 -11
  381. package/dist/state/profile.js.map +1 -1
  382. package/dist/suite/compile.js +15 -6
  383. package/dist/suite/compile.js.map +1 -1
  384. package/dist/suite/lint.js +22 -4
  385. package/dist/suite/lint.js.map +1 -1
  386. package/dist/suite/pom.js +75 -44
  387. package/dist/suite/pom.js.map +1 -1
  388. package/dist/suite/secretGuard.js +266 -0
  389. package/dist/suite/secretGuard.js.map +1 -0
  390. package/dist/suite/testcase.js +15 -11
  391. package/dist/suite/testcase.js.map +1 -1
  392. package/dist/testSuite/exporter.js +7 -3
  393. package/dist/testSuite/exporter.js.map +1 -1
  394. package/dist/testSuite/generator.js +43 -14
  395. package/dist/testSuite/generator.js.map +1 -1
  396. package/dist/testSuite/history.js +1 -1
  397. package/dist/testSuite/history.js.map +1 -1
  398. package/dist/testSuite/issueLinks.js +3 -3
  399. package/dist/testSuite/issueLinks.js.map +1 -1
  400. package/dist/testSuite/lint.js +4 -4
  401. package/dist/testSuite/lint.js.map +1 -1
  402. package/dist/testSuite/merge.js +13 -7
  403. package/dist/testSuite/merge.js.map +1 -1
  404. package/dist/testSuite/schema.js +21 -16
  405. package/dist/testSuite/schema.js.map +1 -1
  406. package/dist/testSuite/store.js +12 -4
  407. package/dist/testSuite/store.js.map +1 -1
  408. package/dist/testSuite/traceability.js +2 -2
  409. package/dist/testSuite/traceability.js.map +1 -1
  410. package/dist/tools/act.js +868 -174
  411. package/dist/tools/act.js.map +1 -1
  412. package/dist/tools/agent.js +521 -112
  413. package/dist/tools/agent.js.map +1 -1
  414. package/dist/tools/appControl.js +82 -19
  415. package/dist/tools/appControl.js.map +1 -1
  416. package/dist/tools/appMap.js +393 -189
  417. package/dist/tools/appMap.js.map +1 -1
  418. package/dist/tools/build.js +92 -51
  419. package/dist/tools/build.js.map +1 -1
  420. package/dist/tools/bundletool.js +141 -40
  421. package/dist/tools/bundletool.js.map +1 -1
  422. package/dist/tools/clearOverlay.js +98 -18
  423. package/dist/tools/clearOverlay.js.map +1 -1
  424. package/dist/tools/device.js +108 -19
  425. package/dist/tools/device.js.map +1 -1
  426. package/dist/tools/doctor.js +98 -35
  427. package/dist/tools/doctor.js.map +1 -1
  428. package/dist/tools/explore.js +110 -42
  429. package/dist/tools/explore.js.map +1 -1
  430. package/dist/tools/featureTesting.js +152 -130
  431. package/dist/tools/featureTesting.js.map +1 -1
  432. package/dist/tools/firstRun.js +94 -66
  433. package/dist/tools/firstRun.js.map +1 -1
  434. package/dist/tools/flow.js +347 -132
  435. package/dist/tools/flow.js.map +1 -1
  436. package/dist/tools/flowRepair.js +42 -10
  437. package/dist/tools/flowRepair.js.map +1 -1
  438. package/dist/tools/generate.js +199 -0
  439. package/dist/tools/generate.js.map +1 -0
  440. package/dist/tools/getArtifact.js +37 -8
  441. package/dist/tools/getArtifact.js.map +1 -1
  442. package/dist/tools/health.js +17 -12
  443. package/dist/tools/health.js.map +1 -1
  444. package/dist/tools/ios.js +148 -194
  445. package/dist/tools/ios.js.map +1 -1
  446. package/dist/tools/issues.js +256 -171
  447. package/dist/tools/issues.js.map +1 -1
  448. package/dist/tools/jobs.js +55 -11
  449. package/dist/tools/jobs.js.map +1 -1
  450. package/dist/tools/metro.js +84 -38
  451. package/dist/tools/metro.js.map +1 -1
  452. package/dist/tools/mobileAudit.js +83 -26
  453. package/dist/tools/mobileAudit.js.map +1 -1
  454. package/dist/tools/network.js +36 -14
  455. package/dist/tools/network.js.map +1 -1
  456. package/dist/tools/note.js +31 -17
  457. package/dist/tools/note.js.map +1 -1
  458. package/dist/tools/prepareIosTarget.js +31 -14
  459. package/dist/tools/prepareIosTarget.js.map +1 -1
  460. package/dist/tools/prepareTarget.js +193 -47
  461. package/dist/tools/prepareTarget.js.map +1 -1
  462. package/dist/tools/report.js +16 -8
  463. package/dist/tools/report.js.map +1 -1
  464. package/dist/tools/resolveArtifact.js +12 -12
  465. package/dist/tools/resolveArtifact.js.map +1 -1
  466. package/dist/tools/resolveTarget.js +81 -18
  467. package/dist/tools/resolveTarget.js.map +1 -1
  468. package/dist/tools/screenRecord.js +85 -20
  469. package/dist/tools/screenRecord.js.map +1 -1
  470. package/dist/tools/screenshot.js +42 -18
  471. package/dist/tools/screenshot.js.map +1 -1
  472. package/dist/tools/smoke.js +29 -12
  473. package/dist/tools/smoke.js.map +1 -1
  474. package/dist/tools/snapshot.js +157 -107
  475. package/dist/tools/snapshot.js.map +1 -1
  476. package/dist/tools/startSession.js +117 -84
  477. package/dist/tools/startSession.js.map +1 -1
  478. package/dist/tools/suite.js +391 -310
  479. package/dist/tools/suite.js.map +1 -1
  480. package/dist/tools/testSuite.js +112 -36
  481. package/dist/tools/testSuite.js.map +1 -1
  482. package/dist/tools/testThis.js +35 -802
  483. package/dist/tools/testThis.js.map +1 -1
  484. package/dist/tools/visual.js +698 -116
  485. package/dist/tools/visual.js.map +1 -1
  486. package/dist/tools/wait.js +9 -23
  487. package/dist/tools/wait.js.map +1 -1
  488. package/dist/tools/wda.js +312 -54
  489. package/dist/tools/wda.js.map +1 -1
  490. package/dist/version.js +37 -64
  491. package/dist/version.js.map +1 -1
  492. package/dist/visual/ocr.js +65 -8
  493. package/dist/visual/ocr.js.map +1 -1
  494. package/dist/visual/provider.js +87 -15
  495. package/dist/visual/provider.js.map +1 -1
  496. package/docs/README.md +24 -10
  497. package/docs/ci-reports.md +328 -0
  498. package/docs/concepts.md +227 -0
  499. package/docs/flows.md +153 -0
  500. package/docs/mcp-server.md +210 -104
  501. package/docs/physical-devices.md +84 -0
  502. package/docs/tools.md +808 -217
  503. package/package.json +16 -6
  504. package/dist/appMap/diff.js +0 -60
  505. package/dist/appMap/diff.js.map +0 -1
  506. package/dist/automation/maestroIr.js +0 -282
  507. package/dist/automation/maestroIr.js.map +0 -1
  508. package/dist/interop/maestro.js +0 -151
  509. package/dist/interop/maestro.js.map +0 -1
  510. package/dist/tools/assertVisual.js +0 -74
  511. package/dist/tools/assertVisual.js.map +0 -1
  512. package/dist/tools/automationGenerate.js +0 -394
  513. package/dist/tools/automationGenerate.js.map +0 -1
  514. package/dist/tools/capabilities.js +0 -209
  515. package/dist/tools/capabilities.js.map +0 -1
  516. package/dist/tools/detectContext.js +0 -41
  517. package/dist/tools/detectContext.js.map +0 -1
  518. package/dist/tools/flowGenerate.js +0 -55
  519. package/dist/tools/flowGenerate.js.map +0 -1
  520. package/dist/tools/history.js +0 -56
  521. package/dist/tools/history.js.map +0 -1
  522. package/dist/tools/idling.js +0 -104
  523. package/dist/tools/idling.js.map +0 -1
  524. package/dist/tools/inputCapabilities.js +0 -30
  525. package/dist/tools/inputCapabilities.js.map +0 -1
  526. package/dist/tools/locator.js +0 -57
  527. package/dist/tools/locator.js.map +0 -1
  528. package/dist/tools/maestro.js +0 -78
  529. package/dist/tools/maestro.js.map +0 -1
  530. package/dist/tools/permissions.js +0 -149
  531. package/dist/tools/permissions.js.map +0 -1
  532. package/dist/tools/plan.js +0 -49
  533. package/dist/tools/plan.js.map +0 -1
  534. package/dist/tools/screenInfo.js +0 -70
  535. package/dist/tools/screenInfo.js.map +0 -1
  536. package/dist/tools/seed.js +0 -101
  537. package/dist/tools/seed.js.map +0 -1
  538. package/dist/tools/state.js +0 -170
  539. package/dist/tools/state.js.map +0 -1
  540. package/dist/tools/visualText.js +0 -61
  541. package/dist/tools/visualText.js.map +0 -1
@@ -1,19 +1,21 @@
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
+ import { basename } from 'node:path';
6
7
  import { z } from 'zod';
7
- import { qaOk, qaError } from '../lib/result.js';
8
- import { resolveProjectRoot } from '../context/projectRoot.js';
8
+ import { qaOk, qaError, unknownSessionError } from '../lib/result.js';
9
+ import { qaNeedsInput } from '../lib/needsInput.js';
10
+ import { resolveProjectRoot, unresolvedProjectRootError } from '../context/projectRoot.js';
9
11
  import { buildAppMap, summarizeMap } from '../appMap/build.js';
10
12
  import { queryAppMap } from '../appMap/query.js';
11
- import { loadAppMap, loadCodeIndex, saveAppMap, saveIndexes, validateAppMap, appMapResourceUri, appMapPath, projectId } from '../appMap/store.js';
13
+ import { loadAppMap, loadCodeIndex, saveAppMap, saveIndexes, withAppMapLock, appMapResourceUri, appMapPath, projectId, } from '../appMap/store.js';
12
14
  import { addProvenance, makeProvenance, recomputeConfidence } from '../appMap/provenance.js';
13
- import { diffAppMaps } from '../appMap/diff.js';
14
- import { migrateAppMap } from '../appMap/migrations.js';
15
15
  import { rememberProject, lookupRoot } from '../appMap/projectRegistry.js';
16
+ import { resolveFeatureContext } from './featureTesting.js';
16
17
  import { detectFramework } from '../context/detect.js';
18
+ import { encodeUriSegment } from '../session/store.js';
17
19
  // projectId(root) is a one-way hash, so resource reads need a reverse lookup. We remember every root
18
20
  // touched this session (in-memory) AND in a DURABLE registry (~/.swipium/projects.json, Fix 8) so a
19
21
  // resource URI stays resolvable across server restarts. The resource handler also falls back to live
@@ -45,7 +47,7 @@ async function rootFor(server, sessions, args) {
45
47
  if (args.sessionId) {
46
48
  const s = sessions.get(args.sessionId);
47
49
  if (!s)
48
- return { hint: `Unknown sessionId ${args.sessionId}` };
50
+ return { error: unknownSessionError(args.sessionId) };
49
51
  return { root: s.root, session: s };
50
52
  }
51
53
  const resolved = await resolveProjectRoot(server, args.projectRoot);
@@ -55,7 +57,14 @@ async function rootFor(server, sessions, args) {
55
57
  }
56
58
  function fallbackProject(root) {
57
59
  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'] };
60
+ return {
61
+ root,
62
+ gitRemote: null,
63
+ packageName: null,
64
+ workspaceTarget: null,
65
+ framework: fw,
66
+ platforms: fw === 'native-android' ? ['android'] : fw === 'native-ios' ? ['ios'] : ['android', 'ios'],
67
+ };
59
68
  }
60
69
  /** Read an existing map WITHOUT rescanning. Returns null when no map file exists yet. */
61
70
  function readExistingMap(root) {
@@ -79,19 +88,23 @@ export function registerAppMap(server, sessions) {
79
88
  // ------------------------------------------------------------------ build
80
89
  server.registerTool('qa_app_map_build', {
81
90
  title: 'Build / update the app knowledge map',
82
- description: 'Build or incrementally update the durable App Knowledge Map at .swipium/app-map.json. Runs the framework-aware STATIC scan (Expo Router / React Navigation / native Android manifest / SwiftUI+UIKit / Flutter routes), loads + migrates any existing map, and (with a sessionId in runtime_merge/full mode) MERGES the latest exploration screen graph into runtime topology — linking runtime screens to static screens. Returns a compact summary + the map resource URI; the full map is read via qa_app_map_read or the resource. Does NOT commit the file.',
91
+ description: 'Build or update the app knowledge map (.swipium/app-map.json): framework-aware static scan (Expo Router, React ' +
92
+ 'Navigation, Android manifest, SwiftUI/UIKit, Flutter) plus, with sessionId, a merge of the latest exploration screen ' +
93
+ 'graph. Returns a summary + map URI. Does not commit.',
83
94
  inputSchema: {
84
- projectRoot: z.string().optional().describe('Absolute project root. Omit to use a session root or the MCP workspace root.'),
95
+ projectRoot: z.string().optional().describe('Default: the session or resolved project root.'),
85
96
  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.'),
97
+ mode: z.enum(['static_only', 'runtime_merge', 'full']).optional().describe('static_only | runtime_merge | full (default).'),
87
98
  includeCodeIndex: z.boolean().optional().describe('Persist a code symbol index for queries (default true).'),
88
- forceRescan: z.boolean().optional().describe('Re-run the static scan even if an up-to-date map exists (default false).'),
99
+ forceRescan: z.boolean().optional().describe('Re-scan even if the map is current.'),
89
100
  },
90
- outputSchema: { ok: z.boolean(), appMapUri: z.string().optional(), staticScreens: z.number().optional(), runtimeScreens: z.number().optional() },
101
+ // NOTE: no outputSchema. A declared (closed) output schema makes strict MCP clients
102
+ // reject BOTH the rich qaOk payload and the qaError envelope as "additional properties"
103
+ // (caught by test/errorContract.test.ts). structuredContent stays self-describing.
91
104
  }, async ({ projectRoot, sessionId, mode, includeCodeIndex, forceRescan }) => {
92
- const { root, session, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
105
+ const { root, session, hint, error } = await rootFor(server, sessions, { projectRoot, sessionId });
93
106
  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 });
107
+ return error ?? unresolvedProjectRootError({ source: 'none', hint });
95
108
  remember(root);
96
109
  const m = (mode ?? 'full');
97
110
  const exploreGraph = m === 'static_only' ? null : latestExploreGraph(sessions, session);
@@ -101,7 +114,9 @@ export function registerAppMap(server, sessions) {
101
114
  const merge = res.mergeResult;
102
115
  const text = `🗺️ app map built (${m}) for ${root}\n` +
103
116
  `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` : '') +
117
+ (merge
118
+ ? `merge: +${merge.newRuntimeScreens} new, ~${merge.updatedRuntimeScreens} updated, ${merge.linkedScreens} linked, ${merge.unmappedRuntimeScreens} unmapped\n`
119
+ : '') +
105
120
  `confidence=${res.map.confidence.overall} · features=${res.map.features.length}\n` +
106
121
  (Array.isArray(summary.topGaps) && summary.topGaps.length ? `gaps: ${summary.topGaps.join('; ')}\n` : '') +
107
122
  `appMapUri: ${res.save?.resourceUri}`;
@@ -113,18 +128,30 @@ export function registerAppMap(server, sessions) {
113
128
  staticScreens: res.map.staticTopology.screens.length,
114
129
  runtimeScreens: res.map.runtimeTopology.screens.length,
115
130
  mergeResult: merge ?? null,
116
- migration: res.migration ? { migratedFrom: res.migration.migratedFrom, applied: res.migration.applied } : null,
131
+ migration: res.migration
132
+ ? {
133
+ migratedFrom: res.migration.migratedFrom,
134
+ applied: res.migration.applied,
135
+ recoveredFrom: res.migration.recoveredFrom ?? null,
136
+ }
137
+ : null,
117
138
  summary,
118
139
  }, text);
119
140
  }
120
141
  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.'] });
142
+ return qaError({
143
+ what: `App map build failed: ${String(e)}`,
144
+ changedState: false,
145
+ retrySafe: true,
146
+ nextSteps: ['Check the project root is a supported mobile project.'],
147
+ });
122
148
  }
123
149
  });
124
150
  // ------------------------------------------------------------------- read
125
151
  server.registerTool('qa_app_map_read', {
126
152
  title: 'Read the app knowledge map',
127
- description: 'Return a COMPACT section of the app map (default summary). Sections: summary | screens | features | auth | automation | testSuite | full. Pass featureId or screenId to drill into one node. The full map and large sections are returned by resource URI to protect context — fetch it with qa_get_artifact / the resource when you need everything.',
153
+ description: 'Read a compact app-map section: summary (default) | screens | features | auth | automation | testSuite | full; ' +
154
+ 'featureId or screenId drills into one node. Large sections come back as a resource URI.',
128
155
  inputSchema: {
129
156
  projectRoot: z.string().optional(),
130
157
  sessionId: z.string().optional(),
@@ -132,29 +159,50 @@ export function registerAppMap(server, sessions) {
132
159
  featureId: z.string().optional(),
133
160
  screenId: z.string().optional(),
134
161
  },
135
- outputSchema: { ok: z.boolean(), appMapUri: z.string().optional(), section: z.string().optional() },
136
162
  }, async ({ projectRoot, sessionId, section, featureId, screenId }) => {
137
- const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
163
+ const { root, hint, error } = await rootFor(server, sessions, { projectRoot, sessionId });
138
164
  if (!root)
139
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
165
+ return error ?? unresolvedProjectRootError({ source: 'none', hint });
140
166
  remember(root);
141
167
  const map = readExistingMap(root);
142
168
  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' });
169
+ return qaError({
170
+ what: 'No app map yet',
171
+ changedState: false,
172
+ retrySafe: true,
173
+ nextSteps: ['Run qa_app_map_build first.'],
174
+ failureCode: 'NO_APP_MAP',
175
+ });
144
176
  const uri = appMapResourceUri(root);
145
177
  const sec = section ?? 'summary';
146
178
  if (featureId) {
147
179
  const f = map.features.find((x) => x.id === featureId);
148
180
  if (!f)
149
- return qaError({ what: `Unknown featureId ${featureId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_app_map_read { section:"features" } to list ids.'] });
150
- return qaOk({ appMapUri: uri, section: 'feature', feature: f, featureResourceUri: `${uri}/feature/${featureId}` }, `feature ${f.title} — ${f.testCoverage} coverage, ${f.status}, confidence ${f.confidence}`);
181
+ return qaError({
182
+ what: `Unknown featureId ${featureId}`,
183
+ changedState: false,
184
+ retrySafe: true,
185
+ nextSteps: ['Call qa_app_map_read { section:"features" } to list ids.'],
186
+ });
187
+ return qaOk({ appMapUri: uri, section: 'feature', feature: f, featureResourceUri: `${uri}/feature/${featureId}` }, `feature ${f.title}: ${f.testCoverage} coverage, ${f.status}, confidence ${f.confidence}`);
151
188
  }
152
189
  if (screenId) {
153
190
  const s = map.staticTopology.screens.find((x) => x.id === screenId);
154
191
  const r = map.runtimeTopology.screens.find((x) => x.id === screenId);
155
192
  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}`);
193
+ return qaError({
194
+ what: `Unknown screenId ${screenId}`,
195
+ changedState: false,
196
+ retrySafe: true,
197
+ nextSteps: ['Call qa_app_map_read { section:"screens" } to list ids.'],
198
+ });
199
+ return qaOk({
200
+ appMapUri: uri,
201
+ section: 'screen',
202
+ staticScreen: s ?? null,
203
+ runtimeScreen: r ?? null,
204
+ screenResourceUri: `${uri}/screen/${screenId}`,
205
+ }, `screen ${screenId}`);
158
206
  }
159
207
  switch (sec) {
160
208
  case 'summary':
@@ -163,8 +211,21 @@ export function registerAppMap(server, sessions) {
163
211
  return qaOk({
164
212
  appMapUri: uri,
165
213
  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 })),
214
+ staticScreens: map.staticTopology.screens.map((s) => ({
215
+ id: s.id,
216
+ name: s.name,
217
+ route: s.route,
218
+ kind: s.kind,
219
+ confidence: s.confidence,
220
+ })),
221
+ runtimeScreens: map.runtimeTopology.screens.map((r) => ({
222
+ id: r.id,
223
+ title: r.title,
224
+ visits: r.visits,
225
+ linkedStaticScreenId: r.linkedStaticScreenId,
226
+ unmapped: r.unmapped,
227
+ locatorReadiness: r.locatorReadiness,
228
+ })),
168
229
  unvisitedStaticScreens: map.runtimeTopology.unvisitedStaticScreens,
169
230
  }, `${map.staticTopology.screens.length} static / ${map.runtimeTopology.screens.length} runtime screens; ${map.runtimeTopology.unvisitedStaticScreens.length} unvisited`);
170
231
  case 'features':
@@ -178,7 +239,12 @@ export function registerAppMap(server, sessions) {
178
239
  case 'full':
179
240
  default:
180
241
  // 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}`);
242
+ return qaOk({
243
+ appMapUri: uri,
244
+ section: 'full',
245
+ summary: summarizeMap(map),
246
+ note: 'Full map omitted from text to protect context. Read the appMapUri resource for everything.',
247
+ }, `Full map at resource: ${uri}`);
182
248
  }
183
249
  });
184
250
  // ------------------------------------------------------------------ query
@@ -192,57 +258,145 @@ export function registerAppMap(server, sessions) {
192
258
  intent: z.enum(['feature', 'screen', 'code', 'test', 'freeform']).optional(),
193
259
  limit: z.number().optional(),
194
260
  },
195
- outputSchema: { ok: z.boolean(), total: z.number().optional() },
196
261
  }, async ({ query, projectRoot, sessionId, intent, limit }) => {
197
- const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
262
+ const { root, hint, error } = await rootFor(server, sessions, { projectRoot, sessionId });
198
263
  if (!root)
199
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
264
+ return error ?? unresolvedProjectRootError({ source: 'none', hint });
200
265
  remember(root);
201
266
  const map = readExistingMap(root);
202
267
  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' });
268
+ return qaError({
269
+ what: 'No app map yet',
270
+ changedState: false,
271
+ retrySafe: true,
272
+ nextSteps: ['Run qa_app_map_build first.'],
273
+ failureCode: 'NO_APP_MAP',
274
+ });
204
275
  const codeIndex = loadCodeIndex(root);
205
276
  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');
207
- return qaOk({ ...out, appMapUri: appMapResourceUri(root) }, `🔎 "${query}" — ${out.total} result(s)\n${top || ' (no matches)'}`);
277
+ const top = out.results
278
+ .slice(0, 5)
279
+ .map((r, i) => ` ${i + 1}. [${r.type}] ${r.title} (score ${r.score}${r.confidence !== undefined ? `, conf ${r.confidence}` : ''}) > ${r.recommendedNextTool.tool}`)
280
+ .join('\n');
281
+ return qaOk({ ...out, appMapUri: appMapResourceUri(root) }, `🔎 "${query}": ${out.total} result(s)\n${top || ' (no matches)'}`);
208
282
  });
209
283
  // ----------------------------------------------------------- feature_scope
210
284
  server.registerTool('qa_app_map_feature_scope', {
211
285
  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.',
286
+ description: 'Resolve a feature (featureId, or free-text query such as "checkout") to a focused test scope: code symbols, static + ' +
287
+ 'runtime screens, existing tests, objective, coverage gaps, strategy, and ranked candidates (one disambiguation ' +
288
+ 'question only on a genuine tie). Works without a map (falls back to a code scan). Read-only.',
213
289
  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".'),
290
+ projectRoot: z.string().optional().describe('Project root when no session exists.'),
291
+ sessionId: z.string().optional().describe('Adds runtime screen-graph evidence.'),
292
+ featureId: z.string().optional().describe('Exact map feature id (or use query).'),
293
+ query: z.string().optional().describe('e.g. "checkout flow".'),
294
+ platform: z.enum(['android', 'ios']).optional(),
295
+ includeCode: z.boolean().optional().describe('query: scan source code (default true).'),
296
+ limit: z.number().optional().describe('Query mode: max items per list in the scope (default 8).'),
218
297
  },
219
- outputSchema: { ok: z.boolean(), featureId: z.string().optional() },
220
- }, async ({ projectRoot, sessionId, featureId, query }) => {
221
- const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
298
+ }, async ({ projectRoot, sessionId, featureId, query, platform, includeCode, limit }) => {
299
+ if (!featureId && !query) {
300
+ return qaError({
301
+ what: 'Provide one of: featureId or query',
302
+ failureCode: 'INVALID_ARGUMENT',
303
+ changedState: false,
304
+ retrySafe: true,
305
+ nextSteps: ['e.g. qa_app_map_feature_scope { query:"login" }'],
306
+ });
307
+ }
308
+ // Free-text QUERY path: full feature scoping (app-map-first, code-index + runtime-graph
309
+ // fallback, objective model, candidate disambiguation). Works without an existing map.
310
+ if (!featureId) {
311
+ const r = await resolveFeatureContext(server, sessions, { sessionId, projectRoot, feature: query, platform, includeCode, limit });
312
+ if (!r.ok)
313
+ return r.result;
314
+ remember(r.ctx.root);
315
+ const { scopeResult, objective, index } = r.ctx;
316
+ const scope = scopeResult.primary;
317
+ if (!scopeResult.found) {
318
+ return qaOk({
319
+ sessionId: sessionId ?? null,
320
+ query,
321
+ found: false,
322
+ scope,
323
+ searched: scopeResult.searched,
324
+ nextRecommendedAction: {
325
+ tool: 'qa_test_this',
326
+ args: { ...(sessionId ? { sessionId } : { projectRoot: r.ctx.root }), goal: 'explore' },
327
+ why: 'Grow the map with an initial run, then re-scope the feature',
328
+ },
329
+ }, `🔎 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.`);
330
+ }
331
+ if (scopeResult.needsInput) {
332
+ return qaNeedsInput({
333
+ needsInput: true,
334
+ kind: 'monorepo_target',
335
+ question: scopeResult.needsInput.question,
336
+ fields: [{ name: 'query', description: 'The exact feature to test', example: scopeResult.candidates[0]?.title }],
337
+ fallbackOptions: scopeResult.needsInput.options,
338
+ resume: { tool: 'qa_app_map_feature_scope', args: {} },
339
+ attempted: [`scoped "${query}" matched ${scopeResult.candidates.length} distinct candidates that tie`],
340
+ ifDeclined: 'Swipium scopes the highest-confidence candidate and records the others as alternatives.',
341
+ }, { sessionId: sessionId ?? undefined, candidates: scopeResult.candidates });
342
+ }
343
+ const nextRecommendedAction = scope.recommendedStrategy === 'manual_blocked'
344
+ ? {
345
+ tool: 'qa_test_feature',
346
+ args: { ...(sessionId ? { sessionId } : { projectRoot: r.ctx.root }), feature: query, mode: 'plan' },
347
+ why: 'Review the plan + setup needed before any automated execution',
348
+ }
349
+ : {
350
+ tool: 'qa_test_feature',
351
+ args: { sessionId: sessionId ?? '${sessionId}', feature: query, mode: 'execute' },
352
+ why: 'Run a focused test of this feature',
353
+ };
354
+ return qaOk({
355
+ sessionId: sessionId ?? null,
356
+ query,
357
+ found: true,
358
+ featureId: scope.featureId,
359
+ scope,
360
+ objective,
361
+ appMapUri: r.ctx.appMapUri,
362
+ mapFeatureId: scopeResult.mapFeatureId ?? null,
363
+ ticketRefs: scopeResult.ticketRefs,
364
+ runtimeSource: scopeResult.runtimeSource,
365
+ candidates: scopeResult.candidates,
366
+ searched: scopeResult.searched,
367
+ codeIndex: { scannedFiles: index.scannedFiles, truncated: index.truncated },
368
+ nextRecommendedAction,
369
+ }, `🔎 ${scope.title} (confidence ${Math.round(scope.confidence * 100)}%, strategy ${scope.recommendedStrategy}): ` +
370
+ `${scope.staticScreens.length} static screen(s), ${scope.runtimeScreens.length} runtime screen(s), ${scope.functions.length} symbol(s), ${scope.existingTests.length} existing test(s).` +
371
+ (scopeResult.candidates.length > 1 ? ` ${scopeResult.candidates.length} candidate(s).` : ''));
372
+ }
373
+ // FEATURE-ID path: exact map lookup (requires an existing map).
374
+ const { root, hint, error } = await rootFor(server, sessions, { projectRoot, sessionId });
222
375
  if (!root)
223
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
376
+ return error ?? unresolvedProjectRootError({ source: 'none', hint });
224
377
  remember(root);
225
378
  const map = readExistingMap(root);
226
379
  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
- }
380
+ return qaError({
381
+ what: 'No app map yet',
382
+ changedState: false,
383
+ retrySafe: true,
384
+ nextSteps: ['Run qa_app_map_build first, or pass query= for map-free scoping.'],
385
+ failureCode: 'NO_APP_MAP',
386
+ });
387
+ const features = map.features.filter((f) => f.id === featureId);
388
+ if (!features.length)
389
+ return qaError({
390
+ what: `Unknown featureId ${featureId}`,
391
+ changedState: false,
392
+ retrySafe: true,
393
+ nextSteps: ['List ids with qa_app_map_read { section:"features" }, or pass query= for free-text scoping.'],
394
+ });
244
395
  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 }));
396
+ const staticScreens = f.staticScreens
397
+ .map((id) => map.staticTopology.screens.find((s) => s.id === id))
398
+ .filter(Boolean)
399
+ .map((s) => ({ id: s.id, name: s.name, route: s.route, sourceFiles: s.sourceFiles }));
246
400
  const needsCreds = f.id === 'feature:auth' || f.blockers.some((b) => /credential/i.test(b));
247
401
  const recommendedGoal = f.id === 'feature:auth' ? 'test_login' : 'reproduce_bug';
248
402
  return {
@@ -259,156 +413,134 @@ export function registerAppMap(server, sessions) {
259
413
  blockers: f.blockers,
260
414
  recommendedPlan: {
261
415
  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`,
416
+ args: {
417
+ mode: 'execute',
418
+ goal: recommendedGoal,
419
+ goalText: f.title,
420
+ explore: true,
421
+ ...(needsCreds ? { stopOnNeedsInput: true } : {}),
422
+ },
423
+ why: needsCreds
424
+ ? `Drive "${f.title}"; will stop for test credentials (fixture); ${f.testCoverage} coverage today`
425
+ : `Drive "${f.title}" with focused exploration; ${f.testCoverage} coverage today`,
264
426
  },
265
427
  };
266
428
  });
267
429
  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');
430
+ scope
431
+ .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)}`)
432
+ .join('\n');
269
433
  return qaOk({ appMapUri: appMapResourceUri(root), featureId: scope[0]?.featureId, scope }, text);
270
434
  });
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
435
  // ----------------------------------------------------------------- update
295
436
  server.registerTool('qa_app_map_update', {
296
437
  title: 'Update the app knowledge map',
297
- 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.",
438
+ description: 'Targeted, provenance-tracked app-map edits without a rebuild: add a note, register test cases, link an automation ' +
439
+ "suite to features/screens, set the environment, or override a feature's coverage. Existing entries with the same " +
440
+ 'id/path are overwritten.',
298
441
  inputSchema: {
299
442
  projectRoot: z.string().optional(),
300
443
  sessionId: z.string().optional(),
301
444
  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(),
445
+ testCases: z
446
+ .array(z.object({
447
+ id: z.string(),
448
+ title: z.string(),
449
+ featureId: z.string().optional(),
450
+ screenId: z.string().optional(),
451
+ status: z.string().optional(),
452
+ source: z.string().optional(),
453
+ stale: z.boolean().optional(),
454
+ }))
455
+ .optional(),
456
+ automationSuite: z
457
+ .object({
458
+ name: z.string(),
459
+ path: z.string(),
460
+ framework: z.string().optional(),
461
+ linkedFeatureIds: z.array(z.string()).optional(),
462
+ linkedScreenIds: z.array(z.string()).optional(),
463
+ })
464
+ .optional(),
304
465
  environment: z.string().optional().describe("App environment, e.g. 'test' | 'staging'."),
305
466
  featureCoverage: z.object({ featureId: z.string(), coverage: z.enum(['none', 'partial', 'covered']) }).optional(),
306
467
  },
307
- outputSchema: { ok: z.boolean(), appMapUri: z.string().optional(), applied: z.array(z.string()).optional() },
308
468
  }, async ({ projectRoot, sessionId, note, testCases, automationSuite, environment, featureCoverage }) => {
309
- const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
469
+ const { root, hint, error } = await rootFor(server, sessions, { projectRoot, sessionId });
310
470
  if (!root)
311
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
471
+ return error ?? unresolvedProjectRootError({ source: 'none', hint });
312
472
  remember(root);
313
- const map = readExistingMap(root);
314
- 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' });
316
- const at = nowIso();
317
- const applied = [];
318
- if (note) {
319
- addProvenance(map, makeProvenance('user_note', at, note, { targetType: 'map' }));
320
- applied.push('note');
321
- }
322
- if (testCases?.length) {
323
- for (const c of testCases) {
324
- const existing = map.testSuite.cases.find((x) => x.id === c.id);
325
- if (existing)
326
- Object.assign(existing, c);
327
- else
328
- map.testSuite.cases.push(c);
473
+ // Synchronous load > mutate > save cycle, held under the cross-process app-map lock (see store.ts).
474
+ return withAppMapLock(root, () => {
475
+ const map = readExistingMap(root);
476
+ if (!map)
477
+ return qaError({
478
+ what: 'No app map yet',
479
+ changedState: false,
480
+ retrySafe: true,
481
+ nextSteps: ['Run qa_app_map_build first.'],
482
+ failureCode: 'NO_APP_MAP',
483
+ });
484
+ const at = nowIso();
485
+ const applied = [];
486
+ if (note) {
487
+ addProvenance(map, makeProvenance('user_note', at, note, { targetType: 'map' }));
488
+ applied.push('note');
329
489
  }
330
- addProvenance(map, makeProvenance('test_case', at, `${testCases.length} test case(s) registered`, { targetType: 'test' }));
331
- applied.push(`testCases(${testCases.length})`);
332
- }
333
- if (automationSuite) {
334
- const existing = map.automation.suites.find((s) => s.path === automationSuite.path);
335
- if (existing)
336
- Object.assign(existing, automationSuite);
337
- else
338
- map.automation.suites.push(automationSuite);
339
- addProvenance(map, makeProvenance('test_case', at, `Automation suite ${automationSuite.name} linked`, { targetType: 'test', refs: [automationSuite.path] }));
340
- applied.push('automationSuite');
341
- }
342
- if (environment) {
343
- map.appIdentity.environment = environment;
344
- applied.push('environment');
345
- }
346
- if (featureCoverage) {
347
- const f = map.features.find((x) => x.id === featureCoverage.featureId);
348
- if (!f)
349
- return qaError({ what: `Unknown featureId ${featureCoverage.featureId}`, changedState: false, retrySafe: true, nextSteps: ['List ids with qa_app_map_read { section:"features" }.'] });
350
- f.testCoverage = featureCoverage.coverage;
351
- addProvenance(map, makeProvenance('user_note', at, `coverage(${f.id})=${featureCoverage.coverage}`, { targetType: 'feature', targetId: f.id }));
352
- applied.push('featureCoverage');
353
- }
354
- 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.'] });
356
- map.updatedAt = at;
357
- map.coverage.staleTests = map.testSuite.cases.filter((c) => c.stale).length;
358
- recomputeConfidence(map);
359
- const save = saveAppMap(root, map);
360
- saveIndexes(root, loadCodeIndex(root), map.features);
361
- return qaOk({ appMapUri: save.resourceUri, applied }, `app map updated: ${applied.join(', ')}\nappMapUri: ${save.resourceUri}`);
362
- });
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;
490
+ if (testCases?.length) {
491
+ for (const c of testCases) {
492
+ const existing = map.testSuite.cases.find((x) => x.id === c.id);
493
+ if (existing)
494
+ Object.assign(existing, c);
495
+ else
496
+ map.testSuite.cases.push(c);
497
+ }
498
+ addProvenance(map, makeProvenance('test_case', at, `${testCases.length} test case(s) registered`, { targetType: 'test' }));
499
+ applied.push(`testCases(${testCases.length})`);
385
500
  }
386
- catch {
387
- return null;
501
+ if (automationSuite) {
502
+ const existing = map.automation.suites.find((s) => s.path === automationSuite.path);
503
+ if (existing)
504
+ Object.assign(existing, automationSuite);
505
+ else
506
+ map.automation.suites.push(automationSuite);
507
+ addProvenance(map, makeProvenance('test_case', at, `Automation suite ${automationSuite.name} linked`, {
508
+ targetType: 'test',
509
+ refs: [automationSuite.path],
510
+ }));
511
+ applied.push('automationSuite');
388
512
  }
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}`);
513
+ if (environment) {
514
+ map.appIdentity.environment = environment;
515
+ applied.push('environment');
403
516
  }
404
- catch {
405
- /* none */
517
+ if (featureCoverage) {
518
+ const f = map.features.find((x) => x.id === featureCoverage.featureId);
519
+ if (!f)
520
+ return qaError({
521
+ what: `Unknown featureId ${featureCoverage.featureId}`,
522
+ changedState: false,
523
+ retrySafe: true,
524
+ nextSteps: ['List ids with qa_app_map_read { section:"features" }.'],
525
+ });
526
+ f.testCoverage = featureCoverage.coverage;
527
+ addProvenance(map, makeProvenance('user_note', at, `coverage(${f.id})=${featureCoverage.coverage}`, { targetType: 'feature', targetId: f.id }));
528
+ applied.push('featureCoverage');
406
529
  }
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}`);
530
+ if (!applied.length)
531
+ return qaError({
532
+ what: 'No update fields provided',
533
+ changedState: false,
534
+ retrySafe: true,
535
+ nextSteps: ['Pass at least one of: note, testCases, automationSuite, environment, featureCoverage.'],
536
+ });
537
+ map.updatedAt = at;
538
+ map.coverage.staleTests = map.testSuite.cases.filter((c) => c.stale).length;
539
+ recomputeConfidence(map);
540
+ const save = saveAppMap(root, map);
541
+ saveIndexes(root, loadCodeIndex(root), map.features);
542
+ return qaOk({ appMapUri: save.resourceUri, applied }, `app map updated: ${applied.join(', ')}\nappMapUri: ${save.resourceUri}`);
543
+ });
412
544
  });
413
545
  }
414
546
  /** Resolve a feature/screen/test-suite/full app-map resource read for the MCP resource handler.
@@ -436,4 +568,76 @@ export function readAppMapResource(root, sub) {
436
568
  }
437
569
  return null;
438
570
  }
571
+ /** Project roots whose app map can be listed RIGHT NOW: roots touched this server run
572
+ * (in-memory registry) plus live-session roots, filtered to those with an app map on disk.
573
+ * Deliberately NOT the durable machine-wide registry. Listing every project ever mapped on
574
+ * this machine into an unrelated client session would be noise, not discovery. */
575
+ function appMapRoots(sessions) {
576
+ const roots = new Set(projectRegistry.values());
577
+ for (const s of sessions.list())
578
+ roots.add(s.root);
579
+ return [...roots].filter((r) => existsSync(appMapPath(r)));
580
+ }
581
+ /** Enumerate the actually-readable app-map resource URIs for the MCP resources/list callbacks.
582
+ * `full`: one complete-map URI per known project; `sections`: the per-feature /
583
+ * per-screen / test-suite section URIs served by readAppMapResource above (same URI shapes
584
+ * qa_app_map_read emits). Section ids are percent-encoded (encodeUriSegment) so ids containing
585
+ * `/`, spaces or `(` still match the `{kind}/{id}` template; the read handlers decode them.
586
+ * Read-only and never throws: an unparseable map is skipped, and an
587
+ * empty world lists as []. Caller applies any cap. */
588
+ export function listAppMapResources(sessions, which) {
589
+ const out = [];
590
+ for (const root of appMapRoots(sessions)) {
591
+ try {
592
+ const map = loadAppMap(root, fallbackProject(root), nowIso()).map;
593
+ if (!map)
594
+ continue;
595
+ const base = appMapResourceUri(root);
596
+ const project = map.appIdentity.appName ?? basename(root);
597
+ if (which === 'full') {
598
+ out.push({
599
+ uri: base,
600
+ name: `app-map (${project})`,
601
+ mimeType: 'application/json',
602
+ description: `Complete app knowledge map for ${root}`,
603
+ });
604
+ continue;
605
+ }
606
+ for (const f of map.features) {
607
+ out.push({
608
+ uri: `${base}/feature/${encodeUriSegment(f.id)}`,
609
+ name: f.title,
610
+ mimeType: 'application/json',
611
+ description: `feature section: ${project}`,
612
+ });
613
+ }
614
+ // Static + runtime screens share the /screen/{id} read path; dedupe on id (static wins).
615
+ const screens = new Map();
616
+ for (const s of map.staticTopology.screens)
617
+ screens.set(s.id, s.name);
618
+ for (const r of map.runtimeTopology.screens)
619
+ if (!screens.has(r.id))
620
+ screens.set(r.id, r.title ?? r.id);
621
+ for (const [id, name] of screens) {
622
+ out.push({
623
+ uri: `${base}/screen/${encodeUriSegment(id)}`,
624
+ name,
625
+ mimeType: 'application/json',
626
+ description: `screen section: ${project}`,
627
+ });
628
+ }
629
+ // The read path ignores {id} for test-suite; "cases" is the listed canonical spelling.
630
+ out.push({
631
+ uri: `${base}/test-suite/cases`,
632
+ name: `test-suite (${project})`,
633
+ mimeType: 'application/json',
634
+ description: `test-suite section: ${map.testSuite.cases.length} cases`,
635
+ });
636
+ }
637
+ catch {
638
+ // Listing must never throw. A broken map is simply not browsable; reads still error loudly.
639
+ }
640
+ }
641
+ return out;
642
+ }
439
643
  //# sourceMappingURL=appMap.js.map