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,15 +1,24 @@
1
- // Session + job + artifact store with DISK PERSISTENCE (DESIGN §3, M6).
1
+ // Session + job + artifact store with DISK PERSISTENCE.
2
2
  // Live, non-serializable bits (driver, lastSnapshot, abort controllers) stay in memory;
3
- // a serializable subset is written to <sessionDir>/state.json on every mutation, and a
4
- // small registry under ~/.swipium/registry.json lets a fresh server instance reload
5
- // prior sessions so artifacts/reports/job-status survive a server restart. (A restart does
6
- // NOT resurrect a running child process — such jobs are marked failed on reload.)
3
+ // a serializable subset is written to <sessionDir>/state.json on mutation (debounced;
4
+ // synchronous for the mutation ledger + session creation + shutdown flush), and a small
5
+ // registry under ~/.swipium/registry.json (lock-guarded, lazily loaded on first access)
6
+ // lets a fresh server instance reload prior sessions so artifacts/reports/job-status survive
7
+ // a server restart. (A restart does NOT resurrect a running child process: such jobs are
8
+ // marked failed on reload, and orphaned Metro pids are verified via `ps` and reaped.)
7
9
  import { createHash, randomUUID } from 'node:crypto';
8
- import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
9
- import { join, resolve } from 'node:path';
10
+ import { chmodSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
11
+ import { extname, join, resolve, sep } from 'node:path';
10
12
  import { homedir } from 'node:os';
11
13
  import { DEFAULT_RESPONSE_MODE } from '../lib/result.js';
12
- import { makeRedactor } from '../lib/redact.js';
14
+ import { makeRedactor, redactDeep, redactStructuredText, structuredKindOf } from '../lib/redact.js';
15
+ import { generatedOutputRedactor, inputBindings, inputPlaceholderFor, secretSafeActions, secretSafeNotes, swipiumVarName, } from '../suite/secretGuard.js';
16
+ import { withFileLock, writeFileAtomicSync } from '../lib/lockfile.js';
17
+ import { log } from '../lib/logger.js';
18
+ import { pidOwnedByLiveServer, reclaimPid } from './processRegistry.js';
19
+ import { fixtureMetadata, rehydrateFixtures } from '../fixtures/load.js';
20
+ import { scheduleStartupPrune } from './retention.js';
21
+ import { approvalMechanismFor } from '../consent/consent.js';
13
22
  // Phase 2.2 P1.6: budget classes (minutes) keyed by workflow ambition.
14
23
  export const BUDGET_PROFILES = {
15
24
  guardrail: 8, // guardrail/setup validation
@@ -17,7 +26,7 @@ export const BUDGET_PROFILES = {
17
26
  full_smoke: 15, // full authenticated smoke
18
27
  install_smoke: 20, // install/boot/rebuild involved
19
28
  };
20
- // Security model (REQ-02): raw secret values (generated passwords/OTPs/tokens) must never touch
29
+ // Security model: raw secret values (generated passwords/OTPs/tokens) must never touch
21
30
  // disk. In memory we keep the raw value (for same-session reuse via inputValues), but the
22
31
  // serialized form sent to state.json redacts secret values while preserving all reproducibility
23
32
  // metadata (varName/field/generator/secret/artifactUri/timestamps). Non-secret generated values
@@ -25,6 +34,36 @@ export const BUDGET_PROFILES = {
25
34
  export function serializeGeneratedValues(records) {
26
35
  return records.map((r) => (r.secret ? { ...r, value: '<redacted>' } : r));
27
36
  }
37
+ /** state.json form of the session's free-text records: raw registered secret values never touch
38
+ * disk. Recorded actions go through the same rewrite the generators use (a secret literal typed into
39
+ * a non-secure field becomes a secret step with no text); note/finding prose is redacted. URIs,
40
+ * paths and numbers are left alone. */
41
+ export function serializeSecretSafe(s) {
42
+ const toolErrors = s.toolErrors ?? [];
43
+ const jobs = [...(s.jobs?.values() ?? [])];
44
+ const envChanges = s.envChanges ?? [];
45
+ const mutations = s.mutations ?? [];
46
+ // Fixture values and seed specs are live config, never persisted (not even redacted): a
47
+ // rehydrated session re-reads them from .swipium/fixtures.json (see fixtures/load.ts).
48
+ const fixtures = fixtureMetadata(s.fixtures ?? []);
49
+ if (!s.secrets.size)
50
+ return { recordedActions: s.recordedActions, notes: s.notes, findings: s.findings, toolErrors, jobs, envChanges, mutations, fixtures };
51
+ const redact = makeRedactor(s.secrets);
52
+ const r = (v) => (v ? (redact(v) ?? v) : v);
53
+ // Structured records (job results, mutation targets, fixtures): strong secrets scrubbed anywhere,
54
+ // weak (dictionary-like) ones only as a whole value, so package ids / selectors stay intact.
55
+ const structured = generatedOutputRedactor(s.secrets);
56
+ return {
57
+ recordedActions: secretSafeActions(s.recordedActions, s.secrets).actions,
58
+ notes: secretSafeNotes(s.notes, s.secrets),
59
+ findings: s.findings.map((f) => ({ ...f, detail: r(f.detail) ?? f.detail, evidence: r(f.evidence) })),
60
+ toolErrors: toolErrors.map((t) => ({ ...t, message: r(t.message) ?? t.message })),
61
+ jobs: jobs.map((j) => ({ ...redactDeep(j, structured), error: r(j.error), progress: r(j.progress), resultText: r(j.resultText) })),
62
+ envChanges: envChanges.map((e) => r(e) ?? e),
63
+ mutations: mutations.map((m) => ({ ...redactDeep(m, structured), detail: r(m.detail) })),
64
+ fixtures: redactDeep(fixtures, structured),
65
+ };
66
+ }
28
67
  export const DEFAULT_BUDGET = {
29
68
  maxMinutes: 8,
30
69
  maxActions: 20,
@@ -40,8 +79,10 @@ export function isTextArtifactMime(mime) {
40
79
  }
41
80
  export function artifactDirectoryName(kind) {
42
81
  switch (kind) {
43
- case 'screenshot': return 'screenshots';
44
- case 'recording': return 'videos';
82
+ case 'screenshot':
83
+ return 'screenshots';
84
+ case 'recording':
85
+ return 'videos';
45
86
  case 'logs':
46
87
  case 'logcat':
47
88
  case 'metro':
@@ -51,60 +92,247 @@ export function artifactDirectoryName(kind) {
51
92
  return kind;
52
93
  }
53
94
  }
95
+ /** Filesystem-safe artifact file name (review B5): path separators, NUL/control chars and `..`
96
+ * runs are replaced, so a caller-supplied name (e.g. a visual baseline name) can never escape
97
+ * the session directory. Long names are truncated, keeping the extension. */
98
+ export function safeArtifactName(name) {
99
+ let n = String(name)
100
+ // eslint-disable-next-line no-control-regex
101
+ .replace(/[\\/\x00-\x1f]/g, '_')
102
+ .replace(/\.{2,}/g, '_')
103
+ .trim();
104
+ if (!n || n === '.')
105
+ n = 'artifact';
106
+ if (n.length > 180) {
107
+ const ext = extname(n).slice(0, 16);
108
+ n = n.slice(0, 180 - ext.length) + ext;
109
+ }
110
+ return n;
111
+ }
112
+ /** Percent-encode the characters that would stop a URI segment from matching the
113
+ * swipium:// resource templates (`/`, `,`, space, `%`, `?`, `#`, non-ASCII…). Readable ids such as
114
+ * `feature:login` stay as-is; pair with decodeUriSegment on the read side. */
115
+ export function encodeUriSegment(value) {
116
+ return value.replace(/[^A-Za-z0-9\-._~:@!$&'()*+;=]/gu, (c) => encodeURIComponent(c));
117
+ }
118
+ /** Inverse of encodeUriSegment; returns the input unchanged if it is not valid percent-encoding. */
119
+ export function decodeUriSegment(value) {
120
+ try {
121
+ return decodeURIComponent(value);
122
+ }
123
+ catch {
124
+ return value;
125
+ }
126
+ }
127
+ /** Canonical form of a swipium:// URI for equality: every path segment decoded. */
128
+ function canonicalUri(uri) {
129
+ return uri.split('/').map(decodeUriSegment).join('/');
130
+ }
131
+ /** True when `child` is `parent` or lies inside it (both resolved). */
132
+ export function isWithinRoot(child, parent) {
133
+ const c = resolve(child);
134
+ const p = resolve(parent);
135
+ return c === p || c.startsWith(p.endsWith(sep) ? p : p + sep);
136
+ }
137
+ /** Owner-only permissions for ~/.swipium/runs session data (dirs 0700, files 0600). POSIX only;
138
+ * best-effort (a chmod failure never breaks a write). */
139
+ export const PRIVATE_DIR_MODE = 0o700;
140
+ export const PRIVATE_FILE_MODE = 0o600;
141
+ function restrictFile(path) {
142
+ if (process.platform === 'win32')
143
+ return;
144
+ try {
145
+ chmodSync(path, PRIVATE_FILE_MODE);
146
+ }
147
+ catch {
148
+ /* best-effort */
149
+ }
150
+ }
151
+ function mkdirPrivate(dir) {
152
+ mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
153
+ if (process.platform === 'win32')
154
+ return;
155
+ try {
156
+ chmodSync(dir, PRIVATE_DIR_MODE); // mode above only applies to NEWLY created dirs (and umask)
157
+ }
158
+ catch {
159
+ /* best-effort */
160
+ }
161
+ }
54
162
  function defaultSessionDir(root, id) {
55
163
  const projectHash = createHash('sha256').update(resolve(root)).digest('hex').slice(0, 16);
56
164
  return join(REGISTRY_DIR, 'runs', projectHash, id);
57
165
  }
166
+ /** Trailing debounce for state.json writes (P2 write amplification): every mutator used to
167
+ * rewrite the full file synchronously. Correctness-critical writes (session creation, the
168
+ * mutation ledger, shutdown) still flush synchronously via persistNow()/flushAll(). */
169
+ const PERSIST_DEBOUNCE_MS = 150;
58
170
  export class SessionStore {
59
171
  sessions = new Map();
60
- constructor() {
172
+ // Lazy prior-session loading (P2 startup cost): the registry of up to 200 prior sessions is
173
+ // read on first access (get/list/find/create), not in the constructor.
174
+ registryLoaded = false;
175
+ dirty = new Set();
176
+ flushTimer;
177
+ // Sessions created or looked up by id in THIS process (see activeRoots).
178
+ activeIds = new Set();
179
+ ensureRegistryLoaded() {
180
+ if (this.registryLoaded)
181
+ return;
182
+ this.registryLoaded = true;
61
183
  this.loadRegistry();
184
+ // Retention (session/retention.ts): one non-blocking background prune of old ~/.swipium/runs
185
+ // session dirs per process, never a registered or live session. SWIPIUM_RETENTION_DAYS=off disables.
186
+ scheduleStartupPrune(() => [...this.sessions.values()].map((x) => x.dir));
62
187
  }
63
188
  create(root, budget, opts) {
189
+ this.ensureRegistryLoaded();
64
190
  const id = randomUUID().slice(0, 8);
65
191
  const dir = opts?.sessionDir ?? defaultSessionDir(root, id);
66
- mkdirSync(dir, { recursive: true });
192
+ mkdirPrivate(dir);
67
193
  const now = Date.now();
68
194
  const s = {
69
- id, root, dir, createdAt: now, screenshotCount: 0,
195
+ id,
196
+ root,
197
+ dir,
198
+ createdAt: now,
199
+ screenshotCount: 0,
70
200
  mode: 'structured',
71
201
  responseMode: opts?.responseMode ?? DEFAULT_RESPONSE_MODE,
72
202
  sensitive: opts?.sensitive ?? false,
73
203
  budget: { ...DEFAULT_BUDGET, ...(budget ?? {}) },
74
204
  counters: { actions: 0, screenshots: 0, snapshotFailures: 0, noChangeActions: 0 },
75
- envChanges: [], workarounds: [],
76
- jobs: new Map(), artifacts: [], findings: [], notes: [], mutations: [], recordedActions: [],
77
- fixtures: opts?.fixtures ?? [], auth: {}, milestones: { session_start: now }, budgetProfile: opts?.budgetProfile,
78
- secrets: new Set(), inputs: [], generatedValues: [], inputValues: new Map(), aborts: new Map(),
205
+ envChanges: [],
206
+ workarounds: [],
207
+ jobs: new Map(),
208
+ artifacts: [],
209
+ findings: [],
210
+ notes: [],
211
+ mutations: [],
212
+ recordedActions: [],
213
+ toolErrors: [],
214
+ fixtures: opts?.fixtures ?? [],
215
+ auth: {},
216
+ milestones: { session_start: now },
217
+ budgetProfile: opts?.budgetProfile,
218
+ secrets: new Set(),
219
+ inputs: [],
220
+ generatedValues: [],
221
+ inputValues: new Map(),
222
+ aborts: new Map(),
79
223
  };
80
224
  this.sessions.set(id, s);
225
+ this.activeIds.add(id);
81
226
  this.appendRegistry(id, dir);
82
- this.persist(s);
227
+ this.persistNow(s); // synchronous: a concurrent instance / crash must see the new session
83
228
  return s;
84
229
  }
85
230
  get(id) {
86
- return this.sessions.get(id);
231
+ this.ensureRegistryLoaded();
232
+ const s = this.sessions.get(id);
233
+ if (s)
234
+ this.activeIds.add(id);
235
+ return s;
87
236
  }
88
237
  list() {
238
+ this.ensureRegistryLoaded();
89
239
  return [...this.sessions.values()];
90
240
  }
241
+ /** Schedule a state.json write (trailing debounce). Mutators call this on every change;
242
+ * the actual write happens at most once per PERSIST_DEBOUNCE_MS per burst. Use
243
+ * persistNow()/flushAll() where a synchronous write is required for correctness. */
91
244
  persist(s) {
245
+ this.dirty.add(s);
246
+ if (this.flushTimer)
247
+ return;
248
+ this.flushTimer = setTimeout(() => this.flushDirty(), PERSIST_DEBOUNCE_MS);
249
+ this.flushTimer.unref?.(); // never keep the process alive just for a pending flush
250
+ }
251
+ /** Write this session's state.json immediately (and drop any pending debounce for it). */
252
+ persistNow(s) {
253
+ this.dirty.delete(s);
254
+ this.writeState(s);
255
+ }
256
+ /** Flush every dirty session synchronously. Called on shutdown / process exit so a graceful
257
+ * stop never loses debounced state. */
258
+ flushAll() {
259
+ if (this.flushTimer) {
260
+ clearTimeout(this.flushTimer);
261
+ this.flushTimer = undefined;
262
+ }
263
+ for (const s of [...this.dirty]) {
264
+ this.dirty.delete(s);
265
+ this.writeState(s);
266
+ }
267
+ }
268
+ flushDirty() {
269
+ this.flushTimer = undefined;
270
+ for (const s of [...this.dirty]) {
271
+ this.dirty.delete(s);
272
+ this.writeState(s);
273
+ }
274
+ }
275
+ writeState(s) {
92
276
  try {
277
+ const safe = serializeSecretSafe(s); // registered secret values never reach state.json
278
+ // Keep the plain transport fields in sync with the live driver (duck-typed: no driver import),
279
+ // except while the live driver is a fallback for an unreachable WDA: the persisted 'wda'
280
+ // transport stays authoritative so a later restart retries it.
281
+ if (s.driver && !(s.transportFallback && s.driver.kind === 'simulator')) {
282
+ s.driverKind = s.driver.kind;
283
+ const baseUrl = s.driver.baseUrl;
284
+ if (s.driver.kind === 'wda' && typeof baseUrl === 'string')
285
+ s.wdaUrl = baseUrl;
286
+ }
93
287
  const state = {
94
- id: s.id, root: s.root, dir: s.dir, createdAt: s.createdAt,
95
- device: s.device, appId: s.appId, headless: s.headless, metroPid: s.metroPid, screenshotCount: s.screenshotCount,
96
- network: s.network, envChanges: s.envChanges, workarounds: s.workarounds,
97
- mode: s.mode, responseMode: s.responseMode, sensitive: s.sensitive, budget: s.budget, counters: s.counters,
98
- jobs: [...s.jobs.values()], artifacts: s.artifacts, findings: s.findings, notes: s.notes, mutations: s.mutations, recordedActions: s.recordedActions,
99
- fixtures: s.fixtures, auth: s.auth, milestones: s.milestones, budgetProfile: s.budgetProfile,
100
- inputs: s.inputs, // METADATA only — never the values
288
+ id: s.id,
289
+ root: s.root,
290
+ dir: s.dir,
291
+ createdAt: s.createdAt,
292
+ device: s.device,
293
+ driverKind: s.driverKind,
294
+ wdaUrl: s.wdaUrl,
295
+ appId: s.appId,
296
+ headless: s.headless,
297
+ metroPid: s.metroPid,
298
+ screenshotCount: s.screenshotCount,
299
+ network: s.network,
300
+ envChanges: safe.envChanges,
301
+ workarounds: s.workarounds,
302
+ chosenTarget: s.chosenTarget,
303
+ lastTestThisArgs: s.lastTestThisArgs && s.secrets.size ? redactDeep(s.lastTestThisArgs, makeRedactor(s.secrets)) : s.lastTestThisArgs,
304
+ mode: s.mode,
305
+ responseMode: s.responseMode,
306
+ sensitive: s.sensitive,
307
+ budget: s.budget,
308
+ counters: s.counters,
309
+ jobs: safe.jobs,
310
+ artifacts: s.artifacts,
311
+ findings: safe.findings,
312
+ notes: safe.notes,
313
+ mutations: safe.mutations,
314
+ recordedActions: safe.recordedActions,
315
+ toolErrors: safe.toolErrors,
316
+ fixtures: safe.fixtures,
317
+ auth: s.auth,
318
+ milestones: s.milestones,
319
+ budgetProfile: s.budgetProfile,
320
+ inputs: s.inputs, // METADATA only, never the values
101
321
  generatedValues: serializeGeneratedValues(s.generatedValues), // secret raw values redacted before disk
102
322
  exploration: s.exploration,
103
323
  };
104
- writeFileSync(join(s.dir, 'state.json'), JSON.stringify(state, null, 2));
324
+ // Atomic write (tmp + rename) so a crash mid-write never leaves a truncated state.json.
325
+ const target = join(s.dir, 'state.json');
326
+ writeFileAtomicSync(target, JSON.stringify(state, null, 2));
327
+ restrictFile(target); // owner-only: state.json carries run history / paths
105
328
  }
106
- catch {
107
- /* best-effort */
329
+ catch (e) {
330
+ // A silent failure here means session state (jobs/artifacts/findings) is lost on restart.
331
+ log('error', 'failed to persist session state.json; this session will not survive a server restart', {
332
+ sessionId: s.id,
333
+ dir: s.dir,
334
+ err: String(e),
335
+ });
108
336
  }
109
337
  }
110
338
  // ---- jobs ----
@@ -123,6 +351,20 @@ export class SessionStore {
123
351
  s.mode = mode;
124
352
  this.persist(s);
125
353
  }
354
+ /**
355
+ * A structured UI dump just succeeded on this session: visual-fallback is per-screen, never
356
+ * permanent. Switch back to 'structured' and restart the consecutive-failure count. Returns
357
+ * true when the session was in visual-fallback (the caller may tell the agent it recovered).
358
+ */
359
+ noteStructuredDump(s) {
360
+ const recovered = s.mode === 'visual-fallback';
361
+ if (!recovered && s.counters.snapshotFailures === 0)
362
+ return false;
363
+ s.mode = 'structured';
364
+ s.counters.snapshotFailures = 0;
365
+ this.persist(s);
366
+ return recovered;
367
+ }
126
368
  bump(s, key, by = 1) {
127
369
  s.counters[key] += by;
128
370
  this.persist(s);
@@ -139,14 +381,14 @@ export class SessionStore {
139
381
  if (c.screenshots >= b.maxScreenshots)
140
382
  return `screenshot budget reached (${c.screenshots}/${b.maxScreenshots})`;
141
383
  if (c.noChangeActions >= b.maxNoChangeActions)
142
- return `repeated no-change actions (${c.noChangeActions}/${b.maxNoChangeActions}) — likely wrong coords / disabled element / auth wall`;
384
+ return `repeated no-change actions (${c.noChangeActions}/${b.maxNoChangeActions}): likely wrong coords / disabled element / auth wall`;
143
385
  return null;
144
386
  }
145
387
  updateJob(s, job, patch) {
146
388
  Object.assign(job, patch);
147
389
  this.persist(s);
148
390
  }
149
- /** Apply a patch ONLY while the job is still running — so a cancelled (or otherwise
391
+ /** Apply a patch ONLY while the job is still running, so a cancelled (or otherwise
150
392
  * terminal) job is never overwritten back to done/failed by a racing worker. */
151
393
  updateJobIfRunning(s, job, patch) {
152
394
  const cur = s.jobs.get(job.jobId);
@@ -175,7 +417,7 @@ export class SessionStore {
175
417
  s.envChanges.push(`${new Date().toISOString()} ${note}`);
176
418
  this.persist(s);
177
419
  }
178
- /** Record a safe fallback Swipium chose (roadmap §11 "workarounds attempted"). De-duped. */
420
+ /** Record a safe fallback Swipium chose. De-duped. */
179
421
  addWorkaround(s, note) {
180
422
  if (!s.workarounds.includes(note)) {
181
423
  s.workarounds.push(note);
@@ -187,15 +429,43 @@ export class SessionStore {
187
429
  this.persist(s);
188
430
  }
189
431
  recordMutation(s, mutation) {
190
- const rec = { id: randomUUID().slice(0, 8), at: Date.now(), ...mutation };
432
+ // Audit trail (THREAT_MODEL): stamp HOW a consented action was approved (elicitation vs
433
+ // client re-call assertion) from the consent module, so every call site gets it for free.
434
+ let consent = mutation.consent;
435
+ if (consent?.approved && consent.consentId && !consent.approvalMechanism) {
436
+ const mechanism = approvalMechanismFor(consent.consentId);
437
+ if (mechanism)
438
+ consent = { ...consent, approvalMechanism: mechanism };
439
+ }
440
+ const rec = { id: randomUUID().slice(0, 8), at: Date.now(), ...mutation, ...(consent ? { consent } : {}) };
191
441
  s.mutations.push(rec);
192
442
  if (s.mutations.length > 500)
193
443
  s.mutations.splice(0, s.mutations.length - 500);
194
- this.persist(s);
444
+ this.persistNow(s); // the mutation ledger is an audit trail; never leave it debounce-only
195
445
  return rec;
196
446
  }
197
- /** Append an action-IR step (bounded) for qa_flow_generate. */
447
+ /** Record a tool call that returned an error for this session (bounded to the last 200). */
448
+ recordToolError(s, rec) {
449
+ if (rec.failureCode === 'CANCELLED')
450
+ return; // cancelled work is not a tool error
451
+ const list = (s.toolErrors ??= []);
452
+ list.push({ at: rec.at ?? Date.now(), tool: rec.tool, failureCode: rec.failureCode, message: rec.message.slice(0, 500) });
453
+ if (list.length > 200)
454
+ list.splice(0, list.length - 200);
455
+ this.persist(s);
456
+ }
457
+ /** Append an action-IR step (bounded) for qa_generate target:"flow". */
198
458
  addRecordedAction(s, ra) {
459
+ // Typed text equal to a stored session input (e.g. the email from qa_continue_from_blocker) is
460
+ // recorded as that input's ${VAR} placeholder, never the literal (generators do this too).
461
+ const bound = ra.action === 'type' ? inputPlaceholderFor(ra.text, inputBindings(s)) : undefined;
462
+ if (bound) {
463
+ ra = {
464
+ ...ra,
465
+ text: `\${${swipiumVarName(bound.varName)}}`,
466
+ ...(bound.secret ? { secret: true, exportability: 'needs-human-data' } : {}),
467
+ };
468
+ }
199
469
  s.recordedActions.push(ra);
200
470
  if (s.recordedActions.length > 300)
201
471
  s.recordedActions.splice(0, s.recordedActions.length - 300);
@@ -221,7 +491,9 @@ export class SessionStore {
221
491
  this.persist(s);
222
492
  }
223
493
  /** Store a user-provided input (hardening P0.5). The VALUE stays in-memory only; secrets are
224
- * added to the redaction set. Persists metadata so reports can say it was provided. */
494
+ * added to the redaction set. Persists metadata so reports can say it was provided.
495
+ * Secrets of 3+ characters are redacted (short ones as whole tokens); 1-2 character values are
496
+ * not matched and are reported as partial redaction (src/lib/redact.ts). */
225
497
  setInput(s, varName, value, secret, source) {
226
498
  s.inputValues.set(varName, value);
227
499
  if (secret)
@@ -237,7 +509,7 @@ export class SessionStore {
237
509
  }
238
510
  this.persist(s);
239
511
  }
240
- /** Variable map (varName → value) for the flow runner. In-memory values only. */
512
+ /** Variable map (varName > value) for the flow runner. In-memory values only. */
241
513
  inputVariables(s) {
242
514
  return Object.fromEntries(s.inputValues);
243
515
  }
@@ -245,33 +517,86 @@ export class SessionStore {
245
517
  clearInputValues(s) {
246
518
  s.inputValues.clear();
247
519
  }
520
+ /** Remember the ORIGINAL qa_test_this arguments (goal, goalText, flags) of this session's run. */
521
+ setLastTestThisArgs(s, args) {
522
+ s.lastTestThisArgs = { ...args, at: args.at ?? Date.now() };
523
+ this.persist(s);
524
+ }
248
525
  /** Record the latest guided-exploration result (Phase 3.3) for qa_report. */
249
526
  setExploration(s, rec) {
250
527
  s.exploration = rec;
251
528
  this.persist(s);
252
529
  }
253
530
  saveArtifact(s, kind, name, data, mime, label) {
531
+ // B5: never trust kind/name as path components. Sanitize, then verify containment.
532
+ kind = safeArtifactName(kind);
533
+ name = safeArtifactName(name);
254
534
  const sub = join(s.dir, artifactDirectoryName(kind));
255
- mkdirSync(sub, { recursive: true });
256
535
  const path = join(sub, name);
536
+ if (!isWithinRoot(path, s.dir) || resolve(path) === resolve(s.dir)) {
537
+ throw new Error(`Refusing to write artifact outside the session directory: ${kind}/${name}`);
538
+ }
539
+ mkdirPrivate(sub);
257
540
  const redact = makeRedactor(s.secrets);
258
- const storedData = typeof data === 'string' && isTextArtifactMime(mime) ? (redact(data) ?? '') : data;
541
+ const isRedactableText = typeof data === 'string' && isTextArtifactMime(mime);
542
+ // JSON/XML artifacts are redacted structurally (string values / attribute values / text
543
+ // nodes only) so a short numeric secret (CVV "123") can never rewrite `{"actions":123}`.
544
+ const storedData = isRedactableText ? redactStructuredText(data, structuredKindOf(mime, name), redact) : data;
259
545
  const storedLabel = label ? redact(label) : label;
260
- writeFileSync(path, storedData);
261
- const uri = `swipium://session/${s.id}/${kind}/${name}`;
262
- s.artifacts.push({ uri, path, mime, kind, createdAt: Date.now(), label: storedLabel });
546
+ const skipped = redact.skippedShortSecrets ?? 0;
547
+ writeFileSync(path, storedData, { mode: PRIVATE_FILE_MODE });
548
+ restrictFile(path); // an overwritten artifact keeps its old mode otherwise
549
+ const uri = `swipium://session/${encodeUriSegment(s.id)}/${encodeUriSegment(kind)}/${encodeUriSegment(name)}`;
550
+ // Binary artifacts (screenshots/recordings) cannot be redacted, so tag them explicitly so
551
+ // consumers (qa_get_artifact, reports) know pixels may show sensitive on-screen content.
552
+ s.artifacts.push({
553
+ uri,
554
+ path,
555
+ mime,
556
+ kind,
557
+ createdAt: Date.now(),
558
+ label: storedLabel,
559
+ redaction: !isRedactableText ? 'not-applied' : skipped > 0 ? 'partial' : 'applied',
560
+ ...(isRedactableText && skipped > 0
561
+ ? {
562
+ redactionNote: `${skipped} secret value(s) shorter than 3 characters were not redacted (matching them would corrupt ordinary text).`,
563
+ }
564
+ : {}),
565
+ });
263
566
  this.persist(s);
264
567
  return uri;
265
568
  }
266
569
  findArtifact(uri) {
570
+ this.ensureRegistryLoaded();
267
571
  for (const s of this.sessions.values()) {
268
572
  const rec = s.artifacts.find((a) => a.uri === uri);
269
573
  if (rec)
270
574
  return { session: s, rec };
271
575
  }
576
+ // Encoding-insensitive fallback: a client (or URL normalisation) may percent-encode a URI
577
+ // that was stored raw (pre-1.6 records), or vice versa.
578
+ const want = canonicalUri(uri);
579
+ for (const s of this.sessions.values()) {
580
+ const rec = s.artifacts.find((a) => canonicalUri(a.uri) === want);
581
+ if (rec)
582
+ return { session: s, rec };
583
+ }
272
584
  return undefined;
273
585
  }
274
- // ---- persistence reload ----
586
+ /** Project roots this server process is actively working on: roots of sessions created here
587
+ * or touched by a tool call (get) since start, NOT every prior session reloaded from the
588
+ * machine-wide registry. Used to scope resources/list to the current project(s). */
589
+ activeRoots() {
590
+ this.ensureRegistryLoaded();
591
+ const roots = new Set();
592
+ for (const id of this.activeIds) {
593
+ const s = this.sessions.get(id);
594
+ if (s)
595
+ roots.add(s.root);
596
+ }
597
+ return [...roots];
598
+ }
599
+ // ---- persistence reload (lazy, triggered by the first get/list/find/create) ----
275
600
  loadRegistry() {
276
601
  try {
277
602
  if (!existsSync(REGISTRY))
@@ -283,6 +608,9 @@ export class SessionStore {
283
608
  continue;
284
609
  try {
285
610
  const st = JSON.parse(readFileSync(sp, 'utf8'));
611
+ // Never clobber a LIVE session created by this process (lazy load can run after create()).
612
+ if (typeof st.id === 'string' && this.sessions.has(st.id))
613
+ continue;
286
614
  const jobs = new Map((st.jobs ?? []).map((j) => [j.jobId, j]));
287
615
  for (const j of jobs.values()) {
288
616
  if (j.status === 'running') {
@@ -290,39 +618,111 @@ export class SessionStore {
290
618
  j.error = 'server restarted while job was running (child process gone)';
291
619
  }
292
620
  }
621
+ // Orphaned-process handling: a prior server persisted its Metro pid. If no live
622
+ // server instance owns that pid, reclaimPid (processRegistry) reaps it ONLY when the
623
+ // registry's recorded fingerprint (start time + full command) still matches. A
624
+ // recycled or unverifiable pid is never signalled, and a process group is signalled
625
+ // only when Swipium spawned the child as a group leader. Either way the reloaded
626
+ // session drops the pid: it is not ours to manage (or stop) anymore.
627
+ if (typeof st.metroPid === 'number' && st.metroPid > 0) {
628
+ if (!pidOwnedByLiveServer(st.metroPid)) {
629
+ const outcome = reclaimPid(st.metroPid, 'metro');
630
+ if (outcome === 'killed')
631
+ log('warn', 'reaped orphaned Metro bundler from a previous server run', { pid: st.metroPid, sessionId: st.id });
632
+ }
633
+ st.metroPid = undefined; // reaped, gone, recycled, or another live server's; never ours
634
+ }
635
+ // Raw secret values never touch disk, so a rehydrated session cannot rebuild
636
+ // its redaction set. If the persisted state shows prior secret-bearing activity
637
+ // (secret inputs, secret generated values, or a performed login), flag the session so
638
+ // qa_report discloses that artifacts written after the restart are no longer scrubbed.
639
+ const priorSecrets = (st.inputs ?? []).some((i) => i.secret) ||
640
+ (st.generatedValues ?? []).some((g) => g.secret) ||
641
+ st.auth?.loginPerformed === true;
293
642
  const s = {
294
- id: st.id, root: st.root, dir: st.dir, createdAt: st.createdAt,
295
- device: st.device, appId: st.appId, headless: st.headless, metroPid: st.metroPid, screenshotCount: st.screenshotCount ?? 0,
296
- network: st.network, envChanges: st.envChanges ?? [], workarounds: st.workarounds ?? [],
643
+ id: st.id,
644
+ root: st.root,
645
+ dir: st.dir,
646
+ createdAt: st.createdAt,
647
+ device: st.device,
648
+ appId: st.appId,
649
+ headless: st.headless,
650
+ metroPid: st.metroPid,
651
+ screenshotCount: st.screenshotCount ?? 0,
652
+ network: st.network,
653
+ envChanges: st.envChanges ?? [],
654
+ workarounds: st.workarounds ?? [],
655
+ chosenTarget: typeof st.chosenTarget === 'string' ? st.chosenTarget : undefined,
656
+ lastTestThisArgs: st.lastTestThisArgs && typeof st.lastTestThisArgs === 'object' && !Array.isArray(st.lastTestThisArgs)
657
+ ? st.lastTestThisArgs
658
+ : undefined,
297
659
  mode: st.mode ?? 'structured',
298
660
  responseMode: st.responseMode ?? DEFAULT_RESPONSE_MODE,
299
661
  sensitive: st.sensitive ?? false,
300
662
  budget: { ...DEFAULT_BUDGET, ...(st.budget ?? {}) },
301
663
  counters: { actions: 0, screenshots: 0, snapshotFailures: 0, noChangeActions: 0, ...(st.counters ?? {}) },
302
- jobs, artifacts: st.artifacts ?? [], findings: st.findings ?? [], notes: st.notes ?? [], mutations: st.mutations ?? [], recordedActions: st.recordedActions ?? [],
303
- fixtures: st.fixtures ?? [], auth: st.auth ?? {}, milestones: st.milestones ?? { session_start: st.createdAt }, budgetProfile: st.budgetProfile,
304
- secrets: new Set(), inputs: st.inputs ?? [], generatedValues: st.generatedValues ?? [], inputValues: new Map(), exploration: st.exploration, aborts: new Map(),
664
+ jobs,
665
+ artifacts: st.artifacts ?? [],
666
+ findings: st.findings ?? [],
667
+ notes: st.notes ?? [],
668
+ mutations: st.mutations ?? [],
669
+ recordedActions: st.recordedActions ?? [],
670
+ toolErrors: st.toolErrors ?? [],
671
+ driverKind: typeof st.driverKind === 'string' ? st.driverKind : undefined,
672
+ wdaUrl: typeof st.wdaUrl === 'string' ? st.wdaUrl : undefined,
673
+ // Live fixture values come from the project's fixtures.json (+ SWIPIUM_* env at use),
674
+ // never from state.json, which only holds value-less metadata.
675
+ fixtures: rehydrateFixtures(st.root, st.fixtures),
676
+ auth: st.auth ?? {},
677
+ milestones: st.milestones ?? { session_start: st.createdAt },
678
+ budgetProfile: st.budgetProfile,
679
+ secrets: new Set(),
680
+ redactionDegraded: priorSecrets || undefined,
681
+ inputs: st.inputs ?? [],
682
+ generatedValues: st.generatedValues ?? [],
683
+ inputValues: new Map(),
684
+ exploration: st.exploration,
685
+ aborts: new Map(),
305
686
  };
306
687
  this.sessions.set(s.id, s);
307
688
  }
308
- catch {
309
- /* skip a corrupt session */
689
+ catch (e) {
690
+ // Skip a corrupt session, but say so, otherwise prior artifacts/reports vanish silently.
691
+ log('warn', 'skipping corrupt prior session state.json on reload', { dir, err: String(e) });
310
692
  }
311
693
  }
312
694
  }
313
- catch {
314
- /* no registry */
695
+ catch (e) {
696
+ log('warn', 'session registry unreadable, prior sessions will not be reloaded', { registry: REGISTRY, err: String(e) });
315
697
  }
316
698
  }
699
+ /** Append this session to the shared ~/.swipium/registry.json. Guarded by an advisory
700
+ * lockfile + atomic rename so two concurrent server instances (common with multiple MCP
701
+ * clients) never clobber each other's read-modify-write. */
317
702
  appendRegistry(id, dir) {
318
703
  try {
319
704
  mkdirSync(REGISTRY_DIR, { recursive: true });
320
- const reg = existsSync(REGISTRY) ? JSON.parse(readFileSync(REGISTRY, 'utf8')) : [];
321
- reg.push({ id, dir });
322
- writeFileSync(REGISTRY, JSON.stringify(reg.slice(-200), null, 2));
705
+ withFileLock(`${REGISTRY}.lock`, () => {
706
+ let reg = [];
707
+ if (existsSync(REGISTRY)) {
708
+ try {
709
+ const parsed = JSON.parse(readFileSync(REGISTRY, 'utf8'));
710
+ if (Array.isArray(parsed))
711
+ reg = parsed;
712
+ }
713
+ catch (e) {
714
+ log('warn', 'registry.json corrupt, rebuilding it (prior sessions may need re-registration)', {
715
+ registry: REGISTRY,
716
+ err: String(e),
717
+ });
718
+ }
719
+ }
720
+ reg.push({ id, dir });
721
+ writeFileAtomicSync(REGISTRY, JSON.stringify(reg.slice(-200), null, 2)); // atomic on the same filesystem
722
+ });
323
723
  }
324
- catch {
325
- /* best-effort */
724
+ catch (e) {
725
+ log('error', 'failed to append session to registry; it will not be reloadable after a restart', { id, dir, err: String(e) });
326
726
  }
327
727
  }
328
728
  }