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
@@ -0,0 +1,1207 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import process from "node:process";
4
+ import {
5
+ access, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile
6
+ } from "node:fs/promises";
7
+ import {
8
+ dirname, isAbsolute, join, relative, resolve, sep
9
+ } from "node:path";
10
+
11
+ const CATALOG = "cases.json";
12
+ const LEDGER = "case-ledger.json";
13
+ const STATUS = "STATUS.md";
14
+ const LOCK = ".case-ledger.lock";
15
+ const shaPattern = /^[a-f0-9]{64}$/;
16
+ const COMPUTE_SHA = "compute";
17
+ const caseIdPattern = /^[A-Z][A-Z0-9_-]{0,63}$/;
18
+ const suiteIdPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
19
+ const flowNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,199}$/;
20
+ const terminalStates = new Set(["verified", "archived"]);
21
+ const inactiveStates = new Set(["pending", "deferred", ...terminalStates]);
22
+ const statuses = new Set([
23
+ "pending", "briefing", "briefReady", "generating", "recoveryRequired",
24
+ "verificationPending", "verificationFailed", "blocked", "deferred",
25
+ "verified", "archived"
26
+ ]);
27
+ const transitions = new Map([
28
+ ["pending", new Set(["briefing", "blocked", "deferred", "archived"])],
29
+ ["briefing", new Set(["briefReady", "blocked", "deferred", "archived"])],
30
+ ["briefReady", new Set([
31
+ "briefing", "generating", "blocked", "deferred", "archived"
32
+ ])],
33
+ ["generating", new Set([
34
+ "recoveryRequired", "verificationPending", "blocked", "deferred", "archived"
35
+ ])],
36
+ ["recoveryRequired", new Set([
37
+ "generating", "verificationPending", "blocked", "deferred", "archived"
38
+ ])],
39
+ ["verificationPending", new Set([
40
+ "recoveryRequired", "verificationFailed", "verified", "blocked", "deferred",
41
+ "archived"
42
+ ])],
43
+ ["verificationFailed", new Set([
44
+ "generating", "blocked", "deferred", "archived"
45
+ ])],
46
+ ["blocked", new Set([
47
+ "pending", "briefing", "briefReady", "generating", "recoveryRequired",
48
+ "verificationPending", "verificationFailed", "deferred", "archived"
49
+ ])],
50
+ ["deferred", new Set([
51
+ "pending", "briefing", "briefReady", "generating", "recoveryRequired",
52
+ "verificationPending", "verificationFailed", "blocked", "archived"
53
+ ])],
54
+ ["verified", new Set()],
55
+ ["archived", new Set()]
56
+ ]);
57
+
58
+ function fail(code, message) {
59
+ const error = new Error(message);
60
+ error.code = code;
61
+ throw error;
62
+ }
63
+
64
+ function digest(bytes) {
65
+ return createHash("sha256").update(bytes).digest("hex");
66
+ }
67
+
68
+ function canonical(value) {
69
+ if (Array.isArray(value)) return value.map(canonical);
70
+ if (value !== null && typeof value === "object") {
71
+ return Object.fromEntries(
72
+ Object.entries(value)
73
+ .filter(([, item]) => item !== undefined)
74
+ .sort(([left], [right]) => left.localeCompare(right))
75
+ .map(([key, item]) => [key, canonical(item)])
76
+ );
77
+ }
78
+ return value;
79
+ }
80
+
81
+ function canonicalHash(value) {
82
+ return digest(JSON.stringify(canonical(value)));
83
+ }
84
+
85
+ function exactKeys(value, required, optional = []) {
86
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
87
+ fail("CASE_SUITE_INVALID", "Expected a JSON object");
88
+ }
89
+ const allowed = new Set([...required, ...optional]);
90
+ for (const key of Object.keys(value)) {
91
+ if (!allowed.has(key)) {
92
+ fail("CASE_SUITE_INVALID", `Unknown field "${key}"`);
93
+ }
94
+ }
95
+ for (const key of required) {
96
+ if (!(key in value)) {
97
+ fail("CASE_SUITE_INVALID", `Missing required field "${key}"`);
98
+ }
99
+ }
100
+ }
101
+
102
+ function nonempty(value, label, max = 1000) {
103
+ if (typeof value !== "string") {
104
+ fail("CASE_SUITE_INVALID", `${label} must be a string`);
105
+ }
106
+ if (!value.trim()) {
107
+ fail("CASE_SUITE_INVALID", `${label} must be a non-empty string`);
108
+ }
109
+ if (value.length > max) {
110
+ fail("CASE_SUITE_INVALID", `${label} exceeds ${String(max)} characters`);
111
+ }
112
+ return value;
113
+ }
114
+
115
+ function integer(value, label, minimum = 0) {
116
+ if (!Number.isInteger(value) || value < minimum) {
117
+ fail("CASE_SUITE_INVALID", `${label} must be an integer >= ${String(minimum)}`);
118
+ }
119
+ return value;
120
+ }
121
+
122
+ function assertSha(value, label) {
123
+ if (typeof value !== "string" || !shaPattern.test(value)) {
124
+ fail("CASE_SUITE_INVALID", `${label} must be a lowercase SHA-256`);
125
+ }
126
+ }
127
+
128
+ function inside(root, path) {
129
+ const rel = relative(root, path);
130
+ return rel === "" || (
131
+ rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)
132
+ );
133
+ }
134
+
135
+ function projectPath(value, label) {
136
+ const path = nonempty(value, label);
137
+ if (isAbsolute(path) || path.includes("\\") || path.split("/").includes("..")
138
+ || path.startsWith("./") || path.endsWith("/")) {
139
+ fail("CASE_SUITE_INVALID", `${label} must be a normalized project-relative path`);
140
+ }
141
+ return path;
142
+ }
143
+
144
+ async function json(path, label = path) {
145
+ try {
146
+ return JSON.parse(await readFile(path, "utf8"));
147
+ } catch (error) {
148
+ fail("CASE_SUITE_INVALID", `${label} is not readable JSON: ${String(error)}`);
149
+ }
150
+ }
151
+
152
+ async function exactHash(path) {
153
+ return digest(await readFile(path));
154
+ }
155
+
156
+ async function resolveProjectFile(projectRoot, path, label) {
157
+ const relativePath = projectPath(path, `${label}.path`);
158
+ const absolute = resolve(projectRoot, relativePath);
159
+ if (!inside(projectRoot, absolute)) {
160
+ fail("CASE_SUITE_INVALID", `${label} escapes the project`);
161
+ }
162
+ let info;
163
+ try {
164
+ info = await lstat(absolute);
165
+ } catch {
166
+ fail("CASE_SUITE_INVALID", `${label} does not exist: ${relativePath}`);
167
+ }
168
+ if (!info.isFile() || info.isSymbolicLink()) {
169
+ fail("CASE_SUITE_INVALID", `${label} must be a regular non-symlink file`);
170
+ }
171
+ const resolved = await realpath(absolute);
172
+ if (!inside(projectRoot, resolved)) {
173
+ fail("CASE_SUITE_INVALID", `${label} resolves outside the project`);
174
+ }
175
+ return { absolute: resolved, relativePath };
176
+ }
177
+
178
+ async function regularProjectFile(projectRoot, artifact, label) {
179
+ exactKeys(artifact, ["path", "sha256"]);
180
+ assertSha(artifact.sha256, `${label}.sha256`);
181
+ const file = await resolveProjectFile(projectRoot, artifact.path, label);
182
+ const actual = await exactHash(file.absolute);
183
+ if (actual !== artifact.sha256) {
184
+ fail("CASE_SUITE_STALE", `${label} hash changed`);
185
+ }
186
+ return file;
187
+ }
188
+
189
+ function parseArtifact(value, label, { allowCompute = false } = {}) {
190
+ exactKeys(value, ["path", "sha256"]);
191
+ projectPath(value.path, `${label}.path`);
192
+ if (!(allowCompute && value.sha256 === COMPUTE_SHA)) {
193
+ assertSha(value.sha256, `${label}.sha256`);
194
+ }
195
+ return { path: value.path, sha256: value.sha256 };
196
+ }
197
+
198
+ // The recorded hash is the exact file bytes, not the canonical JSON hash that
199
+ // meta.journeySha256 and report.journey.sha256 use.
200
+ async function computeArtifactHash(projectRoot, artifact, label) {
201
+ if (artifact.sha256 !== COMPUTE_SHA) {
202
+ return artifact;
203
+ }
204
+ const file = await resolveProjectFile(projectRoot, artifact.path, label);
205
+ return { path: artifact.path, sha256: await exactHash(file.absolute) };
206
+ }
207
+
208
+ function parseFailure(value) {
209
+ exactKeys(value, ["code", "message"]);
210
+ return {
211
+ code: nonempty(value.code, "failure.code", 200),
212
+ message: nonempty(value.message, "failure.message", 1000)
213
+ };
214
+ }
215
+
216
+ function parseGeneration(value) {
217
+ exactKeys(value, ["id"], ["baseFlow"]);
218
+ const generation = { id: nonempty(value.id, "generation.id", 200) };
219
+ if (value.baseFlow !== undefined) {
220
+ generation.baseFlow = nonempty(value.baseFlow, "generation.baseFlow", 200);
221
+ }
222
+ return generation;
223
+ }
224
+
225
+ function parseCatalogCase(value) {
226
+ exactKeys(
227
+ value,
228
+ ["id", "order", "title", "sourceText", "risk", "dependsOn"],
229
+ ["plannedBaseFlow"]
230
+ );
231
+ const id = nonempty(value.id, "case.id", 64);
232
+ if (!caseIdPattern.test(id)) {
233
+ fail("CASE_SUITE_INVALID", `Invalid Case ID ${id}`);
234
+ }
235
+ if (!["readOnly", "stateChanging", "external"].includes(value.risk)) {
236
+ fail("CASE_SUITE_INVALID", `Invalid risk for ${id}`);
237
+ }
238
+ if (!Array.isArray(value.dependsOn)
239
+ || value.dependsOn.some((entry) => typeof entry !== "string")) {
240
+ fail("CASE_SUITE_INVALID", `dependsOn for ${id} must be a string array`);
241
+ }
242
+ if (new Set(value.dependsOn).size !== value.dependsOn.length) {
243
+ fail("CASE_SUITE_INVALID", `Case ${id} has duplicate dependencies`);
244
+ }
245
+ return {
246
+ id,
247
+ order: integer(value.order, `${id}.order`, 1),
248
+ title: nonempty(value.title, `${id}.title`, 200),
249
+ sourceText: nonempty(value.sourceText, `${id}.sourceText`, 10000),
250
+ risk: value.risk,
251
+ dependsOn: [...value.dependsOn],
252
+ ...(value.plannedBaseFlow === undefined
253
+ ? {}
254
+ : {
255
+ plannedBaseFlow: nonempty(
256
+ value.plannedBaseFlow, `${id}.plannedBaseFlow`, 200
257
+ )
258
+ })
259
+ };
260
+ }
261
+
262
+ function assertCatalogGraph(cases) {
263
+ const ids = new Set();
264
+ const orders = new Set();
265
+ for (const entry of cases) {
266
+ if (ids.has(entry.id)) fail("CASE_SUITE_INVALID", `Duplicate Case ID ${entry.id}`);
267
+ if (orders.has(entry.order)) {
268
+ fail("CASE_SUITE_INVALID", `Duplicate Case order ${String(entry.order)}`);
269
+ }
270
+ ids.add(entry.id);
271
+ orders.add(entry.order);
272
+ }
273
+ for (const entry of cases) {
274
+ for (const dependency of entry.dependsOn) {
275
+ if (!ids.has(dependency) || dependency === entry.id) {
276
+ fail("CASE_SUITE_INVALID", `Invalid dependency ${dependency} for ${entry.id}`);
277
+ }
278
+ }
279
+ }
280
+ const visiting = new Set();
281
+ const visited = new Set();
282
+ const byId = new Map(cases.map((entry) => [entry.id, entry]));
283
+ const visit = (id) => {
284
+ if (visiting.has(id)) fail("CASE_SUITE_INVALID", `Case dependency cycle at ${id}`);
285
+ if (visited.has(id)) return;
286
+ visiting.add(id);
287
+ for (const dependency of byId.get(id).dependsOn) visit(dependency);
288
+ visiting.delete(id);
289
+ visited.add(id);
290
+ };
291
+ for (const entry of cases) visit(entry.id);
292
+ }
293
+
294
+ function parseCatalog(value) {
295
+ exactKeys(value, [
296
+ "version", "kind", "suiteId", "title", "projectRoot",
297
+ "contextPath", "createdAt", "cases"
298
+ ], ["deviceSerial"]);
299
+ if (value.version !== 1 || value.kind !== "taphound.caseCatalog"
300
+ || !suiteIdPattern.test(value.suiteId ?? "")
301
+ || !isAbsolute(value.projectRoot ?? "")
302
+ || !Array.isArray(value.cases) || value.cases.length === 0) {
303
+ fail("CASE_SUITE_INVALID", "Invalid frozen Case catalog");
304
+ }
305
+ projectPath(value.contextPath, "contextPath");
306
+ nonempty(value.createdAt, "createdAt");
307
+ const cases = value.cases.map(parseCatalogCase).sort(
308
+ (left, right) => left.order - right.order
309
+ );
310
+ assertCatalogGraph(cases);
311
+ return { ...value, cases };
312
+ }
313
+
314
+ function parseLedger(value, catalog) {
315
+ exactKeys(value, [
316
+ "version", "kind", "suiteId", "revision", "catalog",
317
+ "devicePolicy", "baseFlows", "cases", "updatedAt"
318
+ ]);
319
+ if (value.version !== 1 || value.kind !== "taphound.caseLedger"
320
+ || value.suiteId !== catalog.suiteId || !Array.isArray(value.cases)
321
+ || !Array.isArray(value.baseFlows)) {
322
+ fail("CASE_SUITE_INVALID", "Invalid Case Ledger identity");
323
+ }
324
+ integer(value.revision, "ledger.revision");
325
+ exactKeys(value.catalog, ["path", "sha256"]);
326
+ if (value.catalog.path !== CATALOG) {
327
+ fail("CASE_SUITE_INVALID", "Ledger catalog path must be cases.json");
328
+ }
329
+ assertSha(value.catalog.sha256, "ledger.catalog.sha256");
330
+ exactKeys(value.devicePolicy, ["concurrency"], ["deviceSerial"]);
331
+ if (value.devicePolicy.concurrency !== "serial"
332
+ || value.devicePolicy.deviceSerial !== catalog.deviceSerial) {
333
+ fail("CASE_SUITE_INVALID", "Ledger device policy does not match the catalog");
334
+ }
335
+ const catalogById = new Map(catalog.cases.map((entry) => [entry.id, entry]));
336
+ if (value.cases.length !== catalog.cases.length) {
337
+ fail("CASE_SUITE_INVALID", "Ledger Case count differs from the catalog");
338
+ }
339
+ for (const entry of value.cases) {
340
+ exactKeys(entry, [
341
+ "id", "order", "title", "sourceSha256", "risk", "dependsOn",
342
+ "status", "history"
343
+ ], [
344
+ "plannedBaseFlow", "resumeStatus", "brief", "generation", "failure",
345
+ "nextAction", "completion"
346
+ ]);
347
+ const source = catalogById.get(entry.id);
348
+ if (source === undefined || entry.order !== source.order
349
+ || entry.title !== source.title || entry.risk !== source.risk
350
+ || JSON.stringify(entry.dependsOn) !== JSON.stringify(source.dependsOn)
351
+ || entry.plannedBaseFlow !== source.plannedBaseFlow
352
+ || entry.sourceSha256 !== digest(source.sourceText)
353
+ || !statuses.has(entry.status) || !Array.isArray(entry.history)) {
354
+ fail("CASE_SUITE_INVALID", `Ledger Case ${String(entry.id)} drifted from catalog`);
355
+ }
356
+ if (entry.brief !== undefined) parseArtifact(entry.brief, `${entry.id}.brief`);
357
+ if (entry.generation !== undefined) parseGeneration(entry.generation);
358
+ if (entry.failure !== undefined) parseFailure(entry.failure);
359
+ if (entry.nextAction !== undefined) {
360
+ nonempty(entry.nextAction, `${entry.id}.nextAction`, 1000);
361
+ }
362
+ if (entry.completion !== undefined) parseCompletion(entry.completion);
363
+ if (entry.resumeStatus !== undefined && !statuses.has(entry.resumeStatus)) {
364
+ fail("CASE_SUITE_INVALID", `Invalid resumeStatus for ${entry.id}`);
365
+ }
366
+ for (const history of entry.history) {
367
+ exactKeys(history, ["to", "at", "reason"], ["from"]);
368
+ if (!statuses.has(history.to)
369
+ || history.from !== undefined && !statuses.has(history.from)) {
370
+ fail("CASE_SUITE_INVALID", `Invalid transition history for ${entry.id}`);
371
+ }
372
+ nonempty(history.at, `${entry.id}.history.at`, 100);
373
+ nonempty(history.reason, `${entry.id}.history.reason`, 500);
374
+ }
375
+ if (entry.status === "verified" && entry.completion === undefined) {
376
+ fail("CASE_SUITE_INVALID", `Verified Case ${entry.id} lacks completion evidence`);
377
+ }
378
+ if (["recoveryRequired", "verificationFailed", "blocked", "deferred"].includes(
379
+ entry.status
380
+ )
381
+ && (entry.failure === undefined || entry.nextAction === undefined)) {
382
+ fail("CASE_SUITE_INVALID", `${entry.status} Case ${entry.id} lacks recovery details`);
383
+ }
384
+ if (entry.status === "deferred" && entry.resumeStatus === undefined) {
385
+ fail("CASE_SUITE_INVALID", `Deferred Case ${entry.id} lacks resumeStatus`);
386
+ }
387
+ }
388
+ const active = value.cases.filter((entry) => !inactiveStates.has(entry.status));
389
+ if (active.length > 1) {
390
+ fail("CASE_SUITE_CONFLICT", "Serial device policy allows only one active Case");
391
+ }
392
+ const flowNames = new Set();
393
+ for (const flow of value.baseFlows) {
394
+ exactKeys(flow, [
395
+ "name", "status", "path", "sha256", "exitActivity", "journey",
396
+ "resolutionManifest", "report", "recordedAt"
397
+ ]);
398
+ if (!flowNamePattern.test(flow.name ?? "") || flow.status !== "verified") {
399
+ fail("CASE_SUITE_INVALID", "Invalid recorded Base Flow");
400
+ }
401
+ projectPath(flow.path, `${flow.name}.path`);
402
+ assertSha(flow.sha256, `${flow.name}.sha256`);
403
+ nonempty(flow.exitActivity, `${flow.name}.exitActivity`, 500);
404
+ parseArtifact(flow.journey, `${flow.name}.journey`);
405
+ parseArtifact(flow.resolutionManifest, `${flow.name}.resolutionManifest`);
406
+ parseArtifact(flow.report, `${flow.name}.report`);
407
+ nonempty(flow.recordedAt, `${flow.name}.recordedAt`, 100);
408
+ if (flowNames.has(flow.name)) {
409
+ fail("CASE_SUITE_INVALID", `Duplicate Base Flow ${flow.name}`);
410
+ }
411
+ flowNames.add(flow.name);
412
+ }
413
+ return value;
414
+ }
415
+
416
+ function parseCompletion(value, options = {}) {
417
+ exactKeys(value, ["journey", "meta", "finalReport", "independentReport"]);
418
+ return {
419
+ journey: parseArtifact(value.journey, "completion.journey", options),
420
+ meta: parseArtifact(value.meta, "completion.meta", options),
421
+ finalReport: parseArtifact(
422
+ value.finalReport, "completion.finalReport", options
423
+ ),
424
+ independentReport: parseArtifact(
425
+ value.independentReport, "completion.independentReport", options
426
+ )
427
+ };
428
+ }
429
+
430
+ async function computeCompletionHashes(projectRoot, completion) {
431
+ const entries = await Promise.all(
432
+ Object.entries(completion).map(async ([key, artifact]) => [
433
+ key,
434
+ await computeArtifactHash(projectRoot, artifact, `completion.${key}`)
435
+ ])
436
+ );
437
+ return Object.fromEntries(entries);
438
+ }
439
+
440
+ async function loadSuite(suitePath) {
441
+ const root = await realpath(resolve(suitePath)).catch(() => {
442
+ fail("CASE_SUITE_INVALID", `Suite directory does not exist: ${suitePath}`);
443
+ });
444
+ const catalogPath = join(root, CATALOG);
445
+ const ledgerPath = join(root, LEDGER);
446
+ const catalogBytes = await readFile(catalogPath).catch(() => {
447
+ fail("CASE_SUITE_INVALID", `Missing ${CATALOG}`);
448
+ });
449
+ let catalogValue;
450
+ try {
451
+ catalogValue = JSON.parse(catalogBytes.toString("utf8"));
452
+ } catch {
453
+ fail("CASE_SUITE_INVALID", `${CATALOG} is invalid JSON`);
454
+ }
455
+ const catalog = parseCatalog(catalogValue);
456
+ const projectRoot = await realpath(catalog.projectRoot).catch(() => {
457
+ fail("CASE_SUITE_INVALID", "Catalog projectRoot does not exist");
458
+ });
459
+ if (projectRoot !== catalog.projectRoot || !inside(projectRoot, root)) {
460
+ fail("CASE_SUITE_INVALID", "Suite must stay beneath canonical projectRoot");
461
+ }
462
+ const ledger = parseLedger(await json(ledgerPath, LEDGER), catalog);
463
+ const catalogSha256 = digest(catalogBytes);
464
+ if (ledger.catalog.sha256 !== catalogSha256) {
465
+ fail("CASE_SUITE_STALE", "Frozen Case catalog hash changed");
466
+ }
467
+ const suite = {
468
+ root, projectRoot, catalog, ledger, ledgerPath, catalogSha256
469
+ };
470
+ for (const entry of ledger.cases) {
471
+ if (entry.brief !== undefined) {
472
+ await regularProjectFile(projectRoot, entry.brief, `${entry.id}.brief`);
473
+ }
474
+ if (entry.status === "verified") {
475
+ await validateCompletion(suite, entry, entry.completion);
476
+ }
477
+ }
478
+ for (const flow of ledger.baseFlows) {
479
+ await validateFlowEvidence(suite, flow);
480
+ }
481
+ return suite;
482
+ }
483
+
484
+ async function atomicWrite(path, content) {
485
+ const temporary = join(
486
+ dirname(path),
487
+ `.${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`
488
+ );
489
+ try {
490
+ await writeFile(temporary, content, { flag: "wx" });
491
+ await rename(temporary, path);
492
+ } finally {
493
+ await rm(temporary, { force: true });
494
+ }
495
+ }
496
+
497
+ function statusMarkdown(catalog, ledger) {
498
+ const counts = Object.fromEntries(
499
+ [...statuses].map((status) => [
500
+ status, ledger.cases.filter((entry) => entry.status === status).length
501
+ ])
502
+ );
503
+ const rows = ledger.cases
504
+ .slice()
505
+ .sort((left, right) => left.order - right.order)
506
+ .map((entry) => {
507
+ const marker = entry.status === "verified"
508
+ ? "x"
509
+ : entry.status === "archived" ? "-" : " ";
510
+ const flow = entry.plannedBaseFlow ?? "";
511
+ const next = entry.nextAction?.replaceAll("|", "\\|") ?? "";
512
+ return `| [${marker}] | ${entry.id} | ${entry.title.replaceAll("|", "\\|")} | ${entry.status} | ${flow} | ${next} |`;
513
+ });
514
+ return [
515
+ `# ${catalog.title}`,
516
+ "",
517
+ `Suite: \`${catalog.suiteId}\` `,
518
+ `Ledger revision: ${String(ledger.revision)} `,
519
+ `Updated: ${ledger.updatedAt}`,
520
+ "",
521
+ `Verified: ${String(counts.verified)} / ${String(ledger.cases.length)}`,
522
+ "",
523
+ "| Done | Case | Title | Status | Base Flow | Next action |",
524
+ "|---|---|---|---|---|---|",
525
+ ...rows,
526
+ "",
527
+ "> Generated from case-ledger.json. Do not edit this file manually.",
528
+ ""
529
+ ].join("\n");
530
+ }
531
+
532
+ async function withLock(suite, callback) {
533
+ const lockPath = join(suite.root, LOCK);
534
+ let handle;
535
+ try {
536
+ handle = await open(lockPath, "wx");
537
+ await handle.writeFile(JSON.stringify({
538
+ pid: process.pid,
539
+ createdAt: new Date().toISOString()
540
+ }));
541
+ } catch {
542
+ fail("CASE_SUITE_LOCKED", "Another Case Suite update holds the Ledger lock");
543
+ }
544
+ try {
545
+ const fresh = await loadSuite(suite.root);
546
+ return await callback(fresh);
547
+ } finally {
548
+ await handle?.close().catch(() => undefined);
549
+ await rm(lockPath, { force: true });
550
+ }
551
+ }
552
+
553
+ async function recoverLock(suitePath) {
554
+ const suite = await loadSuite(suitePath);
555
+ const lockPath = join(suite.root, LOCK);
556
+ let lock;
557
+ try {
558
+ lock = await json(lockPath, "Ledger lock");
559
+ } catch (error) {
560
+ if (error?.code === "CASE_SUITE_INVALID") {
561
+ fail("CASE_SUITE_LOCK_INVALID", "Ledger lock is missing or unreadable");
562
+ }
563
+ throw error;
564
+ }
565
+ if (!Number.isInteger(lock.pid) || lock.pid <= 0
566
+ || typeof lock.createdAt !== "string") {
567
+ fail("CASE_SUITE_LOCK_INVALID", "Ledger lock metadata is invalid");
568
+ }
569
+ let alive = true;
570
+ try {
571
+ process.kill(lock.pid, 0);
572
+ } catch (error) {
573
+ alive = error?.code !== "ESRCH";
574
+ }
575
+ if (alive) {
576
+ fail("CASE_SUITE_LOCKED", `Ledger lock owner PID ${String(lock.pid)} is alive`);
577
+ }
578
+ await rm(lockPath);
579
+ return {
580
+ status: "lockRecovered",
581
+ suiteId: suite.ledger.suiteId,
582
+ previousPid: lock.pid
583
+ };
584
+ }
585
+
586
+ async function persist(suite, ledger) {
587
+ parseLedger(ledger, suite.catalog);
588
+ await atomicWrite(
589
+ suite.ledgerPath,
590
+ `${JSON.stringify(ledger, null, 2)}\n`
591
+ );
592
+ await atomicWrite(
593
+ join(suite.root, STATUS),
594
+ statusMarkdown(suite.catalog, ledger)
595
+ );
596
+ }
597
+
598
+ function parseSuiteInput(value) {
599
+ exactKeys(
600
+ value,
601
+ ["version", "suiteId", "title", "projectRoot", "cases"],
602
+ ["deviceSerial", "contextPath"]
603
+ );
604
+ if (value.version !== 1 || !suiteIdPattern.test(value.suiteId ?? "")
605
+ || !isAbsolute(value.projectRoot ?? "")
606
+ || !Array.isArray(value.cases) || value.cases.length === 0) {
607
+ fail("CASE_SUITE_INVALID", "Invalid Suite initialization input");
608
+ }
609
+ const cases = value.cases.map(parseCatalogCase).sort(
610
+ (left, right) => left.order - right.order
611
+ );
612
+ assertCatalogGraph(cases);
613
+ return {
614
+ version: 1,
615
+ suiteId: value.suiteId,
616
+ title: nonempty(value.title, "title", 200),
617
+ projectRoot: value.projectRoot,
618
+ ...(value.deviceSerial === undefined
619
+ ? {}
620
+ : { deviceSerial: nonempty(value.deviceSerial, "deviceSerial", 200) }),
621
+ contextPath: projectPath(
622
+ value.contextPath ?? ".taphound/context/project-context.json",
623
+ "contextPath"
624
+ ),
625
+ cases
626
+ };
627
+ }
628
+
629
+ async function init(inputPath, outputPath) {
630
+ const input = parseSuiteInput(await json(resolve(inputPath), "Suite input"));
631
+ const projectRoot = await realpath(input.projectRoot);
632
+ const output = resolve(outputPath);
633
+ if (!inside(projectRoot, output) || output === projectRoot) {
634
+ fail("CASE_SUITE_INVALID", "Suite output must be a new directory inside projectRoot");
635
+ }
636
+ await access(output).then(
637
+ () => fail("CASE_SUITE_EXISTS", "Suite output already exists"),
638
+ () => undefined
639
+ );
640
+ await mkdir(dirname(output), { recursive: true });
641
+ await mkdir(output);
642
+ const now = new Date().toISOString();
643
+ const catalog = {
644
+ version: 1,
645
+ kind: "taphound.caseCatalog",
646
+ suiteId: input.suiteId,
647
+ title: input.title,
648
+ projectRoot,
649
+ ...(input.deviceSerial === undefined ? {} : { deviceSerial: input.deviceSerial }),
650
+ contextPath: input.contextPath,
651
+ createdAt: now,
652
+ cases: input.cases
653
+ };
654
+ const catalogText = `${JSON.stringify(catalog, null, 2)}\n`;
655
+ const ledger = {
656
+ version: 1,
657
+ kind: "taphound.caseLedger",
658
+ suiteId: input.suiteId,
659
+ revision: 0,
660
+ catalog: { path: CATALOG, sha256: digest(catalogText) },
661
+ devicePolicy: {
662
+ concurrency: "serial",
663
+ ...(input.deviceSerial === undefined ? {} : { deviceSerial: input.deviceSerial })
664
+ },
665
+ baseFlows: [],
666
+ cases: input.cases.map((entry) => ({
667
+ id: entry.id,
668
+ order: entry.order,
669
+ title: entry.title,
670
+ sourceSha256: digest(entry.sourceText),
671
+ risk: entry.risk,
672
+ dependsOn: entry.dependsOn,
673
+ ...(entry.plannedBaseFlow === undefined
674
+ ? {}
675
+ : { plannedBaseFlow: entry.plannedBaseFlow }),
676
+ status: "pending",
677
+ history: [{
678
+ to: "pending",
679
+ at: now,
680
+ reason: "Suite initialized from frozen Case catalog"
681
+ }]
682
+ })),
683
+ updatedAt: now
684
+ };
685
+ try {
686
+ await writeFile(join(output, CATALOG), catalogText, { flag: "wx" });
687
+ await writeFile(
688
+ join(output, LEDGER), `${JSON.stringify(ledger, null, 2)}\n`, { flag: "wx" }
689
+ );
690
+ await writeFile(join(output, STATUS), statusMarkdown(catalog, ledger), {
691
+ flag: "wx"
692
+ });
693
+ } catch (error) {
694
+ await rm(output, { recursive: true, force: true });
695
+ throw error;
696
+ }
697
+ return {
698
+ status: "initialized",
699
+ suiteId: input.suiteId,
700
+ suitePath: output,
701
+ catalogSha256: ledger.catalog.sha256,
702
+ revision: 0,
703
+ caseCount: ledger.cases.length
704
+ };
705
+ }
706
+
707
+ function parseTransition(value) {
708
+ exactKeys(
709
+ value,
710
+ ["version", "expectedRevision", "caseId", "from", "to", "reason"],
711
+ ["brief", "generation", "failure", "nextAction", "completion"]
712
+ );
713
+ if (value.version !== 1 || !caseIdPattern.test(value.caseId ?? "")
714
+ || !statuses.has(value.from) || !statuses.has(value.to)) {
715
+ fail("CASE_SUITE_INVALID", "Invalid Case transition identity");
716
+ }
717
+ return {
718
+ expectedRevision: integer(value.expectedRevision, "expectedRevision"),
719
+ caseId: value.caseId,
720
+ from: value.from,
721
+ to: value.to,
722
+ reason: nonempty(value.reason, "reason", 500),
723
+ ...(value.brief === undefined
724
+ ? {}
725
+ : { brief: parseArtifact(value.brief, "brief", { allowCompute: true }) }),
726
+ ...(value.generation === undefined
727
+ ? {}
728
+ : { generation: parseGeneration(value.generation) }),
729
+ ...(value.failure === undefined
730
+ ? {}
731
+ : { failure: parseFailure(value.failure) }),
732
+ ...(value.nextAction === undefined
733
+ ? {}
734
+ : { nextAction: nonempty(value.nextAction, "nextAction", 1000) }),
735
+ ...(value.completion === undefined
736
+ ? {}
737
+ : { completion: parseCompletion(value.completion, { allowCompute: true }) })
738
+ };
739
+ }
740
+
741
+ async function validateCompletion(suite, entry, completion) {
742
+ const journeyFile = await regularProjectFile(
743
+ suite.projectRoot, completion.journey, "completion.journey"
744
+ );
745
+ const metaFile = await regularProjectFile(
746
+ suite.projectRoot, completion.meta, "completion.meta"
747
+ );
748
+ const finalFile = await regularProjectFile(
749
+ suite.projectRoot, completion.finalReport, "completion.finalReport"
750
+ );
751
+ const independentFile = await regularProjectFile(
752
+ suite.projectRoot, completion.independentReport, "completion.independentReport"
753
+ );
754
+ const [journey, meta, finalReport, independentReport] = await Promise.all([
755
+ json(journeyFile.absolute, "Journey"),
756
+ json(metaFile.absolute, "Generation meta"),
757
+ json(finalFile.absolute, "Finalization report"),
758
+ json(independentFile.absolute, "Independent report")
759
+ ]);
760
+ const journeySha256 = canonicalHash(journey);
761
+ if (journey?.version !== 2 || !Array.isArray(journey.steps) || journey.steps.length === 0
762
+ || meta?.version !== 1 || !["verified", "promoted"].includes(meta.status)
763
+ || meta.generationId !== entry.generation?.id
764
+ || meta.journeyPath !== completion.journey.path
765
+ || meta.journeySha256 !== journeySha256
766
+ || meta.replayPolicy?.generatedReplayPolicy !== true
767
+ || meta.replayPolicy?.requireFocusedInput !== true
768
+ || meta.verification?.runs !== 1
769
+ || meta.verification?.reportSha256 !== completion.finalReport.sha256
770
+ || finalReport?.schemaVersion !== 4 || finalReport.status !== "passed"
771
+ || independentReport?.schemaVersion !== 4 || independentReport.status !== "passed"
772
+ || finalReport.journey?.sha256 !== journeySha256
773
+ || independentReport.journey?.sha256 !== journeySha256
774
+ || typeof finalReport.runId !== "string"
775
+ || typeof independentReport.runId !== "string"
776
+ || meta.verification?.runId !== finalReport.runId
777
+ || finalReport.runId === independentReport.runId
778
+ || finalReport.fallbackUsed === true || independentReport.fallbackUsed === true) {
779
+ fail(
780
+ "CASE_SUITE_EVIDENCE_INVALID",
781
+ `Completion evidence does not prove Case ${entry.id}`
782
+ );
783
+ }
784
+ }
785
+
786
+ async function transition(suitePath, inputPath) {
787
+ const request = parseTransition(await json(resolve(inputPath), "Transition input"));
788
+ const loaded = await loadSuite(suitePath);
789
+ return withLock(loaded, async (suite) => {
790
+ if (suite.ledger.revision !== request.expectedRevision) {
791
+ fail(
792
+ "CASE_SUITE_REVISION_CONFLICT",
793
+ `Expected revision ${String(request.expectedRevision)}, found ${String(suite.ledger.revision)}`
794
+ );
795
+ }
796
+ const entry = suite.ledger.cases.find((item) => item.id === request.caseId);
797
+ if (entry === undefined || entry.status !== request.from) {
798
+ fail("CASE_SUITE_CONFLICT", "Case status does not match transition.from");
799
+ }
800
+ if (!transitions.get(request.from)?.has(request.to)) {
801
+ fail(
802
+ "CASE_SUITE_TRANSITION_INVALID",
803
+ `Transition ${request.from} -> ${request.to} is not allowed`
804
+ );
805
+ }
806
+ if (request.from === "blocked"
807
+ && !["archived", "deferred"].includes(request.to)
808
+ && request.to !== entry.resumeStatus) {
809
+ fail("CASE_SUITE_TRANSITION_INVALID", "Blocked Case must resume its prior state");
810
+ }
811
+ if (request.from === "recoveryRequired"
812
+ && !["archived", "blocked", "deferred"].includes(request.to)
813
+ && request.to !== entry.resumeStatus) {
814
+ fail(
815
+ "CASE_SUITE_TRANSITION_INVALID",
816
+ "Recovery must return to the interrupted Case state"
817
+ );
818
+ }
819
+ if (request.from === "deferred" && request.to !== "archived"
820
+ && request.to !== entry.resumeStatus) {
821
+ fail(
822
+ "CASE_SUITE_TRANSITION_INVALID",
823
+ "Deferred Case must resume its prior state"
824
+ );
825
+ }
826
+ const resumingDeferredActive = request.from === "deferred"
827
+ && !inactiveStates.has(request.to);
828
+ if (request.to === "briefing" || resumingDeferredActive) {
829
+ const incomplete = entry.dependsOn.filter((id) => (
830
+ suite.ledger.cases.find((item) => item.id === id)?.status !== "verified"
831
+ ));
832
+ if (incomplete.length > 0) {
833
+ fail(
834
+ "CASE_SUITE_DEPENDENCY_BLOCKED",
835
+ `Case dependencies are not verified: ${incomplete.join(", ")}`
836
+ );
837
+ }
838
+ const active = suite.ledger.cases.find((item) => (
839
+ item.id !== entry.id && !inactiveStates.has(item.status)
840
+ ));
841
+ if (active !== undefined) {
842
+ fail("CASE_SUITE_CONFLICT", `Case ${active.id} is already active`);
843
+ }
844
+ }
845
+ if (request.brief !== undefined) {
846
+ request.brief = await computeArtifactHash(
847
+ suite.projectRoot, request.brief, `${entry.id}.brief`
848
+ );
849
+ }
850
+ if (request.completion !== undefined) {
851
+ request.completion = await computeCompletionHashes(
852
+ suite.projectRoot, request.completion
853
+ );
854
+ }
855
+ const brief = request.brief ?? entry.brief;
856
+ const generation = request.generation ?? entry.generation;
857
+ if (request.to === "briefReady") {
858
+ if (request.brief === undefined) {
859
+ fail("CASE_SUITE_INVALID", "briefReady requires a bound Brief");
860
+ }
861
+ const briefFile = await regularProjectFile(
862
+ suite.projectRoot, request.brief, `${entry.id}.brief`
863
+ );
864
+ if (!briefFile.relativePath.endsWith(
865
+ `/briefs/${entry.id}/taphound-journey-brief.md`
866
+ )) {
867
+ fail("CASE_SUITE_INVALID", "Brief path must be Case-specific");
868
+ }
869
+ }
870
+ if (request.to === "generating") {
871
+ if (brief === undefined || generation === undefined) {
872
+ fail("CASE_SUITE_INVALID", "generating requires Brief and generation ID");
873
+ }
874
+ await regularProjectFile(suite.projectRoot, brief, `${entry.id}.brief`);
875
+ if (entry.plannedBaseFlow !== undefined) {
876
+ if (generation.baseFlow !== entry.plannedBaseFlow
877
+ || !suite.ledger.baseFlows.some(
878
+ (flow) => flow.name === generation.baseFlow && flow.status === "verified"
879
+ )) {
880
+ fail(
881
+ "CASE_SUITE_FLOW_UNVERIFIED",
882
+ `Case ${entry.id} requires verified Base Flow ${entry.plannedBaseFlow}`
883
+ );
884
+ }
885
+ } else if (generation.baseFlow !== undefined
886
+ && !suite.ledger.baseFlows.some(
887
+ (flow) => flow.name === generation.baseFlow && flow.status === "verified"
888
+ )) {
889
+ fail("CASE_SUITE_FLOW_UNVERIFIED", "Generation Base Flow is not verified");
890
+ }
891
+ }
892
+ const failure = request.failure ?? entry.failure;
893
+ const nextAction = request.nextAction ?? entry.nextAction;
894
+ if ([
895
+ "recoveryRequired", "verificationFailed", "blocked", "deferred"
896
+ ].includes(request.to)
897
+ && (failure === undefined || nextAction === undefined)) {
898
+ fail("CASE_SUITE_INVALID", `${request.to} requires failure and nextAction`);
899
+ }
900
+ if (["verificationPending", "verificationFailed", "recoveryRequired"].includes(
901
+ request.to
902
+ ) && generation === undefined) {
903
+ fail("CASE_SUITE_INVALID", `${request.to} requires a generation session`);
904
+ }
905
+ if (request.to === "verified") {
906
+ if (request.completion === undefined || generation === undefined) {
907
+ fail("CASE_SUITE_INVALID", "verified requires completion and generation");
908
+ }
909
+ await validateCompletion(suite, { ...entry, generation }, request.completion);
910
+ }
911
+ const now = new Date().toISOString();
912
+ const next = {
913
+ ...entry,
914
+ status: request.to,
915
+ ...(brief === undefined ? {} : { brief }),
916
+ ...(generation === undefined ? {} : { generation }),
917
+ ...(failure === undefined ? {} : { failure }),
918
+ ...(nextAction === undefined ? {} : { nextAction }),
919
+ ...(request.completion === undefined
920
+ ? {}
921
+ : { completion: request.completion }),
922
+ history: [...entry.history, {
923
+ from: request.from,
924
+ to: request.to,
925
+ at: now,
926
+ reason: request.reason
927
+ }]
928
+ };
929
+ if (["blocked", "recoveryRequired", "deferred"].includes(request.to)) {
930
+ next.resumeStatus = ["blocked", "recoveryRequired"].includes(request.from)
931
+ ? entry.resumeStatus
932
+ : request.from;
933
+ } else {
934
+ delete next.resumeStatus;
935
+ }
936
+ if (![
937
+ "blocked", "recoveryRequired", "verificationFailed", "deferred"
938
+ ].includes(request.to)) {
939
+ if (request.failure === undefined) delete next.failure;
940
+ if (request.nextAction === undefined) delete next.nextAction;
941
+ }
942
+ const ledger = {
943
+ ...suite.ledger,
944
+ revision: suite.ledger.revision + 1,
945
+ cases: suite.ledger.cases.map((item) => item.id === next.id ? next : item),
946
+ updatedAt: now
947
+ };
948
+ await persist(suite, ledger);
949
+ return {
950
+ status: "transitioned",
951
+ suiteId: ledger.suiteId,
952
+ revision: ledger.revision,
953
+ caseId: next.id,
954
+ from: request.from,
955
+ to: request.to
956
+ };
957
+ });
958
+ }
959
+
960
+ function parseFlowRecord(value) {
961
+ exactKeys(value, [
962
+ "version", "expectedRevision", "name", "path", "sha256",
963
+ "exitActivity", "journey", "resolutionManifest", "report"
964
+ ]);
965
+ if (value.version !== 1 || !flowNamePattern.test(value.name ?? "")) {
966
+ fail("CASE_SUITE_INVALID", "Invalid Base Flow record");
967
+ }
968
+ return {
969
+ expectedRevision: integer(value.expectedRevision, "expectedRevision"),
970
+ name: value.name,
971
+ path: projectPath(value.path, "flow.path"),
972
+ sha256: value.sha256,
973
+ exitActivity: nonempty(value.exitActivity, "exitActivity", 500),
974
+ journey: parseArtifact(value.journey, "flow.journey"),
975
+ resolutionManifest: parseArtifact(
976
+ value.resolutionManifest, "flow.resolutionManifest"
977
+ ),
978
+ report: parseArtifact(value.report, "flow.report")
979
+ };
980
+ }
981
+
982
+ async function recordFlow(suitePath, inputPath) {
983
+ const request = parseFlowRecord(await json(resolve(inputPath), "Base Flow record"));
984
+ assertSha(request.sha256, "flow.sha256");
985
+ const loaded = await loadSuite(suitePath);
986
+ return withLock(loaded, async (suite) => {
987
+ if (suite.ledger.revision !== request.expectedRevision) {
988
+ fail("CASE_SUITE_REVISION_CONFLICT", "Base Flow revision is stale");
989
+ }
990
+ if (suite.ledger.cases.some((entry) => !inactiveStates.has(entry.status))) {
991
+ fail("CASE_SUITE_CONFLICT", "Record Base Flows only between active Cases");
992
+ }
993
+ if (suite.ledger.baseFlows.some((flow) => flow.name === request.name)) {
994
+ fail("CASE_SUITE_CONFLICT", `Base Flow ${request.name} is already recorded`);
995
+ }
996
+ const now = new Date().toISOString();
997
+ const recorded = {
998
+ name: request.name,
999
+ status: "verified",
1000
+ path: request.path,
1001
+ sha256: request.sha256,
1002
+ exitActivity: request.exitActivity,
1003
+ journey: request.journey,
1004
+ resolutionManifest: request.resolutionManifest,
1005
+ report: request.report,
1006
+ recordedAt: now
1007
+ };
1008
+ await validateFlowEvidence(suite, recorded);
1009
+ const ledger = {
1010
+ ...suite.ledger,
1011
+ revision: suite.ledger.revision + 1,
1012
+ baseFlows: [...suite.ledger.baseFlows, recorded].sort(
1013
+ (left, right) => left.name.localeCompare(right.name)
1014
+ ),
1015
+ updatedAt: now
1016
+ };
1017
+ await persist(suite, ledger);
1018
+ return {
1019
+ status: "recorded",
1020
+ suiteId: ledger.suiteId,
1021
+ revision: ledger.revision,
1022
+ baseFlow: request.name
1023
+ };
1024
+ });
1025
+ }
1026
+
1027
+ async function validateFlowEvidence(suite, flowRecord) {
1028
+ const flowArtifact = {
1029
+ path: flowRecord.path,
1030
+ sha256: flowRecord.sha256
1031
+ };
1032
+ const flowFile = await regularProjectFile(
1033
+ suite.projectRoot, flowArtifact, "Base Flow"
1034
+ );
1035
+ const journeyFile = await regularProjectFile(
1036
+ suite.projectRoot, flowRecord.journey, "Base Flow Journey"
1037
+ );
1038
+ const manifestFile = await regularProjectFile(
1039
+ suite.projectRoot,
1040
+ flowRecord.resolutionManifest,
1041
+ "Base Flow resolution manifest"
1042
+ );
1043
+ const reportFile = await regularProjectFile(
1044
+ suite.projectRoot, flowRecord.report, "Base Flow report"
1045
+ );
1046
+ if (flowRecord.path !== `.taphound/flows/${flowRecord.name}.json`) {
1047
+ fail("CASE_SUITE_INVALID", "Base Flow path does not match its name");
1048
+ }
1049
+ const [flow, journey, manifest, report] = await Promise.all([
1050
+ json(flowFile.absolute, "Base Flow"),
1051
+ json(journeyFile.absolute, "Base Flow Journey"),
1052
+ json(manifestFile.absolute, "Base Flow resolution manifest"),
1053
+ json(reportFile.absolute, "Base Flow report")
1054
+ ]);
1055
+ const journeySha256 = canonicalHash(journey);
1056
+ const dependency = manifest?.flows?.find(
1057
+ (item) => item.name === flowRecord.name
1058
+ );
1059
+ const {
1060
+ resolutionSha256: recordedResolutionSha256,
1061
+ ...unsignedManifest
1062
+ } = manifest ?? {};
1063
+ if (flow?.version !== 1 || flow.kind !== "flow"
1064
+ || flow.name !== flowRecord.name
1065
+ || journey?.version !== 2 || !Array.isArray(journey.steps)
1066
+ || journey.steps.length === 0
1067
+ || journey.steps.at(-1)?.activity?.after !== flowRecord.exitActivity
1068
+ || manifest?.version !== 1 || dependency?.path !== flowRecord.path
1069
+ || dependency?.sha256 !== flowRecord.sha256
1070
+ || !Array.isArray(manifest.expansion)
1071
+ || manifest.expansion.length !== manifest.flows?.length
1072
+ || manifest.flows.some(
1073
+ (item, index) => item.name !== manifest.expansion[index]
1074
+ )
1075
+ || manifest.journey?.sha256 !== journeySha256
1076
+ || recordedResolutionSha256 !== canonicalHash(unsignedManifest)
1077
+ || report?.schemaVersion !== 4 || report.status !== "passed"
1078
+ || report.journey?.sha256 !== journeySha256
1079
+ || report.fallbackUsed === true) {
1080
+ fail("CASE_SUITE_EVIDENCE_INVALID", "Base Flow proof is not hash-bound and passed");
1081
+ }
1082
+ }
1083
+
1084
+ function summary(suite) {
1085
+ const counts = Object.fromEntries(
1086
+ [...statuses].map((status) => [
1087
+ status, suite.ledger.cases.filter((entry) => entry.status === status).length
1088
+ ])
1089
+ );
1090
+ const current = suite.ledger.cases.find(
1091
+ (entry) => !inactiveStates.has(entry.status)
1092
+ );
1093
+ const next = current === undefined
1094
+ ? suite.ledger.cases
1095
+ .slice()
1096
+ .sort((left, right) => left.order - right.order)
1097
+ .find((entry) => entry.status === "pending" && entry.dependsOn.every(
1098
+ (id) => suite.ledger.cases.find((item) => item.id === id)?.status === "verified"
1099
+ ))
1100
+ : undefined;
1101
+ return {
1102
+ status: "valid",
1103
+ suiteId: suite.ledger.suiteId,
1104
+ revision: suite.ledger.revision,
1105
+ catalogSha256: suite.catalogSha256,
1106
+ counts,
1107
+ ...(current === undefined ? {} : {
1108
+ currentCase: {
1109
+ id: current.id,
1110
+ status: current.status,
1111
+ ...(current.generation === undefined
1112
+ ? {}
1113
+ : { generationId: current.generation.id }),
1114
+ ...(current.nextAction === undefined
1115
+ ? {}
1116
+ : { nextAction: current.nextAction })
1117
+ }
1118
+ }),
1119
+ ...(next === undefined ? {} : { nextCase: { id: next.id, status: next.status } }),
1120
+ complete: suite.ledger.cases.every(
1121
+ (entry) => terminalStates.has(entry.status)
1122
+ )
1123
+ };
1124
+ }
1125
+
1126
+ function caseDetail(suite, caseId) {
1127
+ const entry = suite.ledger.cases.find((item) => item.id === caseId);
1128
+ if (entry === undefined) {
1129
+ fail("CASE_SUITE_CASE_NOT_FOUND", `Case ${caseId} does not exist`);
1130
+ }
1131
+ return {
1132
+ status: "valid",
1133
+ suiteId: suite.ledger.suiteId,
1134
+ revision: suite.ledger.revision,
1135
+ case: entry
1136
+ };
1137
+ }
1138
+
1139
+ function options(argv, names) {
1140
+ const result = {};
1141
+ for (let index = 0; index < argv.length; index += 2) {
1142
+ const name = argv[index];
1143
+ const value = argv[index + 1];
1144
+ if (!names.includes(name) || value === undefined) {
1145
+ fail("CASE_SUITE_USAGE", `Invalid option ${String(name)}`);
1146
+ }
1147
+ result[name.slice(2)] = value;
1148
+ }
1149
+ return result;
1150
+ }
1151
+
1152
+ function help() {
1153
+ return [
1154
+ "TapHound Case Suite Ledger",
1155
+ "",
1156
+ "Commands:",
1157
+ " init --input <json> --out <suite-directory>",
1158
+ " validate --suite <suite-directory>",
1159
+ " status --suite <suite-directory> [--case <case-id>]",
1160
+ " transition --suite <suite-directory> --input <json>",
1161
+ " record-flow --suite <suite-directory> --input <json>",
1162
+ " recover-lock --suite <suite-directory>"
1163
+ ].join("\n");
1164
+ }
1165
+
1166
+ async function main() {
1167
+ const [command, ...argv] = process.argv.slice(2);
1168
+ if (command === undefined || command === "help" || command === "--help") {
1169
+ process.stdout.write(`${help()}\n`);
1170
+ return;
1171
+ }
1172
+ let output;
1173
+ if (command === "init") {
1174
+ const input = options(argv, ["--input", "--out"]);
1175
+ output = await init(input.input, input.out);
1176
+ } else if (command === "validate") {
1177
+ const input = options(argv, ["--suite"]);
1178
+ output = summary(await loadSuite(input.suite));
1179
+ } else if (command === "status") {
1180
+ const input = options(argv, ["--suite", "--case"]);
1181
+ const suite = await loadSuite(input.suite);
1182
+ output = input.case === undefined
1183
+ ? summary(suite)
1184
+ : caseDetail(suite, input.case);
1185
+ } else if (command === "transition") {
1186
+ const input = options(argv, ["--suite", "--input"]);
1187
+ output = await transition(input.suite, input.input);
1188
+ } else if (command === "record-flow") {
1189
+ const input = options(argv, ["--suite", "--input"]);
1190
+ output = await recordFlow(input.suite, input.input);
1191
+ } else if (command === "recover-lock") {
1192
+ const input = options(argv, ["--suite"]);
1193
+ output = await recoverLock(input.suite);
1194
+ } else {
1195
+ fail("CASE_SUITE_USAGE", `Unknown command ${command}`);
1196
+ }
1197
+ process.stdout.write(`${JSON.stringify(output)}\n`);
1198
+ }
1199
+
1200
+ main().catch((error) => {
1201
+ process.stdout.write(`${JSON.stringify({
1202
+ status: "error",
1203
+ code: error?.code ?? "CASE_SUITE_INTERNAL",
1204
+ message: error instanceof Error ? error.message : String(error)
1205
+ })}\n`);
1206
+ process.exitCode = 2;
1207
+ });