taphound 0.2.0-dev.1 → 0.2.0-dev.11

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 (447) hide show
  1. package/README.md +49 -149
  2. package/README.zh-CN.md +47 -147
  3. package/assets/skills/taphound-case-suite/SKILL.md +363 -0
  4. package/assets/skills/taphound-case-suite/schemas/base-flow-record.schema.json +22 -0
  5. package/assets/skills/taphound-case-suite/schemas/case-catalog.schema.json +26 -0
  6. package/assets/skills/taphound-case-suite/schemas/case-ledger.schema.json +158 -0
  7. package/assets/skills/taphound-case-suite/schemas/suite-input.schema.json +41 -0
  8. package/assets/skills/taphound-case-suite/schemas/transition.schema.json +61 -0
  9. package/assets/skills/taphound-case-suite/scripts/ledger.mjs +1207 -0
  10. package/assets/skills/taphound-case-suite/templates/base-flow-record.example.json +20 -0
  11. package/assets/skills/taphound-case-suite/templates/suite-input.example.json +27 -0
  12. package/assets/skills/taphound-case-suite/templates/transition.example.json +8 -0
  13. package/assets/skills/taphound-flash/SKILL.md +115 -0
  14. package/assets/skills/taphound-flash/scripts/flash.mjs +585 -0
  15. package/assets/skills/taphound-flash/templates/flash-plan.example.json +15 -0
  16. package/assets/skills/taphound-journey-brief-author/CONTEXT-GUIDE.md +471 -0
  17. package/assets/skills/taphound-journey-brief-author/SKILL.md +494 -0
  18. package/assets/skills/taphound-journey-brief-author/prompts/brief-author-role.md +96 -0
  19. package/assets/skills/taphound-journey-brief-author/prompts/brief-author-role.zh-CN.md +80 -0
  20. package/assets/skills/taphound-journey-brief-author/prompts/context-analyze-project.md +309 -0
  21. package/assets/skills/taphound-journey-brief-author/schemas/context-refresh-result.json +123 -0
  22. package/assets/skills/taphound-journey-brief-author/schemas/project-context-module.json +192 -0
  23. package/assets/skills/taphound-journey-brief-author/schemas/project-context.json +152 -0
  24. package/assets/skills/taphound-journey-brief-author/templates/project-context-module.example.json +58 -0
  25. package/assets/skills/taphound-journey-brief-author/templates/project-context.example.json +51 -0
  26. package/assets/skills/taphound-journey-brief-author/templates/taphound-journey-brief.template.md +63 -0
  27. package/assets/skills/taphound-journey-generator/SKILL.md +500 -0
  28. package/assets/skills/{taphound-ai-journey → taphound-journey-generator}/prompts/check-completion.md +3 -0
  29. package/assets/skills/taphound-journey-generator/prompts/consume-journey-brief.md +52 -0
  30. package/assets/skills/taphound-journey-generator/prompts/generate-step.md +211 -0
  31. package/assets/skills/taphound-journey-generator/prompts/select-flow.md +91 -0
  32. package/assets/skills/taphound-journey-generator/references/bridge.md +35 -0
  33. package/assets/skills/taphound-journey-generator/references/mid-session.md +44 -0
  34. package/assets/skills/taphound-journey-generator/schemas/external-flow.json +37 -0
  35. package/assets/skills/taphound-journey-generator/schemas/flow.json +28 -0
  36. package/assets/skills/taphound-journey-generator/schemas/journey-source.json +25 -0
  37. package/assets/skills/{taphound-ai-journey → taphound-journey-generator}/schemas/observe-output.json +13 -4
  38. package/assets/skills/{taphound-ai-journey → taphound-journey-generator}/schemas/proposed-step-envelope.json +127 -5
  39. package/assets/skills/taphound-journey-generator/scripts/envelope.mjs +561 -0
  40. package/assets/skills/taphound-journey-generator/templates/external-flow.example.json +22 -0
  41. package/assets/skills/taphound-journey-generator/templates/flow.example.json +22 -0
  42. package/assets/skills/taphound-journey-generator/templates/journey-source.example.json +17 -0
  43. package/assets/skills/taphound-journey-generator/templates/taphound-journey-brief.example.md +70 -0
  44. package/assets/skills/taphound-verify-change/SKILL.md +91 -0
  45. package/assets/skills/taphound-verify-change/references/accept.md +38 -0
  46. package/assets/skills/taphound-verify-change/references/preserve.md +349 -0
  47. package/assets/skills/taphound-verify-change/schemas/workflow-manifest.schema.json +295 -0
  48. package/assets/skills/taphound-verify-change/scripts/handoff.mjs +482 -0
  49. package/assets/skills/taphound-verify-change/scripts/ui-refactor.mjs +505 -0
  50. package/dist/adapters/adb/adb-adapter.d.ts +16 -2
  51. package/dist/adapters/adb/adb-adapter.js +199 -0
  52. package/dist/adapters/adb/system-uiautomator-snapshot-provider.d.ts +13 -0
  53. package/dist/adapters/adb/system-uiautomator-snapshot-provider.js +122 -0
  54. package/dist/adapters/adb/ui-automator-parser.d.ts +3 -0
  55. package/dist/adapters/adb/ui-automator-parser.js +160 -0
  56. package/dist/adapters/adb/window-topology-parser.d.ts +2 -0
  57. package/dist/adapters/adb/window-topology-parser.js +101 -0
  58. package/dist/adapters/android-cli/android-cli-adapter.d.ts +16 -8
  59. package/dist/adapters/android-cli/android-cli-adapter.js +137 -15
  60. package/dist/adapters/android-cli/android-cli-snapshot-provider.d.ts +9 -0
  61. package/dist/adapters/android-cli/android-cli-snapshot-provider.js +99 -0
  62. package/dist/adapters/android-cli/layout-parser.js +12 -8
  63. package/dist/adapters/appium/appium-doctor.d.ts +7 -0
  64. package/dist/adapters/appium/appium-doctor.js +128 -0
  65. package/dist/adapters/appium/appium-ui-snapshot-provider.d.ts +34 -0
  66. package/dist/adapters/appium/appium-ui-snapshot-provider.js +240 -0
  67. package/dist/adapters/camera/camera-probe-adapter.d.ts +18 -0
  68. package/dist/adapters/camera/camera-probe-adapter.js +286 -0
  69. package/dist/adapters/filesystem/artifact-store.js +40 -1
  70. package/dist/adapters/filesystem/context-document-writer.d.ts +15 -0
  71. package/dist/adapters/filesystem/context-document-writer.js +112 -0
  72. package/dist/adapters/filesystem/external-flow-registry.d.ts +17 -0
  73. package/dist/adapters/filesystem/external-flow-registry.js +325 -0
  74. package/dist/adapters/filesystem/generation-session-store.d.ts +17 -11
  75. package/dist/adapters/filesystem/generation-session-store.js +281 -855
  76. package/dist/adapters/filesystem/generation-store/session-lock.d.ts +29 -0
  77. package/dist/adapters/filesystem/generation-store/session-lock.js +202 -0
  78. package/dist/adapters/filesystem/generation-store/session-transitions.d.ts +24 -0
  79. package/dist/adapters/filesystem/generation-store/session-transitions.js +340 -0
  80. package/dist/adapters/filesystem/generation-store/store-files.d.ts +50 -0
  81. package/dist/adapters/filesystem/generation-store/store-files.js +363 -0
  82. package/dist/adapters/filesystem/journey-composition-store.d.ts +23 -0
  83. package/dist/adapters/filesystem/journey-composition-store.js +151 -0
  84. package/dist/adapters/filesystem/knowledge-registry.d.ts +12 -0
  85. package/dist/adapters/filesystem/knowledge-registry.js +223 -0
  86. package/dist/adapters/filesystem/project-bound-file.js +3 -5
  87. package/dist/adapters/filesystem/project-file-inspector.d.ts +2 -0
  88. package/dist/adapters/filesystem/project-file-inspector.js +57 -28
  89. package/dist/adapters/filesystem/project-identity-inspector.d.ts +7 -0
  90. package/dist/adapters/filesystem/project-identity-inspector.js +215 -0
  91. package/dist/adapters/filesystem/project-inventory-inspector.d.ts +8 -0
  92. package/dist/adapters/filesystem/project-inventory-inspector.js +93 -0
  93. package/dist/adapters/filesystem/project-module-discoverer.d.ts +6 -0
  94. package/dist/adapters/filesystem/project-module-discoverer.js +158 -0
  95. package/dist/adapters/filesystem/skill-installer.d.ts +7 -2
  96. package/dist/adapters/filesystem/skill-installer.js +40 -11
  97. package/dist/adapters/filesystem/workspace-layout.d.ts +7 -0
  98. package/dist/adapters/filesystem/workspace-layout.js +57 -0
  99. package/dist/adapters/git/node-git-diff.d.ts +17 -0
  100. package/dist/adapters/git/node-git-diff.js +114 -0
  101. package/dist/adapters/process/node-detached-process-launcher.d.ts +6 -0
  102. package/dist/adapters/process/node-detached-process-launcher.js +96 -0
  103. package/dist/adapters/process/node-process-runner.js +11 -2
  104. package/dist/adapters/prompt/inquirer-align-prompt.d.ts +27 -0
  105. package/dist/adapters/prompt/inquirer-align-prompt.js +65 -0
  106. package/dist/adapters/prompt/inquirer-generation-prompt.js +3 -0
  107. package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +9 -1
  108. package/dist/adapters/prompt/inquirer-recorder-prompt.js +63 -0
  109. package/dist/adapters/runtime/adb-runtime-backend.d.ts +67 -0
  110. package/dist/adapters/runtime/adb-runtime-backend.js +206 -0
  111. package/dist/adapters/runtime/fake-runtime-backend.d.ts +87 -0
  112. package/dist/adapters/runtime/fake-runtime-backend.js +240 -0
  113. package/dist/adapters/runtime/mobile-mcp/mcp-tool-client.d.ts +45 -0
  114. package/dist/adapters/runtime/mobile-mcp/mcp-tool-client.js +153 -0
  115. package/dist/adapters/runtime/mobile-mcp/mobile-mcp-errors.d.ts +23 -0
  116. package/dist/adapters/runtime/mobile-mcp/mobile-mcp-errors.js +39 -0
  117. package/dist/adapters/runtime/mobile-mcp/mobile-mcp-responses.d.ts +20 -0
  118. package/dist/adapters/runtime/mobile-mcp/mobile-mcp-responses.js +180 -0
  119. package/dist/adapters/runtime/mobile-mcp/mobile-mcp-runtime-backend.d.ts +78 -0
  120. package/dist/adapters/runtime/mobile-mcp/mobile-mcp-runtime-backend.js +379 -0
  121. package/dist/adapters/runtime/mobile-mcp/mobile-mcp-tools.d.ts +46 -0
  122. package/dist/adapters/runtime/session-adb-view.d.ts +53 -0
  123. package/dist/adapters/runtime/session-adb-view.js +193 -0
  124. package/dist/adapters/runtime/session-backed-ports.d.ts +21 -0
  125. package/dist/adapters/runtime/session-backed-ports.js +43 -0
  126. package/dist/adapters/runtime/shared-session-runtime-backend.d.ts +12 -0
  127. package/dist/adapters/runtime/shared-session-runtime-backend.js +64 -0
  128. package/dist/adapters/ui/auto-ui-snapshot-provider.d.ts +8 -0
  129. package/dist/adapters/ui/auto-ui-snapshot-provider.js +48 -0
  130. package/dist/adapters/ui/device-layout-temp.d.ts +6 -0
  131. package/dist/adapters/ui/device-layout-temp.js +18 -0
  132. package/dist/adapters/ui/device-ui-environment.d.ts +13 -0
  133. package/dist/adapters/ui/device-ui-environment.js +81 -0
  134. package/dist/adapters/ui/layout-normalization.d.ts +9 -0
  135. package/dist/adapters/ui/layout-normalization.js +44 -0
  136. package/dist/adapters/ui/ui-snapshot-error.d.ts +11 -0
  137. package/dist/adapters/ui/ui-snapshot-error.js +12 -0
  138. package/dist/adapters/ui/ui-snapshot-support.d.ts +15 -0
  139. package/dist/adapters/ui/ui-snapshot-support.js +12 -0
  140. package/dist/application/align/align-service.d.ts +42 -0
  141. package/dist/application/align/align-service.js +106 -0
  142. package/dist/application/assertion/expectation-evaluator.d.ts +18 -5
  143. package/dist/application/assertion/expectation-evaluator.js +156 -18
  144. package/dist/application/assertion/guarded-expectation.d.ts +45 -0
  145. package/dist/application/assertion/guarded-expectation.js +74 -0
  146. package/dist/application/checkpoint/baseline-capturer.d.ts +26 -0
  147. package/dist/application/checkpoint/baseline-capturer.js +163 -0
  148. package/dist/application/checkpoint/baseline-error.d.ts +5 -0
  149. package/dist/application/checkpoint/baseline-error.js +8 -0
  150. package/dist/application/checkpoint/baseline-service.d.ts +31 -0
  151. package/dist/application/checkpoint/baseline-service.js +147 -0
  152. package/dist/application/checkpoint/checkpoint-evaluator.d.ts +35 -0
  153. package/dist/application/checkpoint/checkpoint-evaluator.js +415 -0
  154. package/dist/application/checkpoint/regression-comparator.d.ts +18 -0
  155. package/dist/application/checkpoint/regression-comparator.js +192 -0
  156. package/dist/application/collector/logcat-collector.d.ts +22 -1
  157. package/dist/application/collector/logcat-collector.js +103 -15
  158. package/dist/application/collector/logcat-event.d.ts +8 -0
  159. package/dist/application/collector/logcat-event.js +79 -0
  160. package/dist/application/collector/logcat-stop.d.ts +2 -0
  161. package/dist/application/collector/logcat-stop.js +11 -0
  162. package/dist/application/context/context-generator.d.ts +45 -0
  163. package/dist/application/context/context-generator.js +228 -0
  164. package/dist/application/context/context-loader.d.ts +40 -0
  165. package/dist/application/context/context-loader.js +223 -0
  166. package/dist/application/context/context-refresher.d.ts +59 -0
  167. package/dist/application/context/context-refresher.js +349 -0
  168. package/dist/application/context/context-rehasher.d.ts +36 -0
  169. package/dist/application/context/context-rehasher.js +110 -0
  170. package/dist/application/context/context-validator.d.ts +23 -1
  171. package/dist/application/context/context-validator.js +116 -9
  172. package/dist/application/context/evidence-hash.d.ts +1 -0
  173. package/dist/application/context/evidence-hash.js +125 -0
  174. package/dist/application/context/evidence-match.d.ts +9 -0
  175. package/dist/application/context/evidence-match.js +8 -0
  176. package/dist/application/context/shard-identity.d.ts +2 -0
  177. package/dist/application/context/shard-identity.js +7 -0
  178. package/dist/application/contract/contract-loader.d.ts +25 -0
  179. package/dist/application/contract/contract-loader.js +68 -0
  180. package/dist/application/contract/contract-review.d.ts +25 -0
  181. package/dist/application/contract/contract-review.js +51 -0
  182. package/dist/application/contract/contract-verifier.d.ts +53 -0
  183. package/dist/application/contract/contract-verifier.js +645 -0
  184. package/dist/application/devices/resolve-device-assignments.d.ts +21 -0
  185. package/dist/application/devices/resolve-device-assignments.js +60 -0
  186. package/dist/application/diagnosis/failure-classifier.d.ts +23 -0
  187. package/dist/application/diagnosis/failure-classifier.js +179 -0
  188. package/dist/application/doctor/doctor-service.d.ts +36 -3
  189. package/dist/application/doctor/doctor-service.js +255 -25
  190. package/dist/application/generation/generation-app-preparer.d.ts +20 -0
  191. package/dist/application/generation/generation-app-preparer.js +28 -0
  192. package/dist/application/generation/generation-config-service.d.ts +21 -0
  193. package/dist/application/generation/generation-config-service.js +53 -0
  194. package/dist/application/generation/generation-confirmation-service.d.ts +14 -3
  195. package/dist/application/generation/generation-confirmation-service.js +109 -24
  196. package/dist/application/generation/generation-context-snapshot.d.ts +6 -0
  197. package/dist/application/generation/generation-context-snapshot.js +29 -0
  198. package/dist/application/generation/generation-finalizer.d.ts +15 -9
  199. package/dist/application/generation/generation-finalizer.js +167 -70
  200. package/dist/application/generation/generation-publisher.js +12 -8
  201. package/dist/application/generation/generation-recovery-service.d.ts +35 -0
  202. package/dist/application/generation/generation-recovery-service.js +144 -0
  203. package/dist/application/generation/generation-reopen-service.d.ts +15 -0
  204. package/dist/application/generation/generation-reopen-service.js +37 -0
  205. package/dist/application/generation/generation-replace-service.d.ts +32 -0
  206. package/dist/application/generation/generation-replace-service.js +116 -0
  207. package/dist/application/generation/generation-starter.d.ts +44 -4
  208. package/dist/application/generation/generation-starter.js +175 -8
  209. package/dist/application/generation/generation-step-executor.d.ts +67 -5
  210. package/dist/application/generation/generation-step-executor.js +471 -108
  211. package/dist/application/generation/proposed-step-validator.js +96 -30
  212. package/dist/application/generation/replay-policy-loader.d.ts +16 -0
  213. package/dist/application/generation/replay-policy-loader.js +29 -0
  214. package/dist/application/generation/risk-evaluator.d.ts +4 -1
  215. package/dist/application/generation/risk-evaluator.js +118 -3
  216. package/dist/application/generation/runtime-observer.d.ts +39 -7
  217. package/dist/application/generation/runtime-observer.js +193 -24
  218. package/dist/application/impact/impact-resolver.d.ts +32 -0
  219. package/dist/application/impact/impact-resolver.js +160 -0
  220. package/dist/application/init/init-service.js +26 -13
  221. package/dist/application/interaction/action-executor.d.ts +3 -2
  222. package/dist/application/interaction/action-executor.js +11 -1
  223. package/dist/application/interaction/action-target.d.ts +17 -0
  224. package/dist/application/interaction/action-target.js +40 -0
  225. package/dist/application/interaction/external-step-runner.d.ts +153 -0
  226. package/dist/application/interaction/external-step-runner.js +332 -0
  227. package/dist/application/interaction/fallback-resolver.d.ts +6 -3
  228. package/dist/application/interaction/fallback-resolver.js +7 -5
  229. package/dist/application/interaction/scroll-to-executor.d.ts +17 -3
  230. package/dist/application/interaction/scroll-to-executor.js +118 -36
  231. package/dist/application/journey/external-flow-resolver.d.ts +25 -0
  232. package/dist/application/journey/external-flow-resolver.js +22 -0
  233. package/dist/application/journey/journey-check-service.d.ts +54 -0
  234. package/dist/application/journey/journey-check-service.js +207 -0
  235. package/dist/application/journey/journey-promoter.d.ts +26 -0
  236. package/dist/application/journey/journey-promoter.js +123 -0
  237. package/dist/application/journey/journey-resolver.d.ts +42 -0
  238. package/dist/application/journey/journey-resolver.js +239 -0
  239. package/dist/application/journey/journey-retirer.d.ts +25 -0
  240. package/dist/application/journey/journey-retirer.js +87 -0
  241. package/dist/application/knowledge/anchor-resolver.d.ts +36 -0
  242. package/dist/application/knowledge/anchor-resolver.js +110 -0
  243. package/dist/application/knowledge/knowledge-loader.d.ts +16 -0
  244. package/dist/application/knowledge/knowledge-loader.js +36 -0
  245. package/dist/application/locator/layout-failure-summary.d.ts +2 -0
  246. package/dist/application/locator/layout-failure-summary.js +67 -0
  247. package/dist/application/locator/layout-traversal.d.ts +11 -0
  248. package/dist/application/locator/layout-traversal.js +18 -0
  249. package/dist/application/locator/locator-resolver.d.ts +13 -1
  250. package/dist/application/locator/locator-resolver.js +182 -18
  251. package/dist/application/observe/observe-service.d.ts +20 -0
  252. package/dist/application/observe/observe-service.js +91 -0
  253. package/dist/application/project/project-describer.d.ts +14 -6
  254. package/dist/application/project/project-describer.js +38 -5
  255. package/dist/application/recognition/screen-detector.d.ts +28 -0
  256. package/dist/application/recognition/screen-detector.js +111 -0
  257. package/dist/application/recorder/locator-selector.js +151 -17
  258. package/dist/application/recorder/recorder-service.d.ts +16 -4
  259. package/dist/application/recorder/recorder-service.js +420 -128
  260. package/dist/application/report/report-writer.js +1 -1
  261. package/dist/application/runtime/cold-launch.d.ts +28 -0
  262. package/dist/application/runtime/cold-launch.js +59 -0
  263. package/dist/application/runtime/step-runner.d.ts +49 -4
  264. package/dist/application/runtime/step-runner.js +721 -130
  265. package/dist/application/runtime/verify-runtime.d.ts +57 -7
  266. package/dist/application/runtime/verify-runtime.js +554 -174
  267. package/dist/application/ui/cached-ui-snapshot-provider.d.ts +45 -0
  268. package/dist/application/ui/cached-ui-snapshot-provider.js +111 -0
  269. package/dist/application/ui/ui-snapshot-lifecycle.d.ts +2 -0
  270. package/dist/application/ui/ui-snapshot-lifecycle.js +8 -0
  271. package/dist/application/ui/ui-stability-probe.d.ts +3 -0
  272. package/dist/application/ui/ui-stability-probe.js +8 -0
  273. package/dist/application/wait/idle-advice.d.ts +6 -0
  274. package/dist/application/wait/idle-advice.js +27 -0
  275. package/dist/application/wait/idle-profiles.d.ts +15 -0
  276. package/dist/application/wait/idle-profiles.js +75 -0
  277. package/dist/application/wait/idle-waiter.d.ts +28 -4
  278. package/dist/application/wait/idle-waiter.js +186 -15
  279. package/dist/cli/commands/align.d.ts +3 -0
  280. package/dist/cli/commands/align.js +124 -0
  281. package/dist/cli/commands/baseline.d.ts +3 -0
  282. package/dist/cli/commands/baseline.js +130 -0
  283. package/dist/cli/commands/context.js +300 -27
  284. package/dist/cli/commands/contract.d.ts +3 -0
  285. package/dist/cli/commands/contract.js +120 -0
  286. package/dist/cli/commands/doctor.js +16 -9
  287. package/dist/cli/commands/failure.d.ts +3 -0
  288. package/dist/cli/commands/failure.js +63 -0
  289. package/dist/cli/commands/generation/lifecycle-commands.d.ts +5 -0
  290. package/dist/cli/commands/generation/lifecycle-commands.js +211 -0
  291. package/dist/cli/commands/generation/session-commands.d.ts +11 -0
  292. package/dist/cli/commands/generation/session-commands.js +472 -0
  293. package/dist/cli/commands/generation/shared.d.ts +1414 -0
  294. package/dist/cli/commands/generation/shared.js +291 -0
  295. package/dist/cli/commands/generation/step-commands.d.ts +8 -0
  296. package/dist/cli/commands/generation/step-commands.js +315 -0
  297. package/dist/cli/commands/generation.js +10 -507
  298. package/dist/cli/commands/impact.d.ts +3 -0
  299. package/dist/cli/commands/impact.js +87 -0
  300. package/dist/cli/commands/init.js +13 -6
  301. package/dist/cli/commands/journey.d.ts +3 -0
  302. package/dist/cli/commands/journey.js +382 -0
  303. package/dist/cli/commands/knowledge.d.ts +3 -0
  304. package/dist/cli/commands/knowledge.js +93 -0
  305. package/dist/cli/commands/observe.d.ts +3 -0
  306. package/dist/cli/commands/observe.js +127 -0
  307. package/dist/cli/commands/project.js +2 -1
  308. package/dist/cli/commands/record.js +18 -5
  309. package/dist/cli/commands/verify.js +273 -63
  310. package/dist/cli/dependencies.d.ts +125 -1
  311. package/dist/cli/dependencies.js +450 -51
  312. package/dist/cli/diff-verification.d.ts +41 -0
  313. package/dist/cli/diff-verification.js +192 -0
  314. package/dist/cli/main.js +49 -1
  315. package/dist/cli/output.d.ts +5 -3
  316. package/dist/cli/output.js +12 -0
  317. package/dist/cli/program.js +19 -1
  318. package/dist/cli/project-root.d.ts +1 -0
  319. package/dist/cli/project-root.js +6 -0
  320. package/dist/cli/runtime-selection.d.ts +22 -0
  321. package/dist/cli/runtime-selection.js +92 -0
  322. package/dist/cli/version.d.ts +1 -0
  323. package/dist/cli/version.js +14 -0
  324. package/dist/cli/workspace-guard.d.ts +2 -0
  325. package/dist/cli/workspace-guard.js +3 -0
  326. package/dist/domain/binding-reference.d.ts +4 -0
  327. package/dist/domain/binding-reference.js +23 -0
  328. package/dist/domain/checkpoint.d.ts +404 -0
  329. package/dist/domain/checkpoint.js +297 -0
  330. package/dist/domain/config.d.ts +107 -1
  331. package/dist/domain/config.js +52 -6
  332. package/dist/domain/contract.d.ts +413 -0
  333. package/dist/domain/contract.js +196 -0
  334. package/dist/domain/external-flow.d.ts +493 -0
  335. package/dist/domain/external-flow.js +15 -0
  336. package/dist/domain/failure-classification.d.ts +176 -0
  337. package/dist/domain/failure-classification.js +169 -0
  338. package/dist/domain/failure.d.ts +2 -1
  339. package/dist/domain/failure.js +123 -0
  340. package/dist/domain/generation.d.ts +1671 -132
  341. package/dist/domain/generation.js +202 -13
  342. package/dist/domain/geometry.d.ts +13 -0
  343. package/dist/domain/geometry.js +16 -0
  344. package/dist/domain/impact.d.ts +71 -0
  345. package/dist/domain/impact.js +76 -0
  346. package/dist/domain/init.d.ts +12 -4
  347. package/dist/domain/init.js +24 -18
  348. package/dist/domain/journey-composition.d.ts +2285 -0
  349. package/dist/domain/journey-composition.js +85 -0
  350. package/dist/domain/journey-lifecycle.d.ts +10 -0
  351. package/dist/domain/journey-lifecycle.js +9 -0
  352. package/dist/domain/journey.d.ts +2419 -212
  353. package/dist/domain/journey.js +384 -14
  354. package/dist/domain/knowledge.d.ts +165 -0
  355. package/dist/domain/knowledge.js +188 -0
  356. package/dist/domain/layout.d.ts +34 -5
  357. package/dist/domain/layout.js +64 -6
  358. package/dist/domain/locator-evidence.d.ts +3 -0
  359. package/dist/domain/locator-evidence.js +36 -0
  360. package/dist/domain/logcat-event.d.ts +54 -0
  361. package/dist/domain/logcat-event.js +40 -0
  362. package/dist/domain/observation.d.ts +33 -0
  363. package/dist/domain/observation.js +18 -0
  364. package/dist/domain/project-context.d.ts +219 -1
  365. package/dist/domain/project-context.js +209 -6
  366. package/dist/domain/proposed-step.d.ts +340 -60
  367. package/dist/domain/proposed-step.js +23 -3
  368. package/dist/domain/report.d.ts +1455 -28
  369. package/dist/domain/report.js +211 -14
  370. package/dist/domain/runtime-snapshot.d.ts +58 -1
  371. package/dist/domain/runtime-snapshot.js +14 -3
  372. package/dist/domain/runtime.d.ts +50 -0
  373. package/dist/domain/runtime.js +36 -0
  374. package/dist/domain/system-app-profiles.d.ts +6 -0
  375. package/dist/domain/system-app-profiles.js +43 -0
  376. package/dist/domain/ui-backend.d.ts +28 -0
  377. package/dist/domain/ui-backend.js +33 -0
  378. package/dist/domain/ui-cache.d.ts +11 -0
  379. package/dist/domain/ui-cache.js +10 -0
  380. package/dist/domain/window-hierarchy.d.ts +84 -0
  381. package/dist/domain/window-hierarchy.js +172 -0
  382. package/dist/domain/workflow-manifest.d.ts +77 -0
  383. package/dist/domain/workflow-manifest.js +136 -0
  384. package/dist/domain/workspace.d.ts +28 -0
  385. package/dist/domain/workspace.js +72 -0
  386. package/dist/ports/adb.d.ts +34 -1
  387. package/dist/ports/align-prompt.d.ts +11 -0
  388. package/dist/ports/align-prompt.js +6 -0
  389. package/dist/ports/anchor-resolver.d.ts +30 -0
  390. package/dist/ports/anchor-resolver.js +1 -0
  391. package/dist/ports/annotated-screen-resolver.d.ts +4 -0
  392. package/dist/ports/annotated-screen-resolver.js +1 -0
  393. package/dist/ports/camera-probe.d.ts +23 -0
  394. package/dist/ports/camera-probe.js +8 -0
  395. package/dist/ports/context-document-writer.d.ts +24 -0
  396. package/dist/ports/context-document-writer.js +1 -0
  397. package/dist/ports/detached-process-launcher.d.ts +12 -0
  398. package/dist/ports/detached-process-launcher.js +1 -0
  399. package/dist/ports/external-flow-registry.d.ts +37 -0
  400. package/dist/ports/external-flow-registry.js +1 -0
  401. package/dist/ports/generation-prompt.d.ts +2 -1
  402. package/dist/ports/generation-session-store.d.ts +12 -2
  403. package/dist/ports/git-diff.d.ts +9 -0
  404. package/dist/ports/git-diff.js +1 -0
  405. package/dist/ports/journey-composition-store.d.ts +17 -0
  406. package/dist/ports/journey-composition-store.js +1 -0
  407. package/dist/ports/knowledge-registry.d.ts +20 -0
  408. package/dist/ports/knowledge-registry.js +1 -0
  409. package/dist/ports/process-runner.d.ts +7 -0
  410. package/dist/ports/project-file-inspector.d.ts +1 -0
  411. package/dist/ports/project-identity-inspector.d.ts +24 -0
  412. package/dist/ports/project-identity-inspector.js +1 -0
  413. package/dist/ports/project-inventory-inspector.d.ts +21 -0
  414. package/dist/ports/project-inventory-inspector.js +1 -0
  415. package/dist/ports/project-module-discoverer.d.ts +26 -0
  416. package/dist/ports/project-module-discoverer.js +1 -0
  417. package/dist/ports/recorder-prompt.d.ts +10 -1
  418. package/dist/ports/runtime-backend.d.ts +89 -0
  419. package/dist/ports/runtime-backend.js +13 -0
  420. package/dist/ports/runtime-capability.d.ts +12 -0
  421. package/dist/ports/runtime-capability.js +20 -0
  422. package/dist/ports/runtime-session-ports.d.ts +25 -0
  423. package/dist/ports/runtime-session-ports.js +1 -0
  424. package/dist/ports/screenshot.d.ts +11 -0
  425. package/dist/ports/screenshot.js +1 -0
  426. package/dist/ports/skill-installer.d.ts +2 -2
  427. package/dist/ports/ui-snapshot.d.ts +35 -0
  428. package/dist/ports/ui-snapshot.js +1 -0
  429. package/dist/ports/ui-stability.d.ts +19 -0
  430. package/dist/ports/ui-stability.js +1 -0
  431. package/dist/ports/workspace-layout.d.ts +4 -0
  432. package/dist/ports/workspace-layout.js +1 -0
  433. package/dist/shared/errors.d.ts +2 -0
  434. package/dist/shared/errors.js +22 -0
  435. package/dist/shared/paths.d.ts +2 -0
  436. package/dist/shared/paths.js +24 -0
  437. package/dist/shared/strings.d.ts +2 -0
  438. package/dist/shared/strings.js +16 -0
  439. package/package.json +6 -1
  440. package/assets/skills/taphound-ai-journey/GUIDE.md +0 -862
  441. package/assets/skills/taphound-ai-journey/SKILL.md +0 -377
  442. package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +0 -526
  443. package/assets/skills/taphound-ai-journey/prompts/generate-step.md +0 -109
  444. package/assets/skills/taphound-ai-journey/schemas/project-context.json +0 -81
  445. package/assets/skills/taphound-ai-journey/templates/project-context.example.json +0 -49
  446. package/dist/ports/android-cli.d.ts +0 -21
  447. /package/dist/{ports/android-cli.js → adapters/runtime/mobile-mcp/mobile-mcp-tools.js} +0 -0
@@ -1,12 +1,22 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { constants, link, lstat, mkdir, open, readdir, realpath, rename, rm, unlink } from "node:fs/promises";
3
- import { isAbsolute, join, posix, relative, resolve } from "node:path";
4
- import { GenerationInFlightSchema, GenerationSessionSchema, PendingConfirmationSchema, generationCoreIdentity } from "../../domain/generation.js";
2
+ import { constants, link, lstat, mkdir, open, readdir, rename, rm, unlink } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { GenerationSessionSchema, PendingConfirmationSchema, isGenerationConfirmationExpired } from "../../domain/generation.js";
5
+ import { BUILD_DIR, GENERATIONS_DIR, TAPHOUND_DIR, activeGenerationBundleName } from "../../domain/workspace.js";
5
6
  import { GenerationSessionStoreError } from "../../ports/generation-session-store.js";
7
+ import { isErrnoException } from "../../shared/errors.js";
8
+ import { compareStrings } from "../../shared/strings.js";
9
+ import { ensureBuildIgnored } from "./workspace-layout.js";
10
+ import { SessionLock } from "./generation-store/session-lock.js";
11
+ import { assertArchiveTransition, assertBundlePublishableTransition, assertConfirmationTransition, assertCoreIdentityPreserved, assertId, assertLatestSnapshotPreserved, assertOrdinaryTransition, assertRecoveryTransition, assertSnapshotTransition, assertVerificationCompletionTransition, assertVerificationRecoveryTransition, assertVerificationReopenTransition, parseInFlight, parseSession, parseVerificationPhase, sameInFlight, sessionId, transitionStableState, validateExpectedRevision, validateInitialRevision, validateNextRevision } from "./generation-store/session-transitions.js";
12
+ import { asStoreIoError, assertContained, assertEvidenceNamespaceAvailable, canonicalizeEvidence, captureDirectoryEvidence, captureStoreDirectory, createOrRequireDirectory, evidenceEntrySnapshot, evidenceNotFound, fileIdentity, pathExists, readBoundState, readStateFromDirectory, requireRealDirectory, sameIdentity, sameSnapshotMetadata, serializeJson, snapshotMetadata, syncDirectory, validateEvidencePath, verifyDirectoryEvidence, verifyStoreDirectory, writeStateAtomically } from "./generation-store/store-files.js";
6
13
  const DEFAULT_OPTIONS = {
7
14
  lockTimeoutMs: 2_000,
8
15
  lockRetryMs: 10
9
16
  };
17
+ function activeBundleName(id) {
18
+ return activeGenerationBundleName(id);
19
+ }
10
20
  const HOOK_NAMES = [
11
21
  "beforeLockStagingWrite",
12
22
  "beforeLockInstall",
@@ -44,6 +54,11 @@ function parseStoreConfiguration(projectRoot, options) {
44
54
  if (nowInput !== undefined && typeof nowInput !== "function") {
45
55
  throw new TypeError("now must be a function");
46
56
  }
57
+ const generationRootInput = Reflect.get(options, "generationRoot");
58
+ if (generationRootInput !== undefined
59
+ && typeof generationRootInput !== "string") {
60
+ throw new TypeError("generationRoot must be a string");
61
+ }
47
62
  const hooksInput = Reflect.get(options, "hooks");
48
63
  const hooks = {};
49
64
  if (hooksInput !== undefined) {
@@ -64,7 +79,10 @@ function parseStoreConfiguration(projectRoot, options) {
64
79
  projectRoot: resolve(projectRoot),
65
80
  options: {
66
81
  lockTimeoutMs: lockTimeoutMs,
67
- lockRetryMs: lockRetryMs
82
+ lockRetryMs: lockRetryMs,
83
+ ...(generationRootInput === undefined
84
+ ? {}
85
+ : { generationRoot: generationRootInput })
68
86
  },
69
87
  now: (nowInput ?? (() => new Date())),
70
88
  hooks
@@ -74,610 +92,6 @@ function parseStoreConfiguration(projectRoot, options) {
74
92
  throw new GenerationSessionStoreError("IO_ERROR", "Generation session store configuration is invalid", { cause: error });
75
93
  }
76
94
  }
77
- function snapshotMetadata(stats) {
78
- return {
79
- dev: stats.dev,
80
- ino: stats.ino,
81
- size: stats.size,
82
- mtimeNs: stats.mtimeNs,
83
- ctimeNs: stats.ctimeNs
84
- };
85
- }
86
- function sameSnapshotMetadata(left, right) {
87
- return (sameIdentity(left, right)
88
- && left.size === right.size
89
- && left.mtimeNs === right.mtimeNs
90
- && left.ctimeNs === right.ctimeNs);
91
- }
92
- function evidenceEntryType(entry) {
93
- if (entry.isFile())
94
- return "file";
95
- if (entry.isDirectory())
96
- return "directory";
97
- if (entry.isSymbolicLink())
98
- return "symlink";
99
- return "other";
100
- }
101
- function evidenceEntrySnapshot(entries) {
102
- return entries
103
- .map((entry) => `${entry.name}\0${evidenceEntryType(entry)}`)
104
- .sort();
105
- }
106
- function sameIdentity(left, right) {
107
- return left.dev === right.dev && left.ino === right.ino;
108
- }
109
- async function captureStoreDirectory(path) {
110
- const stats = await lstat(path, { bigint: true });
111
- if (!stats.isDirectory() || stats.isSymbolicLink()) {
112
- throw new GenerationSessionStoreError("IO_ERROR", `Generation store path is not a real directory: ${path}`);
113
- }
114
- return {
115
- path,
116
- canonicalPath: await realpath(path),
117
- identity: { dev: stats.dev, ino: stats.ino }
118
- };
119
- }
120
- async function verifyStoreDirectory(expected) {
121
- const current = await captureStoreDirectory(expected.path);
122
- if (current.canonicalPath !== expected.canonicalPath
123
- || !sameIdentity(current.identity, expected.identity)) {
124
- throw new GenerationSessionStoreError("IO_ERROR", `Generation store directory identity changed: ${expected.path}`);
125
- }
126
- }
127
- async function captureStoreFile(path) {
128
- const stats = await lstat(path, { bigint: true });
129
- if (!stats.isFile() || stats.isSymbolicLink()) {
130
- throw new GenerationSessionStoreError("IO_ERROR", `Generation store path is not a real file: ${path}`);
131
- }
132
- return { dev: stats.dev, ino: stats.ino };
133
- }
134
- async function captureOptionalStoreFile(path) {
135
- try {
136
- return await captureStoreFile(path);
137
- }
138
- catch (error) {
139
- if (isNodeError(error) && error.code === "ENOENT") {
140
- return null;
141
- }
142
- throw error;
143
- }
144
- }
145
- async function verifyOptionalStoreFile(path, expected) {
146
- const current = await captureOptionalStoreFile(path);
147
- if ((current === null) !== (expected === null)
148
- || (current !== null
149
- && expected !== null
150
- && !sameIdentity(current, expected))) {
151
- throw new GenerationSessionStoreError("IO_ERROR", `Generation state file identity changed: ${path}`);
152
- }
153
- }
154
- function isNodeError(error) {
155
- return error instanceof Error && "code" in error;
156
- }
157
- function asStoreIoError(error, action) {
158
- if (error instanceof GenerationSessionStoreError) {
159
- return error;
160
- }
161
- return new GenerationSessionStoreError("IO_ERROR", `Generation session store failed to ${action}`, { cause: error });
162
- }
163
- function assertId(id) {
164
- if (typeof id !== "string"
165
- || !/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(id)) {
166
- throw new GenerationSessionStoreError("INVALID_ID", "Invalid generation session id");
167
- }
168
- }
169
- function parseSession(value, classifyInvalidId = false) {
170
- try {
171
- return GenerationSessionSchema.parse(value);
172
- }
173
- catch (error) {
174
- if (classifyInvalidId && value !== null && typeof value === "object") {
175
- try {
176
- const id = Reflect.get(value, "id");
177
- if (typeof id === "string") {
178
- assertId(id);
179
- }
180
- }
181
- catch (idError) {
182
- if (idError instanceof GenerationSessionStoreError) {
183
- throw idError;
184
- }
185
- }
186
- }
187
- throw new GenerationSessionStoreError("INVALID_SESSION", "Generation session state is invalid", { cause: error });
188
- }
189
- }
190
- function sessionId(value) {
191
- const id = value.id;
192
- assertId(id);
193
- return id;
194
- }
195
- function validateExpectedRevision(revision) {
196
- if (!Number.isSafeInteger(revision) || revision < 0) {
197
- throw new GenerationSessionStoreError("INVALID_REVISION", "Invalid expected generation revision");
198
- }
199
- }
200
- function validateInitialRevision(session) {
201
- if (session.revision !== 0) {
202
- throw new GenerationSessionStoreError("INVALID_REVISION", "A new generation session must start at revision 0");
203
- }
204
- }
205
- function validateNextRevision(id, expectedRevision, next) {
206
- validateExpectedRevision(expectedRevision);
207
- if (expectedRevision === Number.MAX_SAFE_INTEGER) {
208
- throw new GenerationSessionStoreError("INVALID_REVISION", "Generation revision cannot increment beyond Number.MAX_SAFE_INTEGER");
209
- }
210
- if (sessionId(next) !== id) {
211
- throw new GenerationSessionStoreError("INVALID_ID", "Updated generation session id does not match the requested id");
212
- }
213
- if (next.revision !== expectedRevision + 1) {
214
- throw new GenerationSessionStoreError("INVALID_REVISION", "Next generation revision must increment expectedRevision by exactly one");
215
- }
216
- }
217
- function parseInFlight(value) {
218
- try {
219
- return GenerationInFlightSchema.parse(value);
220
- }
221
- catch (error) {
222
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Expected inFlight record is invalid", { cause: error });
223
- }
224
- }
225
- function sameInFlight(left, right) {
226
- return JSON.stringify(left) === JSON.stringify(right);
227
- }
228
- function transitionStableState(session) {
229
- return {
230
- version: session.version,
231
- id: session.id,
232
- bindings: session.bindings,
233
- target: session.target,
234
- variables: session.variables,
235
- candidateSteps: session.candidateSteps,
236
- candidateSources: session.candidateSources,
237
- pendingConfirmation: session.pendingConfirmation,
238
- verification: session.verification,
239
- publication: session.publication
240
- };
241
- }
242
- function assertCoreIdentityPreserved(current, next) {
243
- if (JSON.stringify(generationCoreIdentity(current))
244
- !== JSON.stringify(generationCoreIdentity(next))) {
245
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Generation Core identity is immutable");
246
- }
247
- }
248
- function assertLatestSnapshotPreserved(current, next) {
249
- if (current.bindings.snapshotHash !== next.bindings.snapshotHash) {
250
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Latest snapshot may only change through commitSnapshot");
251
- }
252
- }
253
- function assertSnapshotTransition(current, next) {
254
- assertCoreIdentityPreserved(current, next);
255
- const currentBindings = {
256
- projectHash: current.bindings.projectHash,
257
- configHash: current.bindings.configHash,
258
- contextHash: current.bindings.contextHash
259
- };
260
- const { snapshotHash: nextSnapshotHash, ...nextBindings } = next.bindings;
261
- if (current.state !== "active"
262
- || current.inFlight !== null
263
- || current.pendingConfirmation !== null
264
- || current.verification.status !== "notRun"
265
- || current.publication.status !== "notRun"
266
- || next.state !== "active"
267
- || next.inFlight !== null
268
- || next.pendingConfirmation !== null
269
- || nextSnapshotHash === null
270
- || JSON.stringify(currentBindings) !== JSON.stringify(nextBindings)
271
- || JSON.stringify({
272
- ...transitionStableState(current),
273
- bindings: undefined
274
- }) !== JSON.stringify({
275
- ...transitionStableState(next),
276
- bindings: undefined
277
- })) {
278
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Snapshot commit may only replace the active snapshot binding");
279
- }
280
- }
281
- function assertOrdinaryTransition(current, next) {
282
- assertCoreIdentityPreserved(current, next);
283
- assertLatestSnapshotPreserved(current, next);
284
- if (current.verification.status !== "notRun"
285
- || current.publication.status !== "notRun"
286
- || JSON.stringify(current.verification) !== JSON.stringify(next.verification)
287
- || JSON.stringify(current.publication) !== JSON.stringify(next.publication)) {
288
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification and publication require explicit transitions");
289
- }
290
- if (current.state === "recoveryRequired") {
291
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Recovery-required state must use the explicit recovery transition");
292
- }
293
- if (JSON.stringify(current.pendingConfirmation)
294
- !== JSON.stringify(next.pendingConfirmation)) {
295
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Pending confirmation must use the explicit confirmation transition");
296
- }
297
- const stableStateMatches = JSON.stringify(transitionStableState(current))
298
- === JSON.stringify(transitionStableState(next));
299
- if (current.inFlight !== null) {
300
- if (next.state === "recoveryRequired"
301
- && sameInFlight(current.inFlight, next.inFlight)
302
- && stableStateMatches) {
303
- return;
304
- }
305
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Persisted inFlight may only be marked recoveryRequired without mutation");
306
- }
307
- if (next.state === "recoveryRequired"
308
- || (next.inFlight !== null && !stableStateMatches)) {
309
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Starting inFlight cannot mutate candidate or result state");
310
- }
311
- }
312
- function verificationStableState(session) {
313
- return {
314
- ...transitionStableState(session),
315
- verification: undefined
316
- };
317
- }
318
- function assertVerificationCompletionTransition(current, next, status) {
319
- assertCoreIdentityPreserved(current, next);
320
- assertLatestSnapshotPreserved(current, next);
321
- if (current.state !== "active"
322
- || next.state !== "active"
323
- || current.inFlight !== null
324
- || next.inFlight !== null
325
- || current.pendingConfirmation !== null
326
- || next.pendingConfirmation !== null
327
- || current.verification.status !== "running"
328
- || next.verification.status !== status
329
- || next.publication.status !== "notRun"
330
- || JSON.stringify(verificationStableState(current))
331
- !== JSON.stringify(verificationStableState(next))) {
332
- throw new GenerationSessionStoreError("INVALID_TRANSITION", `Verification completion must record ${status} from the running attempt`);
333
- }
334
- if (next.verification.status === "passed"
335
- && next.verification.attemptId !== current.verification.attemptId) {
336
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification completion attempt does not match");
337
- }
338
- }
339
- function assertBundlePublishableTransition(current, next) {
340
- assertCoreIdentityPreserved(current, next);
341
- assertLatestSnapshotPreserved(current, next);
342
- if (current.state !== "active"
343
- || next.state !== "active"
344
- || current.inFlight !== null
345
- || next.inFlight !== null
346
- || current.pendingConfirmation !== null
347
- || next.pendingConfirmation !== null
348
- || current.verification.status !== "passed"
349
- || JSON.stringify(current.verification) !== JSON.stringify(next.verification)
350
- || current.publication.status !== "notRun"
351
- || next.publication.status !== "published"
352
- || JSON.stringify({
353
- ...transitionStableState(current),
354
- publication: undefined
355
- }) !== JSON.stringify({
356
- ...transitionStableState(next),
357
- publication: undefined
358
- })) {
359
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Bundle publication must be explicitly marked after passed verification");
360
- }
361
- }
362
- function confirmationStableState(session) {
363
- const stable = transitionStableState(session);
364
- return { ...stable, pendingConfirmation: undefined };
365
- }
366
- function assertConfirmationTransition(current, next) {
367
- assertCoreIdentityPreserved(current, next);
368
- assertLatestSnapshotPreserved(current, next);
369
- if (current.state !== "active"
370
- || next.state !== "active"
371
- || current.inFlight !== null
372
- || next.inFlight !== null
373
- || current.verification.status !== "notRun"
374
- || current.publication.status !== "notRun"
375
- || JSON.stringify(confirmationStableState(current))
376
- !== JSON.stringify(confirmationStableState(next))) {
377
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Confirmation transition may only change pending confirmation state");
378
- }
379
- const before = current.pendingConfirmation;
380
- const after = next.pendingConfirmation;
381
- if (before === null) {
382
- if (after !== null && after.status === "pending") {
383
- return;
384
- }
385
- }
386
- else if (after === null) {
387
- return;
388
- }
389
- else if (before.status === "pending"
390
- && after.status === "approved"
391
- && JSON.stringify({ ...before, status: undefined })
392
- === JSON.stringify({ ...after, status: undefined })) {
393
- return;
394
- }
395
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Invalid pending confirmation lifecycle transition");
396
- }
397
- function assertRecoveryTransition(current, next) {
398
- assertCoreIdentityPreserved(current, next);
399
- assertLatestSnapshotPreserved(current, next);
400
- if (current.state !== "recoveryRequired"
401
- || current.inFlight === null
402
- || next.state !== "active"
403
- || next.inFlight !== null
404
- || JSON.stringify(transitionStableState(current))
405
- !== JSON.stringify(transitionStableState(next))) {
406
- throw new GenerationSessionStoreError("INVALID_TRANSITION", "Recovery must only clear preserved inFlight evidence and reactivate state");
407
- }
408
- }
409
- async function pathExists(path) {
410
- try {
411
- await lstat(path);
412
- return true;
413
- }
414
- catch (error) {
415
- if (isNodeError(error) && error.code === "ENOENT") {
416
- return false;
417
- }
418
- throw error;
419
- }
420
- }
421
- async function requireRealDirectory(path) {
422
- const stats = await lstat(path);
423
- if (!stats.isDirectory() || stats.isSymbolicLink()) {
424
- throw new GenerationSessionStoreError("IO_ERROR", `Generation store path is not a real directory: ${path}`);
425
- }
426
- }
427
- async function createOrRequireDirectory(path) {
428
- let created = false;
429
- try {
430
- await mkdir(path);
431
- created = true;
432
- }
433
- catch (error) {
434
- if (!isNodeError(error) || error.code !== "EEXIST") {
435
- throw error;
436
- }
437
- }
438
- await requireRealDirectory(path);
439
- return created;
440
- }
441
- async function syncDirectory(path) {
442
- const handle = await open(path, constants.O_RDONLY);
443
- try {
444
- await handle.sync();
445
- }
446
- finally {
447
- await handle.close();
448
- }
449
- }
450
- function serializeJson(value) {
451
- return `${JSON.stringify(value, null, 2)}\n`;
452
- }
453
- function canonicalizeEvidenceValue(value, ancestors = new Set()) {
454
- if (value === null
455
- || typeof value === "string"
456
- || typeof value === "boolean") {
457
- return value;
458
- }
459
- if (typeof value === "number") {
460
- if (Number.isFinite(value)) {
461
- return value;
462
- }
463
- throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence JSON cannot contain non-finite numbers");
464
- }
465
- if (typeof value !== "object") {
466
- throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence value must be JSON serializable");
467
- }
468
- if (ancestors.has(value)) {
469
- throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence JSON cannot contain cycles");
470
- }
471
- ancestors.add(value);
472
- try {
473
- if (Array.isArray(value)) {
474
- return value.map((item) => canonicalizeEvidenceValue(item, ancestors));
475
- }
476
- const prototype = Object.getPrototypeOf(value);
477
- if (prototype !== Object.prototype && prototype !== null) {
478
- throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence value must contain only JSON objects");
479
- }
480
- return Object.fromEntries(Object.keys(value)
481
- .sort((left, right) => left.localeCompare(right))
482
- .map((key) => [
483
- key,
484
- canonicalizeEvidenceValue(value[key], ancestors)
485
- ]));
486
- }
487
- finally {
488
- ancestors.delete(value);
489
- }
490
- }
491
- function canonicalizeEvidence(value) {
492
- try {
493
- return canonicalizeEvidenceValue(value);
494
- }
495
- catch (error) {
496
- if (error instanceof GenerationSessionStoreError) {
497
- throw error;
498
- }
499
- throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence value cannot be safely inspected", { cause: error });
500
- }
501
- }
502
- function validateEvidencePath(relativePath) {
503
- if (typeof relativePath !== "string"
504
- || relativePath.length === 0
505
- || relativePath.includes("\\")
506
- || relativePath.includes("\0")
507
- || isAbsolute(relativePath)
508
- || posix.isAbsolute(relativePath)) {
509
- throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", "Invalid generation evidence path");
510
- }
511
- const segments = relativePath.split("/");
512
- if (segments.some((segment) => (segment.length === 0
513
- || segment === "."
514
- || segment === ".."))
515
- || posix.normalize(relativePath) !== relativePath) {
516
- throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", "Invalid generation evidence path");
517
- }
518
- return segments;
519
- }
520
- function assertContained(root, candidate, allowRoot = false) {
521
- const fromRoot = relative(root, candidate);
522
- if ((!allowRoot && fromRoot.length === 0)
523
- || fromRoot.startsWith("..")
524
- || isAbsolute(fromRoot)) {
525
- throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence path escapes generation session: ${candidate}`);
526
- }
527
- }
528
- async function captureDirectoryEvidence(path) {
529
- const stats = await lstat(path, { bigint: true });
530
- if (!stats.isDirectory() || stats.isSymbolicLink()) {
531
- throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence parent is not a real directory: ${path}`);
532
- }
533
- return {
534
- path,
535
- canonicalPath: await realpath(path),
536
- identity: { dev: stats.dev, ino: stats.ino }
537
- };
538
- }
539
- async function verifyDirectoryEvidence(activeCanonicalPath, directories) {
540
- for (const evidence of directories) {
541
- const current = await captureDirectoryEvidence(evidence.path);
542
- assertContained(activeCanonicalPath, current.canonicalPath, true);
543
- if (current.canonicalPath !== evidence.canonicalPath
544
- || current.identity.dev !== evidence.identity.dev
545
- || current.identity.ino !== evidence.identity.ino) {
546
- throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence parent identity changed: ${evidence.path}`);
547
- }
548
- }
549
- }
550
- async function assertEvidenceNamespaceAvailable(activeDirectory, activeEvidence, inFlight) {
551
- const relativePath = `evidence/steps/${String(inFlight.stepIndex)}-${inFlight.attemptId}`;
552
- const segments = validateEvidencePath(relativePath);
553
- await verifyStoreDirectory(activeEvidence);
554
- let parent = activeDirectory;
555
- const directories = [activeEvidence];
556
- for (const [index, segment] of segments.entries()) {
557
- const candidate = join(parent, segment);
558
- assertContained(activeDirectory, candidate);
559
- let stats;
560
- try {
561
- stats = await lstat(candidate);
562
- }
563
- catch (error) {
564
- if (isNodeError(error) && error.code === "ENOENT") {
565
- await verifyDirectoryEvidence(activeEvidence.canonicalPath, directories);
566
- return;
567
- }
568
- throw error;
569
- }
570
- if (index === segments.length - 1) {
571
- throw new GenerationSessionStoreError("EVIDENCE_ALREADY_EXISTS", `Generation attempt evidence namespace already exists: ${relativePath}`);
572
- }
573
- if (!stats.isDirectory() || stats.isSymbolicLink()) {
574
- throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Attempt evidence parent is not a safe directory: ${relativePath}`);
575
- }
576
- const evidence = await captureDirectoryEvidence(candidate);
577
- assertContained(activeEvidence.canonicalPath, evidence.canonicalPath, true);
578
- directories.push(evidence);
579
- parent = candidate;
580
- }
581
- }
582
- async function fileIdentity(path) {
583
- const stats = await lstat(path, { bigint: true });
584
- if (!stats.isFile() || stats.isSymbolicLink()) {
585
- throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence output is not a regular file: ${path}`);
586
- }
587
- return { dev: stats.dev, ino: stats.ino };
588
- }
589
- function evidenceNotFound(relativePath, cause) {
590
- return new GenerationSessionStoreError("EVIDENCE_NOT_FOUND", `Generation evidence does not exist: ${relativePath}`, { cause });
591
- }
592
- async function readStateFromDirectory(directory, afterOpen, expectedDirectory) {
593
- const directoryEvidence = expectedDirectory
594
- ?? await captureStoreDirectory(directory);
595
- await verifyStoreDirectory(directoryEvidence);
596
- const statePath = join(directory, "state.json");
597
- let text;
598
- try {
599
- const handle = await open(statePath, constants.O_RDONLY | constants.O_NOFOLLOW);
600
- try {
601
- const openedStats = await handle.stat({ bigint: true });
602
- if (!openedStats.isFile()) {
603
- throw new GenerationSessionStoreError("IO_ERROR", `Generation state is not a regular file: ${statePath}`);
604
- }
605
- const openedIdentity = {
606
- dev: openedStats.dev,
607
- ino: openedStats.ino
608
- };
609
- await afterOpen?.();
610
- await verifyStoreDirectory(directoryEvidence);
611
- const pathIdentity = await captureStoreFile(statePath);
612
- if (!sameIdentity(openedIdentity, pathIdentity)) {
613
- throw new GenerationSessionStoreError("IO_ERROR", `Generation state file identity changed: ${statePath}`);
614
- }
615
- text = await handle.readFile("utf8");
616
- }
617
- finally {
618
- await handle.close();
619
- }
620
- }
621
- catch (error) {
622
- if (error instanceof GenerationSessionStoreError) {
623
- throw error;
624
- }
625
- throw new GenerationSessionStoreError("IO_ERROR", `Unable to read generation session state: ${statePath}`, { cause: error });
626
- }
627
- try {
628
- return parseSession(JSON.parse(text));
629
- }
630
- catch (error) {
631
- if (error instanceof GenerationSessionStoreError) {
632
- throw error;
633
- }
634
- throw new GenerationSessionStoreError("INVALID_SESSION", `Unable to parse generation session state: ${statePath}`, { cause: error });
635
- }
636
- }
637
- async function readBoundState(directory, id, afterOpen, expectedDirectory) {
638
- const session = await readStateFromDirectory(directory, afterOpen, expectedDirectory);
639
- if (sessionId(session) !== id) {
640
- throw new GenerationSessionStoreError("INVALID_SESSION", "Persisted generation session id does not match its directory");
641
- }
642
- return session;
643
- }
644
- async function writeStateAtomically(directory, session, sync = syncDirectory, beforeRename, expectedDirectory, beforeInstall) {
645
- const directoryEvidence = expectedDirectory
646
- ?? await captureStoreDirectory(directory);
647
- await verifyStoreDirectory(directoryEvidence);
648
- const statePath = join(directory, "state.json");
649
- const originalStateIdentity = await captureOptionalStoreFile(statePath);
650
- const temporaryPath = join(directory, `.state.json.${randomUUID()}.tmp`);
651
- let handle;
652
- try {
653
- handle = await open(temporaryPath, constants.O_WRONLY
654
- | constants.O_CREAT
655
- | constants.O_EXCL
656
- | constants.O_NOFOLLOW, 0o600);
657
- await handle.writeFile(serializeJson(session), "utf8");
658
- await handle.sync();
659
- await handle.close();
660
- handle = undefined;
661
- const temporaryIdentity = await captureStoreFile(temporaryPath);
662
- await beforeRename?.();
663
- await verifyStoreDirectory(directoryEvidence);
664
- await verifyOptionalStoreFile(statePath, originalStateIdentity);
665
- await beforeInstall?.();
666
- await rename(temporaryPath, statePath);
667
- const installedIdentity = await captureStoreFile(statePath);
668
- if (!sameIdentity(temporaryIdentity, installedIdentity)) {
669
- throw new GenerationSessionStoreError("IO_ERROR", `Generation state install identity changed: ${statePath}`);
670
- }
671
- await sync(directory);
672
- }
673
- catch (error) {
674
- if (handle !== undefined) {
675
- await handle.close().catch(() => undefined);
676
- }
677
- await rm(temporaryPath, { force: true }).catch(() => undefined);
678
- throw error;
679
- }
680
- }
681
95
  export class FileSystemGenerationSessionStore {
682
96
  projectRoot;
683
97
  generationRoot;
@@ -685,14 +99,26 @@ export class FileSystemGenerationSessionStore {
685
99
  options;
686
100
  now;
687
101
  hooks;
102
+ customGenerationRoot;
103
+ lock;
688
104
  constructor(projectRoot, options = {}) {
689
105
  const configuration = parseStoreConfiguration(projectRoot, options);
690
106
  this.projectRoot = configuration.projectRoot;
691
- this.generationRoot = join(this.projectRoot, ".taphound", "generations");
107
+ this.generationRoot = typeof configuration.options.generationRoot === "string"
108
+ ? resolve(configuration.options.generationRoot)
109
+ : join(this.projectRoot, GENERATIONS_DIR);
110
+ this.customGenerationRoot = this.generationRoot !== join(this.projectRoot, GENERATIONS_DIR);
692
111
  this.locksRoot = join(this.generationRoot, ".locks");
693
112
  this.options = configuration.options;
694
113
  this.now = configuration.now;
695
114
  this.hooks = configuration.hooks;
115
+ this.lock = new SessionLock({
116
+ generationRoot: this.generationRoot,
117
+ locksRoot: this.locksRoot,
118
+ lockTimeoutMs: this.options.lockTimeoutMs,
119
+ lockRetryMs: this.options.lockRetryMs,
120
+ hooks: this.hooks
121
+ });
696
122
  }
697
123
  create = async (input) => {
698
124
  const session = parseSession(input, true);
@@ -836,7 +262,13 @@ export class FileSystemGenerationSessionStore {
836
262
  || (approvedConfirmation !== undefined
837
263
  && (approvedConfirmation.status !== "approved"
838
264
  || approvedConfirmation.proposalHash !== inFlight.proposalHash
839
- || approvedConfirmation.snapshotHash !== inFlight.snapshotHash))) {
265
+ || approvedConfirmation.snapshotHash !== inFlight.snapshotHash))
266
+ || JSON.stringify(inFlight.confirmation) !== JSON.stringify(approvedConfirmation === undefined
267
+ ? undefined
268
+ : {
269
+ challengeId: approvedConfirmation.challengeId,
270
+ approvalMode: approvedConfirmation.approvalMode ?? "localTty"
271
+ })) {
840
272
  throw new GenerationSessionStoreError("INVALID_TRANSITION", "Step begin requires exact active state and approved confirmation");
841
273
  }
842
274
  const next = GenerationSessionSchema.parse({
@@ -851,8 +283,7 @@ export class FileSystemGenerationSessionStore {
851
283
  const now = this.now();
852
284
  if (!(now instanceof Date)
853
285
  || !Number.isFinite(now.getTime())
854
- || now.getTime()
855
- >= new Date(approvedConfirmation.expiresAt).getTime()) {
286
+ || isGenerationConfirmationExpired(approvedConfirmation, now)) {
856
287
  throw new GenerationSessionStoreError("INVALID_TRANSITION", "Approved confirmation expired before step begin");
857
288
  }
858
289
  }
@@ -938,7 +369,7 @@ export class FileSystemGenerationSessionStore {
938
369
  await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
939
370
  });
940
371
  };
941
- beginVerification = async (id, expectedRevision, attemptId) => {
372
+ beginVerification = async (id, expectedRevision, attemptId, owner) => {
942
373
  assertId(id);
943
374
  validateExpectedRevision(expectedRevision);
944
375
  assertId(attemptId);
@@ -971,7 +402,81 @@ export class FileSystemGenerationSessionStore {
971
402
  const next = GenerationSessionSchema.parse({
972
403
  ...current,
973
404
  revision: current.revision + 1,
974
- verification: { status: "running", attemptId }
405
+ verification: {
406
+ status: "running",
407
+ attemptId,
408
+ ...(owner === undefined
409
+ ? {}
410
+ : { ownerPid: owner.pid, startedAt: owner.startedAt })
411
+ }
412
+ });
413
+ await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
414
+ return next;
415
+ });
416
+ };
417
+ updateVerificationPhase = async (id, attemptId, phase) => {
418
+ assertId(id);
419
+ assertId(attemptId);
420
+ const parsedPhase = parseVerificationPhase(phase);
421
+ await this.ensureGenerationRoot();
422
+ await this.withLock(id, async () => {
423
+ const activeDirectory = this.activeDirectory(id);
424
+ if (!await pathExists(activeDirectory)) {
425
+ if (await pathExists(this.finalDirectory(id))) {
426
+ throw new GenerationSessionStoreError("SESSION_PUBLISHED", `Published generation session cannot update verification phase: ${id}`);
427
+ }
428
+ throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
429
+ }
430
+ const activeEvidence = await captureStoreDirectory(activeDirectory);
431
+ const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
432
+ if (current.state !== "active"
433
+ || current.verification.status !== "running"
434
+ || current.verification.attemptId !== attemptId
435
+ || current.publication.status !== "notRun") {
436
+ throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification phase updates require the exact active running attempt");
437
+ }
438
+ const next = GenerationSessionSchema.parse({
439
+ ...current,
440
+ verification: {
441
+ ...current.verification,
442
+ phase: parsedPhase
443
+ }
444
+ });
445
+ await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
446
+ });
447
+ };
448
+ abortVerification = async (id, expectedRevision) => {
449
+ assertId(id);
450
+ validateExpectedRevision(expectedRevision);
451
+ await this.ensureGenerationRoot();
452
+ return this.withLock(id, async () => {
453
+ const activeDirectory = this.activeDirectory(id);
454
+ if (!await pathExists(activeDirectory)) {
455
+ if (await pathExists(this.finalDirectory(id))) {
456
+ throw new GenerationSessionStoreError("SESSION_PUBLISHED", `Published generation session cannot abort verification: ${id}`);
457
+ }
458
+ throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
459
+ }
460
+ const activeEvidence = await captureStoreDirectory(activeDirectory);
461
+ const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
462
+ if (current.revision !== expectedRevision) {
463
+ throw new GenerationSessionStoreError("REVISION_CONFLICT", `Expected generation revision ${String(expectedRevision)}, found ${String(current.revision)}`);
464
+ }
465
+ if (current.state !== "active"
466
+ || current.inFlight !== null
467
+ || current.pendingConfirmation !== null
468
+ || current.verification.status !== "running"
469
+ || current.publication.status !== "notRun") {
470
+ throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification abort requires an active running attempt");
471
+ }
472
+ if (await pathExists(join(activeDirectory, "verification", "receipt.json"))
473
+ || await pathExists(join(activeDirectory, "verification", "report.json"))) {
474
+ throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification abort is forbidden after immutable evidence exists");
475
+ }
476
+ const next = GenerationSessionSchema.parse({
477
+ ...current,
478
+ revision: current.revision + 1,
479
+ verification: { status: "notRun" }
975
480
  });
976
481
  await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
977
482
  return next;
@@ -983,6 +488,52 @@ export class FileSystemGenerationSessionStore {
983
488
  failVerification = async (id, expectedRevision, input) => {
984
489
  await this.writeVerificationTransition(id, expectedRevision, input, "failed");
985
490
  };
491
+ recoverVerification = async (id, expectedRevision, input) => {
492
+ assertId(id);
493
+ const next = parseSession(input, true);
494
+ validateNextRevision(id, expectedRevision, next);
495
+ await this.ensureGenerationRoot();
496
+ await this.withLock(id, async () => {
497
+ const activeDirectory = this.activeDirectory(id);
498
+ if (!await pathExists(activeDirectory)) {
499
+ throw new GenerationSessionStoreError(await pathExists(this.finalDirectory(id))
500
+ ? "SESSION_PUBLISHED"
501
+ : "SESSION_NOT_FOUND", `Generation session cannot recover verification: ${id}`);
502
+ }
503
+ const activeEvidence = await captureStoreDirectory(activeDirectory);
504
+ const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
505
+ if (current.revision !== expectedRevision) {
506
+ throw new GenerationSessionStoreError("REVISION_CONFLICT", `Expected generation revision ${String(expectedRevision)}, found ${String(current.revision)}`);
507
+ }
508
+ if (await pathExists(join(activeDirectory, "verification", "receipt.json"))
509
+ || await pathExists(join(activeDirectory, "verification", "report.json"))) {
510
+ throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification recovery is forbidden after immutable evidence exists");
511
+ }
512
+ assertVerificationRecoveryTransition(current, next);
513
+ await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
514
+ });
515
+ };
516
+ reopenVerification = async (id, expectedRevision, input) => {
517
+ assertId(id);
518
+ const next = parseSession(input, true);
519
+ validateNextRevision(id, expectedRevision, next);
520
+ await this.ensureGenerationRoot();
521
+ await this.withLock(id, async () => {
522
+ const activeDirectory = this.activeDirectory(id);
523
+ if (!await pathExists(activeDirectory)) {
524
+ throw new GenerationSessionStoreError(await pathExists(this.finalDirectory(id))
525
+ ? "SESSION_PUBLISHED"
526
+ : "SESSION_NOT_FOUND", `Generation session cannot reopen verification: ${id}`);
527
+ }
528
+ const activeEvidence = await captureStoreDirectory(activeDirectory);
529
+ const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
530
+ if (current.revision !== expectedRevision) {
531
+ throw new GenerationSessionStoreError("REVISION_CONFLICT", `Expected generation revision ${String(expectedRevision)}, found ${String(current.revision)}`);
532
+ }
533
+ assertVerificationReopenTransition(current, next);
534
+ await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
535
+ });
536
+ };
986
537
  markBundlePublishable = async (id, expectedRevision, input) => {
987
538
  assertId(id);
988
539
  const next = parseSession(input, true);
@@ -1005,6 +556,48 @@ export class FileSystemGenerationSessionStore {
1005
556
  await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
1006
557
  });
1007
558
  };
559
+ archive = async (id, expectedRevision, input) => {
560
+ assertId(id);
561
+ const next = parseSession(input, true);
562
+ validateNextRevision(id, expectedRevision, next);
563
+ await this.ensureGenerationRoot();
564
+ await this.withLock(id, async () => {
565
+ const activeDirectory = this.activeDirectory(id);
566
+ if (!await pathExists(activeDirectory)) {
567
+ if (await pathExists(this.finalDirectory(id))) {
568
+ throw new GenerationSessionStoreError("SESSION_PUBLISHED", `Published generation session cannot be archived: ${id}`);
569
+ }
570
+ throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
571
+ }
572
+ const activeEvidence = await captureStoreDirectory(activeDirectory);
573
+ const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
574
+ if (current.revision !== expectedRevision) {
575
+ throw new GenerationSessionStoreError("REVISION_CONFLICT", `Expected generation revision ${String(expectedRevision)}, found ${String(current.revision)}`);
576
+ }
577
+ assertArchiveTransition(current, next);
578
+ await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
579
+ });
580
+ };
581
+ list = async () => {
582
+ await this.ensureGenerationRoot();
583
+ const entries = await readdir(this.generationRoot, {
584
+ withFileTypes: true
585
+ });
586
+ const sessions = [];
587
+ for (const entry of entries) {
588
+ if (!entry.isDirectory() || entry.name === ".locks") {
589
+ continue;
590
+ }
591
+ const directory = join(this.generationRoot, entry.name);
592
+ const statePath = join(directory, "state.json");
593
+ if (!await pathExists(statePath)) {
594
+ continue;
595
+ }
596
+ const session = await readStateFromDirectory(directory);
597
+ sessions.push(session);
598
+ }
599
+ return sessions.sort((left, right) => left.id.localeCompare(right.id));
600
+ };
1008
601
  writeEvidence = async (id, relativePath, value) => {
1009
602
  const canonicalValue = canonicalizeEvidence(value);
1010
603
  await this.produceEvidence(id, relativePath, async (temporaryPath) => {
@@ -1129,7 +722,7 @@ export class FileSystemGenerationSessionStore {
1129
722
  // Preserve any path that cannot be proven to be our own link.
1130
723
  }
1131
724
  }
1132
- if (isNodeError(error) && error.code === "EEXIST") {
725
+ if (isErrnoException(error) && error.code === "EEXIST") {
1133
726
  throw new GenerationSessionStoreError("EVIDENCE_ALREADY_EXISTS", `Generation evidence already exists: ${relativePath}`, { cause: error });
1134
727
  }
1135
728
  throw error;
@@ -1140,67 +733,10 @@ export class FileSystemGenerationSessionStore {
1140
733
  });
1141
734
  };
1142
735
  readEvidence = async (id, relativePath) => {
1143
- assertId(id);
1144
- const segments = validateEvidencePath(relativePath);
1145
- await this.ensureGenerationRoot();
1146
- return this.withLock(id, async () => {
1147
- const finalDirectory = this.finalDirectory(id);
1148
- const activeDirectory = this.activeDirectory(id);
1149
- const directory = await pathExists(finalDirectory)
1150
- ? finalDirectory
1151
- : activeDirectory;
1152
- if (!await pathExists(directory)) {
1153
- throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
1154
- }
1155
- const rootEvidence = await captureStoreDirectory(directory);
1156
- await readBoundState(directory, id, this.hooks.afterStateOpen, rootEvidence);
1157
- let current = directory;
1158
- const directories = [rootEvidence];
1159
- for (const segment of segments.slice(0, -1)) {
1160
- current = join(current, segment);
1161
- let evidence;
1162
- try {
1163
- evidence = await captureDirectoryEvidence(current);
1164
- }
1165
- catch (error) {
1166
- if (isNodeError(error) && error.code === "ENOENT") {
1167
- throw evidenceNotFound(relativePath, error);
1168
- }
1169
- throw error;
1170
- }
1171
- assertContained(rootEvidence.canonicalPath, evidence.canonicalPath);
1172
- directories.push(evidence);
1173
- }
1174
- const path = join(current, segments.at(-1));
1175
- assertContained(directory, path);
1176
- await verifyDirectoryEvidence(rootEvidence.canonicalPath, directories);
1177
- let handle;
1178
- try {
1179
- handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1180
- }
1181
- catch (error) {
1182
- if (isNodeError(error) && error.code === "ENOENT") {
1183
- throw evidenceNotFound(relativePath, error);
1184
- }
1185
- throw error;
1186
- }
1187
- try {
1188
- const stats = await handle.stat({ bigint: true });
1189
- if (!stats.isFile()) {
1190
- throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence is not a regular file: ${relativePath}`);
1191
- }
1192
- const openedIdentity = { dev: stats.dev, ino: stats.ino };
1193
- const pathIdentity = await fileIdentity(path);
1194
- if (!sameIdentity(openedIdentity, pathIdentity)) {
1195
- throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence file identity changed: ${relativePath}`);
1196
- }
1197
- await verifyDirectoryEvidence(rootEvidence.canonicalPath, directories);
1198
- return await handle.readFile();
1199
- }
1200
- finally {
1201
- await handle.close();
1202
- }
1203
- });
736
+ return this.withVerifiedEvidence(id, relativePath, ({ handle }) => handle.readFile());
737
+ };
738
+ evidenceReference = async (id, relativePath) => {
739
+ return this.withVerifiedEvidence(id, relativePath, ({ published, segments }) => (`${GENERATIONS_DIR}/${published ? id : activeBundleName(id)}/${segments.join("/")}`));
1204
740
  };
1205
741
  listEvidence = async (id) => {
1206
742
  assertId(id);
@@ -1248,7 +784,7 @@ export class FileSystemGenerationSessionStore {
1248
784
  stats = await lstat(childPath, { bigint: true });
1249
785
  }
1250
786
  catch (error) {
1251
- if (isNodeError(error) && error.code === "ENOENT") {
787
+ if (isErrnoException(error) && error.code === "ENOENT") {
1252
788
  throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Generation evidence disappeared during snapshot: ${relativePath}`);
1253
789
  }
1254
790
  throw error;
@@ -1312,7 +848,7 @@ export class FileSystemGenerationSessionStore {
1312
848
  await verifyStoreDirectory(directoryEvidence);
1313
849
  };
1314
850
  await visit(directory, []);
1315
- return files.sort((left, right) => left.path.localeCompare(right.path));
851
+ return files.sort((left, right) => compareStrings(left.path, right.path));
1316
852
  });
1317
853
  };
1318
854
  publish = async (id) => {
@@ -1351,7 +887,7 @@ export class FileSystemGenerationSessionStore {
1351
887
  }
1352
888
  }
1353
889
  catch (error) {
1354
- if (isNodeError(error)
890
+ if (isErrnoException(error)
1355
891
  && (error.code === "EEXIST"
1356
892
  || error.code === "ENOTEMPTY")) {
1357
893
  throw new GenerationSessionStoreError("PUBLISH_DESTINATION_EXISTS", `Generation publish destination already exists: ${finalDirectory}`, { cause: error });
@@ -1364,17 +900,28 @@ export class FileSystemGenerationSessionStore {
1364
900
  };
1365
901
  ensureGenerationRoot = async () => {
1366
902
  try {
903
+ if (this.customGenerationRoot) {
904
+ await mkdir(dirname(this.generationRoot), { recursive: true });
905
+ await createOrRequireDirectory(this.generationRoot);
906
+ await createOrRequireDirectory(this.locksRoot);
907
+ return;
908
+ }
1367
909
  await requireRealDirectory(this.projectRoot);
1368
- const taphoundDirectory = join(this.projectRoot, ".taphound");
910
+ const taphoundDirectory = join(this.projectRoot, TAPHOUND_DIR);
1369
911
  if (await createOrRequireDirectory(taphoundDirectory)) {
1370
912
  await this.syncDirectory(this.projectRoot);
1371
913
  }
1372
- if (await createOrRequireDirectory(this.generationRoot)) {
914
+ const buildDirectory = join(this.projectRoot, BUILD_DIR);
915
+ if (await createOrRequireDirectory(buildDirectory)) {
1373
916
  await this.syncDirectory(taphoundDirectory);
1374
917
  }
918
+ if (await createOrRequireDirectory(this.generationRoot)) {
919
+ await this.syncDirectory(buildDirectory);
920
+ }
1375
921
  if (await createOrRequireDirectory(this.locksRoot)) {
1376
922
  await this.syncDirectory(this.generationRoot);
1377
923
  }
924
+ await ensureBuildIgnored(this.projectRoot);
1378
925
  }
1379
926
  catch (error) {
1380
927
  throw asStoreIoError(error, "initialize its generation directory");
@@ -1402,195 +949,74 @@ export class FileSystemGenerationSessionStore {
1402
949
  await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
1403
950
  });
1404
951
  };
1405
- activeDirectory = (id) => (join(this.generationRoot, `.${id}.work`));
952
+ activeDirectory = (id) => (join(this.generationRoot, activeBundleName(id)));
1406
953
  finalDirectory = (id) => (join(this.generationRoot, id));
1407
- lockPath = (id) => (join(this.locksRoot, `${id}.lock`));
1408
- syncDirectory = async (path) => {
1409
- await syncDirectory(path);
1410
- await this.hooks.afterDirectorySync?.(path);
1411
- };
1412
- withLock = async (id, operation) => {
1413
- const token = randomUUID();
1414
- let generationRootEvidence;
1415
- let locksRootEvidence;
1416
- try {
1417
- generationRootEvidence = await captureStoreDirectory(this.generationRoot);
1418
- locksRootEvidence = await captureStoreDirectory(this.locksRoot);
1419
- await this.acquireLock(id, { pid: process.pid, token }, generationRootEvidence, locksRootEvidence);
1420
- await verifyStoreDirectory(generationRootEvidence);
1421
- await verifyStoreDirectory(locksRootEvidence);
1422
- }
1423
- catch (error) {
1424
- throw asStoreIoError(error, "acquire its exclusive lock");
1425
- }
1426
- let outcome;
1427
- let releaseError;
1428
- try {
1429
- outcome = {
1430
- status: "succeeded",
1431
- value: await operation()
1432
- };
1433
- }
1434
- catch (error) {
1435
- outcome = { status: "failed", error };
1436
- }
1437
- finally {
1438
- try {
1439
- await this.releaseLock(id, token);
1440
- }
1441
- catch (error) {
1442
- releaseError = error;
1443
- }
1444
- }
1445
- if (outcome.status === "failed") {
1446
- throw asStoreIoError(outcome.error, "complete a locked operation");
1447
- }
1448
- if (releaseError !== undefined) {
1449
- throw asStoreIoError(releaseError, "release its exclusive lock");
1450
- }
1451
- return outcome.value;
1452
- };
1453
- acquireLock = async (id, owner, generationRootEvidence, locksRootEvidence) => {
1454
- const lockPath = this.lockPath(id);
1455
- const deadline = Date.now() + this.options.lockTimeoutMs;
1456
- for (;;) {
1457
- if (await this.tryInstallLock(id, lockPath, owner, generationRootEvidence, locksRootEvidence)) {
1458
- return;
1459
- }
1460
- await this.reapDeadOwnerLock(id, lockPath);
1461
- if (Date.now() >= deadline) {
1462
- throw new GenerationSessionStoreError("LOCK_TIMEOUT", `Timed out acquiring generation session lock: ${id}`);
1463
- }
1464
- await new Promise((resolveDelay) => {
1465
- setTimeout(resolveDelay, this.options.lockRetryMs);
1466
- });
1467
- }
1468
- };
1469
- tryInstallLock = async (id, lockPath, owner, generationRootEvidence, locksRootEvidence) => {
1470
- const stagingPath = join(this.locksRoot, `.${id}.lock.acquire-${randomUUID()}.tmp`);
1471
- try {
1472
- const handle = await open(stagingPath, "wx", 0o600);
1473
- try {
1474
- await this.hooks.beforeLockStagingWrite?.();
1475
- await handle.writeFile(serializeJson(owner), "utf8");
1476
- await handle.sync();
954
+ withVerifiedEvidence = async (id, relativePath, operation) => {
955
+ assertId(id);
956
+ const segments = validateEvidencePath(relativePath);
957
+ await this.ensureGenerationRoot();
958
+ return this.withLock(id, async () => {
959
+ const finalDirectory = this.finalDirectory(id);
960
+ const activeDirectory = this.activeDirectory(id);
961
+ const published = await pathExists(finalDirectory);
962
+ const directory = published ? finalDirectory : activeDirectory;
963
+ if (!await pathExists(directory)) {
964
+ throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
1477
965
  }
1478
- finally {
1479
- await handle.close();
966
+ const rootEvidence = await captureStoreDirectory(directory);
967
+ await readBoundState(directory, id, this.hooks.afterStateOpen, rootEvidence);
968
+ let current = directory;
969
+ const directories = [rootEvidence];
970
+ for (const segment of segments.slice(0, -1)) {
971
+ current = join(current, segment);
972
+ let evidence;
973
+ try {
974
+ evidence = await captureDirectoryEvidence(current);
975
+ }
976
+ catch (error) {
977
+ if (isErrnoException(error) && error.code === "ENOENT") {
978
+ throw evidenceNotFound(relativePath, error);
979
+ }
980
+ throw error;
981
+ }
982
+ assertContained(rootEvidence.canonicalPath, evidence.canonicalPath);
983
+ directories.push(evidence);
1480
984
  }
1481
- await this.hooks.beforeLockInstall?.();
1482
- await verifyStoreDirectory(generationRootEvidence);
1483
- await verifyStoreDirectory(locksRootEvidence);
985
+ const path = join(current, segments.at(-1));
986
+ assertContained(directory, path);
987
+ await verifyDirectoryEvidence(rootEvidence.canonicalPath, directories);
988
+ let handle;
1484
989
  try {
1485
- await link(stagingPath, lockPath);
990
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1486
991
  }
1487
992
  catch (error) {
1488
- if (isNodeError(error) && error.code === "EEXIST") {
1489
- return false;
993
+ if (isErrnoException(error) && error.code === "ENOENT") {
994
+ throw evidenceNotFound(relativePath, error);
1490
995
  }
1491
996
  throw error;
1492
997
  }
1493
998
  try {
1494
- await this.syncDirectory(this.locksRoot);
1495
- await verifyStoreDirectory(generationRootEvidence);
1496
- await verifyStoreDirectory(locksRootEvidence);
1497
- }
1498
- catch (error) {
1499
- await this.releaseLock(id, owner.token).catch(() => undefined);
1500
- throw error;
1501
- }
1502
- return true;
1503
- }
1504
- finally {
1505
- await unlink(stagingPath).catch(() => undefined);
1506
- }
1507
- };
1508
- reapDeadOwnerLock = async (id, lockPath) => {
1509
- try {
1510
- const identity = await this.fileIdentity(lockPath);
1511
- const owner = await this.readLockOwner(lockPath);
1512
- if (owner === null || this.isProcessAlive(owner.pid)) {
1513
- return;
1514
- }
1515
- const tombstone = join(this.locksRoot, `.${id}.lock.reap-${randomUUID()}`);
1516
- await rename(lockPath, tombstone);
1517
- const movedIdentity = await this.fileIdentity(tombstone);
1518
- if (movedIdentity.dev !== identity.dev
1519
- || movedIdentity.ino !== identity.ino) {
1520
- await rename(tombstone, lockPath).catch(() => undefined);
1521
- return;
1522
- }
1523
- await this.hooks.afterLockTombstoneRename?.();
1524
- await unlink(tombstone);
1525
- await this.syncDirectory(this.locksRoot);
1526
- }
1527
- catch (error) {
1528
- if (!isNodeError(error) || error.code !== "ENOENT") {
1529
- throw error;
1530
- }
1531
- }
1532
- };
1533
- releaseLock = async (id, token) => {
1534
- const lockPath = this.lockPath(id);
1535
- try {
1536
- const identity = await this.fileIdentity(lockPath);
1537
- const current = await this.readLockOwner(lockPath);
1538
- if (current?.pid === process.pid && current.token === token) {
1539
- const tombstone = join(this.locksRoot, `.${id}.lock.release-${randomUUID()}`);
1540
- await rename(lockPath, tombstone);
1541
- const movedIdentity = await this.fileIdentity(tombstone);
1542
- if (movedIdentity.dev === identity.dev
1543
- && movedIdentity.ino === identity.ino) {
1544
- await unlink(tombstone);
1545
- await this.syncDirectory(this.locksRoot);
999
+ const stats = await handle.stat({ bigint: true });
1000
+ if (!stats.isFile()) {
1001
+ throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence is not a regular file: ${relativePath}`);
1546
1002
  }
1547
- else {
1548
- await rename(tombstone, lockPath).catch(() => undefined);
1003
+ const openedIdentity = { dev: stats.dev, ino: stats.ino };
1004
+ const pathIdentity = await fileIdentity(path);
1005
+ if (!sameIdentity(openedIdentity, pathIdentity)) {
1006
+ throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence file identity changed: ${relativePath}`);
1549
1007
  }
1008
+ await verifyDirectoryEvidence(rootEvidence.canonicalPath, directories);
1009
+ return await operation({ handle, published, segments });
1550
1010
  }
1551
- }
1552
- catch (error) {
1553
- if (!isNodeError(error) || error.code !== "ENOENT") {
1554
- throw error;
1011
+ finally {
1012
+ await handle.close();
1555
1013
  }
1556
- }
1557
- };
1558
- readLockOwner = async (lockPath) => {
1559
- const handle = await open(lockPath, constants.O_RDONLY | constants.O_NOFOLLOW);
1560
- try {
1561
- const value = JSON.parse(await handle.readFile("utf8"));
1562
- if (value === null
1563
- || typeof value !== "object"
1564
- || !Number.isSafeInteger(value.pid)
1565
- || value.pid <= 0
1566
- || typeof value.token !== "string"
1567
- || value.token.length === 0) {
1568
- return null;
1569
- }
1570
- return value;
1571
- }
1572
- catch {
1573
- return null;
1574
- }
1575
- finally {
1576
- await handle.close();
1577
- }
1578
- };
1579
- fileIdentity = async (path) => {
1580
- const stats = await lstat(path, { bigint: true });
1581
- if (!stats.isFile() || stats.isSymbolicLink()) {
1582
- throw new GenerationSessionStoreError("IO_ERROR", `Generation lock path is not a real file: ${path}`);
1583
- }
1584
- return { dev: stats.dev, ino: stats.ino };
1014
+ });
1585
1015
  };
1586
- isProcessAlive = (pid) => {
1587
- try {
1588
- process.kill(pid, 0);
1589
- return true;
1590
- }
1591
- catch (error) {
1592
- return !isNodeError(error) || error.code !== "ESRCH";
1593
- }
1016
+ withLock = (id, operation) => this.lock.withLock(id, operation);
1017
+ syncDirectory = async (path) => {
1018
+ await syncDirectory(path);
1019
+ await this.hooks.afterDirectorySync?.(path);
1594
1020
  };
1595
1021
  assertPublishable(session) {
1596
1022
  if (session.verification.status !== "passed"