wingfoil 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. package/README.md +88 -42
  2. package/dist/agent/discovery.d.ts +72 -0
  3. package/dist/agent/discovery.d.ts.map +1 -0
  4. package/dist/agent/discovery.js +210 -0
  5. package/dist/agent/discovery.js.map +1 -0
  6. package/dist/agent/execute.d.ts +206 -0
  7. package/dist/agent/execute.d.ts.map +1 -0
  8. package/dist/agent/execute.js +581 -0
  9. package/dist/agent/execute.js.map +1 -0
  10. package/dist/agent/index.d.ts +23 -0
  11. package/dist/agent/index.d.ts.map +1 -0
  12. package/dist/agent/index.js +85 -0
  13. package/dist/agent/index.js.map +1 -0
  14. package/dist/agent/launch.d.ts +133 -0
  15. package/dist/agent/launch.d.ts.map +1 -0
  16. package/dist/agent/launch.js +380 -0
  17. package/dist/agent/launch.js.map +1 -0
  18. package/dist/agent/manifest.d.ts +30 -0
  19. package/dist/agent/manifest.d.ts.map +1 -0
  20. package/dist/agent/manifest.js +93 -0
  21. package/dist/agent/manifest.js.map +1 -0
  22. package/dist/agent/placeholders.d.ts +49 -0
  23. package/dist/agent/placeholders.d.ts.map +1 -0
  24. package/dist/agent/placeholders.js +177 -0
  25. package/dist/agent/placeholders.js.map +1 -0
  26. package/dist/agent/run-log.d.ts +254 -0
  27. package/dist/agent/run-log.d.ts.map +1 -0
  28. package/dist/agent/run-log.js +612 -0
  29. package/dist/agent/run-log.js.map +1 -0
  30. package/dist/agent/schema.d.ts +100 -0
  31. package/dist/agent/schema.d.ts.map +1 -0
  32. package/dist/agent/schema.js +89 -0
  33. package/dist/agent/schema.js.map +1 -0
  34. package/dist/agent/step.d.ts +41 -0
  35. package/dist/agent/step.d.ts.map +1 -0
  36. package/dist/agent/step.js +95 -0
  37. package/dist/agent/step.js.map +1 -0
  38. package/dist/build-info.json +4 -0
  39. package/dist/cli/error.d.ts +11 -2
  40. package/dist/cli/error.d.ts.map +1 -1
  41. package/dist/cli/error.js +27 -6
  42. package/dist/cli/error.js.map +1 -1
  43. package/dist/cli/exit.d.ts +33 -3
  44. package/dist/cli/exit.d.ts.map +1 -1
  45. package/dist/cli/exit.js +54 -3
  46. package/dist/cli/exit.js.map +1 -1
  47. package/dist/cli/index.d.ts +2 -2
  48. package/dist/cli/index.d.ts.map +1 -1
  49. package/dist/cli/index.js +3 -2
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/cli/init-command.d.ts.map +1 -1
  52. package/dist/cli/init-command.js +6 -3
  53. package/dist/cli/init-command.js.map +1 -1
  54. package/dist/cli/mcp-command.d.ts +13 -4
  55. package/dist/cli/mcp-command.d.ts.map +1 -1
  56. package/dist/cli/mcp-command.js +25 -8
  57. package/dist/cli/mcp-command.js.map +1 -1
  58. package/dist/cli/output.d.ts +12 -4
  59. package/dist/cli/output.d.ts.map +1 -1
  60. package/dist/cli/output.js +15 -4
  61. package/dist/cli/output.js.map +1 -1
  62. package/dist/cli/program.d.ts +1 -1
  63. package/dist/cli/program.d.ts.map +1 -1
  64. package/dist/cli/program.js +270 -60
  65. package/dist/cli/program.js.map +1 -1
  66. package/dist/cli/registrar.d.ts +16 -7
  67. package/dist/cli/registrar.d.ts.map +1 -1
  68. package/dist/cli/registrar.js +71 -6
  69. package/dist/cli/registrar.js.map +1 -1
  70. package/dist/cli/suggest.d.ts +20 -0
  71. package/dist/cli/suggest.d.ts.map +1 -0
  72. package/dist/cli/suggest.js +49 -0
  73. package/dist/cli/suggest.js.map +1 -0
  74. package/dist/cli/warning.d.ts +26 -0
  75. package/dist/cli/warning.d.ts.map +1 -0
  76. package/dist/cli/warning.js +66 -0
  77. package/dist/cli/warning.js.map +1 -0
  78. package/dist/cli.js +11 -2
  79. package/dist/cli.js.map +1 -1
  80. package/dist/core/agent-execute.d.ts +44 -0
  81. package/dist/core/agent-execute.d.ts.map +1 -0
  82. package/dist/core/agent-execute.js +71 -0
  83. package/dist/core/agent-execute.js.map +1 -0
  84. package/dist/core/agent-list.d.ts +95 -0
  85. package/dist/core/agent-list.d.ts.map +1 -0
  86. package/dist/core/agent-list.js +207 -0
  87. package/dist/core/agent-list.js.map +1 -0
  88. package/dist/core/agent-show.d.ts +44 -0
  89. package/dist/core/agent-show.d.ts.map +1 -0
  90. package/dist/core/agent-show.js +188 -0
  91. package/dist/core/agent-show.js.map +1 -0
  92. package/dist/core/approval-authority.d.ts +17 -14
  93. package/dist/core/approval-authority.d.ts.map +1 -1
  94. package/dist/core/approval-authority.js +9 -9
  95. package/dist/core/approval-authority.js.map +1 -1
  96. package/dist/core/builtin-asset.d.ts +18 -15
  97. package/dist/core/builtin-asset.d.ts.map +1 -1
  98. package/dist/core/builtin-asset.js +17 -14
  99. package/dist/core/builtin-asset.js.map +1 -1
  100. package/dist/core/builtin-integrity.d.ts +12 -2
  101. package/dist/core/builtin-integrity.d.ts.map +1 -1
  102. package/dist/core/builtin-integrity.js +85 -9
  103. package/dist/core/builtin-integrity.js.map +1 -1
  104. package/dist/core/confinement.d.ts +3 -1
  105. package/dist/core/confinement.d.ts.map +1 -1
  106. package/dist/core/confinement.js +7 -4
  107. package/dist/core/confinement.js.map +1 -1
  108. package/dist/core/context.d.ts +150 -98
  109. package/dist/core/context.d.ts.map +1 -1
  110. package/dist/core/context.js +602 -66
  111. package/dist/core/context.js.map +1 -1
  112. package/dist/core/directive-assign.d.ts +33 -7
  113. package/dist/core/directive-assign.d.ts.map +1 -1
  114. package/dist/core/directive-assign.js +67 -27
  115. package/dist/core/directive-assign.js.map +1 -1
  116. package/dist/core/directive-scope.d.ts +36 -0
  117. package/dist/core/directive-scope.d.ts.map +1 -0
  118. package/dist/core/directive-scope.js +34 -0
  119. package/dist/core/directive-scope.js.map +1 -0
  120. package/dist/core/directives-list.d.ts +13 -3
  121. package/dist/core/directives-list.d.ts.map +1 -1
  122. package/dist/core/directives-list.js +18 -6
  123. package/dist/core/directives-list.js.map +1 -1
  124. package/dist/core/dry-run.d.ts +33 -0
  125. package/dist/core/dry-run.d.ts.map +1 -0
  126. package/dist/core/dry-run.js +45 -0
  127. package/dist/core/dry-run.js.map +1 -0
  128. package/dist/core/element-ref.d.ts +23 -0
  129. package/dist/core/element-ref.d.ts.map +1 -0
  130. package/dist/core/element-ref.js +24 -0
  131. package/dist/core/element-ref.js.map +1 -0
  132. package/dist/core/error-details.d.ts +20 -0
  133. package/dist/core/error-details.d.ts.map +1 -0
  134. package/dist/core/error-details.js +65 -0
  135. package/dist/core/error-details.js.map +1 -0
  136. package/dist/core/exit-code.js +6 -6
  137. package/dist/core/git-identity.d.ts +46 -11
  138. package/dist/core/git-identity.d.ts.map +1 -1
  139. package/dist/core/git-identity.js +140 -27
  140. package/dist/core/git-identity.js.map +1 -1
  141. package/dist/core/index.d.ts +189 -14
  142. package/dist/core/index.d.ts.map +1 -1
  143. package/dist/core/index.js +1241 -238
  144. package/dist/core/index.js.map +1 -1
  145. package/dist/core/init.d.ts +50 -8
  146. package/dist/core/init.d.ts.map +1 -1
  147. package/dist/core/init.js +139 -27
  148. package/dist/core/init.js.map +1 -1
  149. package/dist/core/loaders.d.ts +170 -14
  150. package/dist/core/loaders.d.ts.map +1 -1
  151. package/dist/core/loaders.js +570 -89
  152. package/dist/core/loaders.js.map +1 -1
  153. package/dist/core/memory-add-link.d.ts +28 -0
  154. package/dist/core/memory-add-link.d.ts.map +1 -0
  155. package/dist/core/memory-add-link.js +67 -0
  156. package/dist/core/memory-add-link.js.map +1 -0
  157. package/dist/core/memory-add-type.d.ts +25 -1
  158. package/dist/core/memory-add-type.d.ts.map +1 -1
  159. package/dist/core/memory-add-type.js +59 -1
  160. package/dist/core/memory-add-type.js.map +1 -1
  161. package/dist/core/memory-amend.d.ts +80 -0
  162. package/dist/core/memory-amend.d.ts.map +1 -0
  163. package/dist/core/memory-amend.js +160 -0
  164. package/dist/core/memory-amend.js.map +1 -0
  165. package/dist/core/memory-supersede.d.ts +28 -0
  166. package/dist/core/memory-supersede.d.ts.map +1 -0
  167. package/dist/core/memory-supersede.js +79 -0
  168. package/dist/core/memory-supersede.js.map +1 -0
  169. package/dist/core/memory-transition.d.ts +190 -30
  170. package/dist/core/memory-transition.d.ts.map +1 -1
  171. package/dist/core/memory-transition.js +480 -59
  172. package/dist/core/memory-transition.js.map +1 -1
  173. package/dist/core/registry.d.ts +147 -23
  174. package/dist/core/registry.d.ts.map +1 -1
  175. package/dist/core/registry.js +50 -0
  176. package/dist/core/registry.js.map +1 -1
  177. package/dist/core/relevance.d.ts +74 -12
  178. package/dist/core/relevance.d.ts.map +1 -1
  179. package/dist/core/relevance.js +45 -27
  180. package/dist/core/relevance.js.map +1 -1
  181. package/dist/core/require-reason.d.ts.map +1 -1
  182. package/dist/core/require-reason.js +6 -5
  183. package/dist/core/require-reason.js.map +1 -1
  184. package/dist/core/required-fields.d.ts +28 -0
  185. package/dist/core/required-fields.d.ts.map +1 -0
  186. package/dist/core/required-fields.js +60 -0
  187. package/dist/core/required-fields.js.map +1 -0
  188. package/dist/core/revision.d.ts +82 -0
  189. package/dist/core/revision.d.ts.map +1 -0
  190. package/dist/core/revision.js +144 -0
  191. package/dist/core/revision.js.map +1 -0
  192. package/dist/core/types.d.ts +28 -2
  193. package/dist/core/types.d.ts.map +1 -1
  194. package/dist/core/types.js +12 -3
  195. package/dist/core/types.js.map +1 -1
  196. package/dist/core/workflow-core-checks.d.ts +34 -0
  197. package/dist/core/workflow-core-checks.d.ts.map +1 -0
  198. package/dist/core/workflow-core-checks.js +403 -0
  199. package/dist/core/workflow-core-checks.js.map +1 -0
  200. package/dist/core/workflow-create-remove.d.ts +65 -0
  201. package/dist/core/workflow-create-remove.d.ts.map +1 -0
  202. package/dist/core/workflow-create-remove.js +277 -0
  203. package/dist/core/workflow-create-remove.js.map +1 -0
  204. package/dist/core/workflow-deduction.d.ts +48 -0
  205. package/dist/core/workflow-deduction.d.ts.map +1 -0
  206. package/dist/core/workflow-deduction.js +523 -0
  207. package/dist/core/workflow-deduction.js.map +1 -0
  208. package/dist/core/workflow-diagnostics.d.ts +83 -0
  209. package/dist/core/workflow-diagnostics.d.ts.map +1 -0
  210. package/dist/core/workflow-diagnostics.js +375 -0
  211. package/dist/core/workflow-diagnostics.js.map +1 -0
  212. package/dist/core/workflow-exit-state.d.ts +125 -0
  213. package/dist/core/workflow-exit-state.d.ts.map +1 -0
  214. package/dist/core/workflow-exit-state.js +243 -0
  215. package/dist/core/workflow-exit-state.js.map +1 -0
  216. package/dist/core/workflow-finalize.d.ts +37 -0
  217. package/dist/core/workflow-finalize.d.ts.map +1 -0
  218. package/dist/core/workflow-finalize.js +153 -0
  219. package/dist/core/workflow-finalize.js.map +1 -0
  220. package/dist/core/workflow-list-show.d.ts +176 -0
  221. package/dist/core/workflow-list-show.d.ts.map +1 -0
  222. package/dist/core/workflow-list-show.js +268 -0
  223. package/dist/core/workflow-list-show.js.map +1 -0
  224. package/dist/core/workflow-next.d.ts +268 -0
  225. package/dist/core/workflow-next.d.ts.map +1 -0
  226. package/dist/core/workflow-next.js +503 -0
  227. package/dist/core/workflow-next.js.map +1 -0
  228. package/dist/core/workflow-registry.d.ts +52 -0
  229. package/dist/core/workflow-registry.d.ts.map +1 -0
  230. package/dist/core/workflow-registry.js +158 -0
  231. package/dist/core/workflow-registry.js.map +1 -0
  232. package/dist/core/workflow-start-end.d.ts +71 -0
  233. package/dist/core/workflow-start-end.d.ts.map +1 -0
  234. package/dist/core/workflow-start-end.js +298 -0
  235. package/dist/core/workflow-start-end.js.map +1 -0
  236. package/dist/core/workflow-status.d.ts +83 -0
  237. package/dist/core/workflow-status.d.ts.map +1 -0
  238. package/dist/core/workflow-status.js +125 -0
  239. package/dist/core/workflow-status.js.map +1 -0
  240. package/dist/core/write-guard.d.ts +51 -8
  241. package/dist/core/write-guard.d.ts.map +1 -1
  242. package/dist/core/write-guard.js +139 -9
  243. package/dist/core/write-guard.js.map +1 -1
  244. package/dist/directives/index.d.ts +3 -2
  245. package/dist/directives/index.d.ts.map +1 -1
  246. package/dist/directives/index.js +5 -3
  247. package/dist/directives/index.js.map +1 -1
  248. package/dist/directives/schema.d.ts +35 -30
  249. package/dist/directives/schema.d.ts.map +1 -1
  250. package/dist/directives/schema.js +36 -30
  251. package/dist/directives/schema.js.map +1 -1
  252. package/dist/directives/version.d.ts +28 -0
  253. package/dist/directives/version.d.ts.map +1 -0
  254. package/dist/directives/version.js +51 -0
  255. package/dist/directives/version.js.map +1 -0
  256. package/dist/dna/edit.d.ts +7 -6
  257. package/dist/dna/edit.d.ts.map +1 -1
  258. package/dist/dna/edit.js +96 -41
  259. package/dist/dna/edit.js.map +1 -1
  260. package/dist/dna/mutate.js +18 -3
  261. package/dist/dna/mutate.js.map +1 -1
  262. package/dist/dna/path.d.ts +2 -2
  263. package/dist/dna/roles.d.ts +1 -1
  264. package/dist/dna/schema.d.ts +61 -3
  265. package/dist/dna/schema.d.ts.map +1 -1
  266. package/dist/dna/schema.js +121 -6
  267. package/dist/dna/schema.js.map +1 -1
  268. package/dist/mcp/dna-resource.d.ts.map +1 -1
  269. package/dist/mcp/dna-resource.js +4 -4
  270. package/dist/mcp/dna-resource.js.map +1 -1
  271. package/dist/mcp/index.d.ts +9 -5
  272. package/dist/mcp/index.d.ts.map +1 -1
  273. package/dist/mcp/index.js +13 -2
  274. package/dist/mcp/index.js.map +1 -1
  275. package/dist/mcp/memory-resource.d.ts.map +1 -1
  276. package/dist/mcp/memory-resource.js +30 -19
  277. package/dist/mcp/memory-resource.js.map +1 -1
  278. package/dist/mcp/prompt.d.ts +61 -24
  279. package/dist/mcp/prompt.d.ts.map +1 -1
  280. package/dist/mcp/prompt.js +176 -47
  281. package/dist/mcp/prompt.js.map +1 -1
  282. package/dist/mcp/read-only.d.ts +39 -0
  283. package/dist/mcp/read-only.d.ts.map +1 -1
  284. package/dist/mcp/read-only.js +61 -0
  285. package/dist/mcp/read-only.js.map +1 -1
  286. package/dist/mcp/registrar.d.ts +10 -0
  287. package/dist/mcp/registrar.d.ts.map +1 -1
  288. package/dist/mcp/registrar.js +48 -12
  289. package/dist/mcp/registrar.js.map +1 -1
  290. package/dist/mcp/server.d.ts +21 -14
  291. package/dist/mcp/server.d.ts.map +1 -1
  292. package/dist/mcp/server.js +41 -15
  293. package/dist/mcp/server.js.map +1 -1
  294. package/dist/mcp/session-resource.d.ts +13 -0
  295. package/dist/mcp/session-resource.d.ts.map +1 -0
  296. package/dist/mcp/session-resource.js +65 -0
  297. package/dist/mcp/session-resource.js.map +1 -0
  298. package/dist/mcp/workflow-resource.d.ts +18 -0
  299. package/dist/mcp/workflow-resource.d.ts.map +1 -1
  300. package/dist/mcp/workflow-resource.js +37 -8
  301. package/dist/mcp/workflow-resource.js.map +1 -1
  302. package/dist/memory/add.d.ts +160 -17
  303. package/dist/memory/add.d.ts.map +1 -1
  304. package/dist/memory/add.js +361 -51
  305. package/dist/memory/add.js.map +1 -1
  306. package/dist/memory/audit.d.ts +165 -32
  307. package/dist/memory/audit.d.ts.map +1 -1
  308. package/dist/memory/audit.js +267 -97
  309. package/dist/memory/audit.js.map +1 -1
  310. package/dist/memory/commit-message.d.ts +60 -38
  311. package/dist/memory/commit-message.d.ts.map +1 -1
  312. package/dist/memory/commit-message.js +167 -21
  313. package/dist/memory/commit-message.js.map +1 -1
  314. package/dist/memory/entry.d.ts.map +1 -1
  315. package/dist/memory/entry.js +1 -2
  316. package/dist/memory/entry.js.map +1 -1
  317. package/dist/memory/frontmatter-edit.d.ts +16 -4
  318. package/dist/memory/frontmatter-edit.d.ts.map +1 -1
  319. package/dist/memory/frontmatter-edit.js +13 -1
  320. package/dist/memory/frontmatter-edit.js.map +1 -1
  321. package/dist/memory/git-log.d.ts +18 -6
  322. package/dist/memory/git-log.d.ts.map +1 -1
  323. package/dist/memory/git-log.js +40 -13
  324. package/dist/memory/git-log.js.map +1 -1
  325. package/dist/memory/history.d.ts +74 -13
  326. package/dist/memory/history.d.ts.map +1 -1
  327. package/dist/memory/history.js +129 -45
  328. package/dist/memory/history.js.map +1 -1
  329. package/dist/memory/index.d.ts +13 -11
  330. package/dist/memory/index.d.ts.map +1 -1
  331. package/dist/memory/index.js +38 -3
  332. package/dist/memory/index.js.map +1 -1
  333. package/dist/memory/query.d.ts +161 -19
  334. package/dist/memory/query.d.ts.map +1 -1
  335. package/dist/memory/query.js +435 -55
  336. package/dist/memory/query.js.map +1 -1
  337. package/dist/memory/schema.d.ts +52 -3
  338. package/dist/memory/schema.d.ts.map +1 -1
  339. package/dist/memory/schema.js +132 -8
  340. package/dist/memory/schema.js.map +1 -1
  341. package/dist/memory/state-machine.d.ts +65 -10
  342. package/dist/memory/state-machine.d.ts.map +1 -1
  343. package/dist/memory/state-machine.js +136 -48
  344. package/dist/memory/state-machine.js.map +1 -1
  345. package/dist/memory/submit.d.ts +52 -3
  346. package/dist/memory/submit.d.ts.map +1 -1
  347. package/dist/memory/submit.js +118 -11
  348. package/dist/memory/submit.js.map +1 -1
  349. package/dist/storage/build-stamp.d.ts +18 -0
  350. package/dist/storage/build-stamp.d.ts.map +1 -0
  351. package/dist/storage/build-stamp.js +68 -0
  352. package/dist/storage/build-stamp.js.map +1 -0
  353. package/dist/storage/builtin-adapters.d.ts +32 -0
  354. package/dist/storage/builtin-adapters.d.ts.map +1 -0
  355. package/dist/storage/builtin-adapters.js +100 -0
  356. package/dist/storage/builtin-adapters.js.map +1 -0
  357. package/dist/storage/builtin-directives.d.ts +0 -29
  358. package/dist/storage/builtin-directives.d.ts.map +1 -1
  359. package/dist/storage/builtin-directives.js +9 -4
  360. package/dist/storage/builtin-directives.js.map +1 -1
  361. package/dist/storage/commit.d.ts +310 -3
  362. package/dist/storage/commit.d.ts.map +1 -1
  363. package/dist/storage/commit.js +807 -33
  364. package/dist/storage/commit.js.map +1 -1
  365. package/dist/storage/document.d.ts +6 -0
  366. package/dist/storage/document.d.ts.map +1 -1
  367. package/dist/storage/document.js +10 -1
  368. package/dist/storage/document.js.map +1 -1
  369. package/dist/storage/dry-run.d.ts +65 -0
  370. package/dist/storage/dry-run.d.ts.map +1 -0
  371. package/dist/storage/dry-run.js +150 -0
  372. package/dist/storage/dry-run.js.map +1 -0
  373. package/dist/storage/errors.d.ts +26 -0
  374. package/dist/storage/errors.d.ts.map +1 -1
  375. package/dist/storage/errors.js +27 -1
  376. package/dist/storage/errors.js.map +1 -1
  377. package/dist/storage/frontmatter.d.ts +1 -1
  378. package/dist/storage/frontmatter.js +1 -1
  379. package/dist/storage/git-read.d.ts +64 -0
  380. package/dist/storage/git-read.d.ts.map +1 -0
  381. package/dist/storage/git-read.js +107 -0
  382. package/dist/storage/git-read.js.map +1 -0
  383. package/dist/storage/index.d.ts +13 -5
  384. package/dist/storage/index.d.ts.map +1 -1
  385. package/dist/storage/index.js +39 -2
  386. package/dist/storage/index.js.map +1 -1
  387. package/dist/storage/layout.d.ts +2 -2
  388. package/dist/storage/layout.d.ts.map +1 -1
  389. package/dist/storage/layout.js +2 -7
  390. package/dist/storage/layout.js.map +1 -1
  391. package/dist/storage/memory-path.d.ts +18 -5
  392. package/dist/storage/memory-path.d.ts.map +1 -1
  393. package/dist/storage/memory-path.js +41 -5
  394. package/dist/storage/memory-path.js.map +1 -1
  395. package/dist/storage/snapshot.js +1 -1
  396. package/dist/storage/templates.d.ts +33 -6
  397. package/dist/storage/templates.d.ts.map +1 -1
  398. package/dist/storage/templates.js +182 -17
  399. package/dist/storage/templates.js.map +1 -1
  400. package/dist/validation/config-version.d.ts +19 -0
  401. package/dist/validation/config-version.d.ts.map +1 -0
  402. package/dist/validation/config-version.js +53 -0
  403. package/dist/validation/config-version.js.map +1 -0
  404. package/dist/validation/diagnostic.d.ts +40 -0
  405. package/dist/validation/diagnostic.d.ts.map +1 -0
  406. package/dist/validation/diagnostic.js +41 -0
  407. package/dist/validation/diagnostic.js.map +1 -0
  408. package/dist/validation/format.d.ts +74 -0
  409. package/dist/validation/format.d.ts.map +1 -0
  410. package/dist/validation/format.js +104 -0
  411. package/dist/validation/format.js.map +1 -0
  412. package/dist/validation/id.d.ts +40 -0
  413. package/dist/validation/id.d.ts.map +1 -1
  414. package/dist/validation/id.js +99 -12
  415. package/dist/validation/id.js.map +1 -1
  416. package/dist/validation/identity.d.ts +40 -0
  417. package/dist/validation/identity.d.ts.map +1 -0
  418. package/dist/validation/identity.js +78 -0
  419. package/dist/validation/identity.js.map +1 -0
  420. package/dist/validation/index.d.ts +9 -3
  421. package/dist/validation/index.d.ts.map +1 -1
  422. package/dist/validation/index.js +33 -1
  423. package/dist/validation/index.js.map +1 -1
  424. package/dist/validation/secret-scan.d.ts +10 -7
  425. package/dist/validation/secret-scan.d.ts.map +1 -1
  426. package/dist/validation/secret-scan.js +39 -16
  427. package/dist/validation/secret-scan.js.map +1 -1
  428. package/dist/validation/warning.d.ts +30 -16
  429. package/dist/validation/warning.d.ts.map +1 -1
  430. package/dist/validation/warning.js +74 -7
  431. package/dist/validation/warning.js.map +1 -1
  432. package/dist/workflow/bindings.d.ts +152 -0
  433. package/dist/workflow/bindings.d.ts.map +1 -0
  434. package/dist/workflow/bindings.js +249 -0
  435. package/dist/workflow/bindings.js.map +1 -0
  436. package/dist/workflow/create.d.ts +13 -0
  437. package/dist/workflow/create.d.ts.map +1 -0
  438. package/dist/workflow/create.js +41 -0
  439. package/dist/workflow/create.js.map +1 -0
  440. package/dist/workflow/deduce.d.ts +400 -0
  441. package/dist/workflow/deduce.d.ts.map +1 -0
  442. package/dist/workflow/deduce.js +1109 -0
  443. package/dist/workflow/deduce.js.map +1 -0
  444. package/dist/workflow/manifest-edit.d.ts +19 -0
  445. package/dist/workflow/manifest-edit.d.ts.map +1 -0
  446. package/dist/workflow/manifest-edit.js +124 -0
  447. package/dist/workflow/manifest-edit.js.map +1 -0
  448. package/dist/workflow/schema.d.ts +158 -19
  449. package/dist/workflow/schema.d.ts.map +1 -1
  450. package/dist/workflow/schema.js +190 -16
  451. package/dist/workflow/schema.js.map +1 -1
  452. package/package.json +37 -13
@@ -1,12 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EMPTY_TREE_SHA = void 0;
3
+ exports.EMPTY_TREE_SHA = exports.CommitFailure = void 0;
4
4
  exports.commitPaths = commitPaths;
5
+ exports.stagedPaths = stagedPaths;
6
+ exports.commitEmpty = commitEmpty;
7
+ exports.writeAndCommit = writeAndCommit;
8
+ exports.commitFailureSummary = commitFailureSummary;
5
9
  exports.readPathAtRev = readPathAtRev;
10
+ exports.committedBlobMatches = committedBlobMatches;
11
+ exports.storesAsBlob = storesAsBlob;
6
12
  exports.pathPorcelainStatus = pathPorcelainStatus;
7
13
  exports.commitParent = commitParent;
8
14
  exports.changedPathsBetween = changedPathsBetween;
9
15
  exports.listPathsAtRev = listPathsAtRev;
16
+ exports.listBlobEntriesAtRev = listBlobEntriesAtRev;
17
+ exports.resolveCommitAtRev = resolveCommitAtRev;
18
+ exports.readPathsAtRev = readPathsAtRev;
19
+ exports.readRevisionPaths = readRevisionPaths;
20
+ exports.listPathsAtRevs = listPathsAtRevs;
21
+ exports.listBlobsAtRevs = listBlobsAtRevs;
22
+ exports.readBlobs = readBlobs;
23
+ exports.mergeBasesWith = mergeBasesWith;
10
24
  /**
11
25
  * The reusable git-commit primitive (task-018-implement-git-backed-storage, P1.1, REQ-SYS-01).
12
26
  *
@@ -18,7 +32,7 @@ exports.listPathsAtRev = listPathsAtRev;
18
32
  * commit, returning its full sha. Every mutating Wave-3 task (task-019 versioning, task-020 memory
19
33
  * add, task-025 dna set, task-029 `wingfoil init`) turns its writes into a single attributable
20
34
  * commit through this one helper, so the "one operation = one scoped commit" convention
21
- * (CLAUDE.md §5.1) has a single implementation rather than being re-derived per task.
35
+ * (AGENTS.md §5.1) has a single implementation rather than being re-derived per task.
22
36
  *
23
37
  * Mirrors the git-primitive style in `src/memory/git-log.ts`: `execFileSync('git', …)` (never a
24
38
  * shell), `-C <root>` to target the repository, and `env: process.env` passed **explicitly** — the
@@ -26,11 +40,18 @@ exports.listPathsAtRev = listPathsAtRev;
26
40
  * env silently drops `GIT_*` overrides a test sets (the task-014 env-isolation gotcha).
27
41
  */
28
42
  const child_process_1 = require("child_process");
43
+ const fs_1 = require("fs");
44
+ const path_1 = require("path");
45
+ const build_stamp_1 = require("./build-stamp");
46
+ const dry_run_1 = require("./dry-run");
47
+ const errors_1 = require("./errors");
48
+ const git_read_1 = require("./git-read");
49
+ /** The environment git runs with: `process.env`, with {@link CommitOptions.env} merged over it. */
50
+ function gitEnv(options) {
51
+ return options.env ? { ...process.env, ...options.env } : process.env;
52
+ }
29
53
  function runGit(root, args, options) {
30
- return (0, child_process_1.execFileSync)('git', ['-C', root, ...args], {
31
- encoding: 'utf-8',
32
- env: options.env ? { ...process.env, ...options.env } : process.env,
33
- });
54
+ return (0, child_process_1.execFileSync)('git', ['-C', root, ...args], { encoding: 'utf-8', env: gitEnv(options) });
34
55
  }
35
56
  /**
36
57
  * {@link runGit} for a **probe** — an invocation whose failure is an expected answer rather than an
@@ -45,11 +66,29 @@ function runGit(root, args, options) {
45
66
  * has not been created yet. A non-zero exit still raises through `execFileSync` exactly as before.
46
67
  */
47
68
  function probeGit(root, args, options) {
48
- return (0, child_process_1.execFileSync)('git', ['-C', root, ...args], {
49
- encoding: 'utf-8',
50
- env: options.env ? { ...process.env, ...options.env } : process.env,
51
- stdio: ['ignore', 'pipe', 'ignore'],
52
- });
69
+ return (0, child_process_1.execFileSync)('git', ['-C', root, ...args], { encoding: 'utf-8', env: gitEnv(options), stdio: ['ignore', 'pipe', 'ignore'] });
70
+ }
71
+ /** git's exit status for `fatal:` — an absent path, an unresolvable revision, no repository. */
72
+ const GIT_FATAL = 128;
73
+ /** {@link CommitOptions} as the read helper takes them, with the exit statuses that are answers. */
74
+ function gitReadOptions(options, accepted) {
75
+ return options.env ? { accepted, env: options.env } : { accepted };
76
+ }
77
+ /**
78
+ * `message` with the build signature appended as the final trailer paragraph (task-192, `dl-111`): a blank
79
+ * line, then `WingFoil-Version: <semver> (<sha>)` ({@link readBuildStamp}). The caller's
80
+ * {@link CommitOptions.trailers}, when given, open that paragraph, the signature staying its last line
81
+ * (task-226's phase record, task-227's step linkage).
82
+ *
83
+ * A final paragraph of trailers, never a line merged into the caller's body: git reads trailers from the
84
+ * message's final paragraph only, and so does `parseReasonBlock` (`src/memory/commit-message.ts`),
85
+ * whose `Reason:` block ends where that paragraph begins. Merged into an `Approver:`/`Reason:` body
86
+ * (what `git commit --trailer` would do, since that body is itself trailer-shaped), the signature would
87
+ * turn the whole body into the trailer block and cut a multi-line reason short. The caller's message
88
+ * is trimmed of trailing whitespace first, which `--cleanup=whitespace` would drop anyway.
89
+ */
90
+ function stampedMessage(message, trailers = []) {
91
+ return `${message.replace(/\s+$/, '')}\n\n${[...trailers, (0, build_stamp_1.formatVersionTrailer)((0, build_stamp_1.readBuildStamp)())].join('\n')}`;
53
92
  }
54
93
  /**
55
94
  * Stage exactly `paths` (root-relative or absolute; each passed verbatim after `--` so a path that
@@ -58,23 +97,299 @@ function probeGit(root, args, options) {
58
97
  * neither committed nor unstaged (bug-027) — this also holds for the first commit of an empty
59
98
  * repository (`wingfoil init`).
60
99
  *
100
+ * **What the commit records is `message` plus the build signature** (task-192, `dl-111`): a final
101
+ * trailer paragraph `WingFoil-Version: <semver> (<sha>)` ({@link stampedMessage}), so every commit
102
+ * WingFoil writes says which build wrote it and a hand-written one carries no such line. The body is
103
+ * normalized by `--cleanup=whitespace`, pinned rather than inherited from `commit.cleanup`
104
+ * (`bug-051`). Callers keep returning their own `message` in `CoreResult.commit`; the signature is
105
+ * this primitive's, not the operation's.
106
+ *
61
107
  * Determinism note (REQ-SYS-07): a git commit's sha necessarily incorporates the author/commit
62
108
  * timestamp, so two runs produce different shas — that is inherent to *creating* history and is not
63
109
  * a context-building read path (the determinism rule targets read/derivation paths, e.g.
64
110
  * `computeStateSnapshot`), so it is correct and intended here.
65
111
  *
112
+ * A mutating operation commits through {@link writeAndCommit}, which writes, commits and undoes a
113
+ * refused commit in one place; during a dry run this function throws instead of committing (task-210).
114
+ *
66
115
  * @throws whatever `git` raises (via `execFileSync`) — e.g. nothing staged to commit, or `root` is
67
116
  * not a git repository. Callers that must translate those into a `CoreResult` do so at their layer
68
117
  * (see `src/core/init.ts`); this primitive stays a thin, throwing mechanism by design.
69
118
  */
70
119
  function commitPaths(root, paths, message, options = {}) {
71
- runGit(root, ['add', '--', ...paths], options);
120
+ (0, dry_run_1.refuseDuringDryRun)('commitPaths');
121
+ return commitPathsNow(root, paths, message, options);
122
+ }
123
+ /**
124
+ * {@link commitPaths} without the dry-run guard — what {@link writeAndCommit} runs once it has decided
125
+ * this is a real run. git's stderr is captured rather than inherited (task-210, `bug-217`): a refusing
126
+ * hook's text reaches the caller inside the error, which {@link writeAndCommit} rewords, instead of
127
+ * being printed raw to the operator's terminal before WingFoil's own message.
128
+ */
129
+ function commitPathsNow(root, paths, message, options) {
130
+ quietGit(root, ['add', '--', ...paths], options);
72
131
  // `--only -- <paths>` records exactly these paths, whatever else is staged: anything a caller or
73
132
  // another tool already staged stays staged and uncommitted (bug-027). A plain `git commit` would
74
133
  // commit the whole index under a subject that names only this operation.
75
- runGit(root, ['commit', '--only', '--quiet', '-m', message, '--', ...paths], options);
134
+ const commitOptions = withAuthor(options);
135
+ // `--cleanup=whitespace` (bug-051): the body's normal form — trailing whitespace stripped, blank-line
136
+ // runs collapsed, a `#` line kept — is the tool's, whatever `commit.cleanup` the operator's or the
137
+ // repository's git config sets. Unpinned, `strip` deleted a reason line opening with `#` and
138
+ // `verbatim` kept what `dl-067` clause 3 declares removed.
139
+ quietGit(root, ['commit', '--only', '--quiet', '--cleanup=whitespace', '-m', stampedMessage(message, options.trailers), '--', ...paths], commitOptions);
140
+ return runGit(root, ['rev-parse', 'HEAD'], options).trim();
141
+ }
142
+ /** {@link CommitOptions} with the pinned author, when one is given, in git's environment (task-132). */
143
+ function withAuthor(options) {
144
+ return options.author ? { ...options, env: { ...options.env, GIT_AUTHOR_NAME: options.author.name, GIT_AUTHOR_EMAIL: options.author.email } } : options;
145
+ }
146
+ /**
147
+ * The root-relative paths the index holds a change to, against `HEAD` (`git diff --cached --name-only`),
148
+ * in git's order — what a plain `git commit` would record. Empty when the index matches `HEAD`.
149
+ */
150
+ function stagedPaths(root, options = {}) {
151
+ return runGit(root, ['-c', 'core.quotePath=false', 'diff', '--cached', '--name-only', '-z'], options)
152
+ .split('\0')
153
+ .filter((path) => path.length > 0);
154
+ }
155
+ /**
156
+ * Make one commit that changes **no file** — `workflow finalize`'s phase record (`spec-017` §7.9, task-226) —
157
+ * with `message`, {@link CommitOptions.trailers} and the build signature in its final paragraph, returning
158
+ * its sha. The commit primitive's third form, beside {@link writeAndCommit}:
159
+ *
160
+ * - **`git commit --only --allow-empty`, no pathspec** — git's documented form for an empty commit that
161
+ * records nothing the index holds: a change the operator staged is neither committed nor unstaged
162
+ * (`bug-027`, `dl-106`), even when a caller skipped the dirty-index guard (`requireCleanIndex`,
163
+ * `src/core/write-guard.ts`) that refuses it first.
164
+ * - **`--cleanup=whitespace`** pinned over `commit.cleanup` (`bug-051`), as for every tool commit.
165
+ * - **Dry run** (`dl-106` W2): inside `captureDryRun` nothing is committed; the plan is the subject, the
166
+ * full message, no paths and no diff.
167
+ *
168
+ * @throws {@link CommitFailure} (`E_COMMIT_FAILED`) when git does not make the commit (a refusing hook);
169
+ * nothing was written, so there is nothing to put back.
170
+ */
171
+ function commitEmpty(root, message, options = {}) {
172
+ const stamped = stampedMessage(message, options.trailers);
173
+ if ((0, dry_run_1.isDryRunActive)()) {
174
+ (0, dry_run_1.stopWithPlan)({ dryRun: true, subject: /^.*/.exec(message)[0], message: stamped, paths: [], diff: '' }, options.plannedWarnings ?? options.warnings);
175
+ }
176
+ try {
177
+ quietGit(root, ['commit', '--only', '--allow-empty', '--quiet', '--cleanup=whitespace', '-m', stamped], withAuthor(options));
178
+ }
179
+ catch (error) {
180
+ throw new CommitFailure(root, [], gitExplanation(error), undefined);
181
+ }
76
182
  return runGit(root, ['rev-parse', 'HEAD'], options).trim();
77
183
  }
184
+ /** {@link runGit} with stderr captured into the thrown error instead of inherited by the terminal. */
185
+ function quietGit(root, args, options) {
186
+ return (0, child_process_1.execFileSync)('git', ['-C', root, ...args], { encoding: 'utf-8', env: gitEnv(options), stdio: ['ignore', 'pipe', 'pipe'] });
187
+ }
188
+ /** `path` relative to `root`, with `/` separators — the spelling git and the plan use. */
189
+ function repoRelative(root, path) {
190
+ return ((0, path_1.isAbsolute)(path) ? (0, path_1.relative)(root, path) : path).split(path_1.sep).join('/');
191
+ }
192
+ /**
193
+ * Write `changes` and commit exactly those paths as one commit with `message`, returning its sha — the
194
+ * single primitive every mutating operation writes through (task-210), so that two behaviours are
195
+ * implemented once for all of them:
196
+ *
197
+ * - **Dry run** (`dl-106` W2, `spec-008` §2 `--dry-run`). Inside `captureDryRun` (`./dry-run`), nothing
198
+ * is written: the commit the call would make — subject, full message with the `WingFoil-Version:`
199
+ * trailer, sorted root-relative paths, and the diff from `HEAD` to the blobs it would record
200
+ * (`planDiff`) — is recorded with {@link CommitOptions.warnings}, and the operation stops here. Every
201
+ * guard and refusal the operation runs before its write has already run, so a dry run of a refused
202
+ * operation is refused the same way.
203
+ * - **A failed commit leaves nothing behind** (`bug-217`). When writing a file or git's `add`/`commit`
204
+ * fails (a refusing hook, a full disk, an unparseable author date, a held `index.lock`), the working
205
+ * tree is put back first — a created file is deleted, then every directory the write created, deepest
206
+ * first across all paths; an overwritten or deleted file gets its bytes back — and then, when it
207
+ * differs from what it was, the index entry of every path, whatever the operator had staged there. So
208
+ * a retry of `memory add` gets the same id and nothing rides into the operator's next commit. A git
209
+ * failure is then reported as {@link CommitFailure} (`E_COMMIT_FAILED`), naming the paths repository-
210
+ * relative and quoting git's explanation with the project root removed — and, if the index could not
211
+ * be put back, saying so. A filesystem failure is rethrown as it was raised (the index is untouched by
212
+ * then).
213
+ *
214
+ * Callers run their confinement and dirty-target guards before calling: this primitive writes where it
215
+ * is told.
216
+ */
217
+ function writeAndCommit(root, changes, message, options = {}) {
218
+ const planned = changes.map((change) => ({
219
+ absolute: (0, path_1.isAbsolute)(change.path) ? change.path : (0, path_1.join)(root, change.path),
220
+ relative: repoRelative(root, change.path),
221
+ content: change.content,
222
+ }));
223
+ if ((0, dry_run_1.isDryRunActive)()) {
224
+ const sorted = [...planned].sort((a, b) => Number(a.relative > b.relative) - Number(a.relative < b.relative));
225
+ (0, dry_run_1.stopWithPlan)({
226
+ dryRun: true,
227
+ subject: /^.*/.exec(message)[0],
228
+ message: stampedMessage(message, options.trailers),
229
+ paths: sorted.map((change) => change.relative),
230
+ diff: sorted.map((change) => (0, dry_run_1.planDiff)(root, change.relative, change.content, gitEnv(options))).join(''),
231
+ }, options.plannedWarnings ?? options.warnings);
232
+ }
233
+ const relativePaths = planned.map((change) => change.relative);
234
+ const indexBefore = indexEntries(root, relativePaths, options);
235
+ const bytesBefore = planned.map((change) => ((0, fs_1.existsSync)(change.absolute) ? (0, fs_1.readFileSync)(change.absolute) : null));
236
+ const createdDirectories = [];
237
+ const restoreWorkingTree = () => {
238
+ planned.forEach((change, index) => {
239
+ const bytes = bytesBefore[index] ?? null;
240
+ if (bytes !== null)
241
+ (0, fs_1.writeFileSync)(change.absolute, bytes);
242
+ else if ((0, fs_1.existsSync)(change.absolute))
243
+ (0, fs_1.unlinkSync)(change.absolute);
244
+ });
245
+ removeCreatedDirectories(createdDirectories);
246
+ };
247
+ try {
248
+ for (const change of planned) {
249
+ if (change.content === null) {
250
+ (0, fs_1.unlinkSync)(change.absolute);
251
+ }
252
+ else {
253
+ const created = (0, fs_1.mkdirSync)((0, path_1.dirname)(change.absolute), { recursive: true });
254
+ if (created !== undefined)
255
+ createdDirectories.push({ file: change.absolute, created });
256
+ (0, fs_1.writeFileSync)(change.absolute, change.content, 'utf-8');
257
+ }
258
+ }
259
+ }
260
+ catch (error) {
261
+ restoreWorkingTree();
262
+ throw error;
263
+ }
264
+ try {
265
+ return commitPathsNow(root, changes.map((change) => change.path), message, options);
266
+ }
267
+ catch (error) {
268
+ restoreWorkingTree();
269
+ const indexProblem = restoreIndexIfChanged(root, relativePaths, indexBefore, options);
270
+ throw new CommitFailure(root, relativePaths, gitExplanation(error), indexProblem === undefined ? undefined : withoutRoot(root, indexProblem));
271
+ }
272
+ }
273
+ /**
274
+ * `E_COMMIT_FAILED` (task-210, `bug-217`): git did not make the commit of a write, and
275
+ * {@link writeAndCommit} has put the working tree back. Carries its parts so a caller that must word
276
+ * the failure differently (approve's supersede recovery) does not parse the message.
277
+ */
278
+ class CommitFailure extends errors_1.StorageError {
279
+ /** The repository-relative paths the commit would have contained. */
280
+ paths;
281
+ /** git's explanation, on one line, with the project root removed. */
282
+ gitDetail;
283
+ /** Why the index entries could not be put back, when they could not; `undefined` when they were. */
284
+ indexProblem;
285
+ constructor(root, paths, raw, indexProblem) {
286
+ const gitDetail = withoutRoot(root, raw);
287
+ const outcome = indexProblem === undefined
288
+ ? 'nothing was committed, and the working tree and the index are as they were'
289
+ : `nothing was committed and the working tree is as it was, but the index entries of ${paths.join(', ')} could not be put back (${indexProblem}): check them with git status`;
290
+ super(errors_1.E_COMMIT_FAILED, `git did not commit ${committedWhat(paths)}: ${gitDetail} — ${outcome}`);
291
+ this.name = 'CommitFailure';
292
+ this.paths = paths;
293
+ this.gitDetail = gitDetail;
294
+ this.indexProblem = indexProblem;
295
+ Object.setPrototypeOf(this, CommitFailure.prototype);
296
+ }
297
+ }
298
+ exports.CommitFailure = CommitFailure;
299
+ /**
300
+ * The reason a caller quotes for a failed write it reports in its own words (approve's supersede
301
+ * recovery, task-210 review F5): for a {@link CommitFailure}, `E_COMMIT_FAILED: git did not commit
302
+ * <paths>: <explanation>` — without the clause saying the working tree is as it was, which such a caller
303
+ * is about to make untrue — plus the index problem, if any; for anything else, `String(error)`.
304
+ */
305
+ function commitFailureSummary(error) {
306
+ if (!(error instanceof CommitFailure))
307
+ return String(error);
308
+ const index = error.indexProblem === undefined ? '' : ` (the index entries could not be put back: ${error.indexProblem})`;
309
+ return `${errors_1.E_COMMIT_FAILED}: git did not commit ${committedWhat(error.paths)}: ${error.gitDetail}${index}`;
310
+ }
311
+ /** What a failed commit would have recorded: its paths, or `an empty commit` for {@link commitEmpty}'s. */
312
+ function committedWhat(paths) {
313
+ return paths.length === 0 ? 'an empty commit' : paths.join(', ');
314
+ }
315
+ /** `git ls-files -s -z` of `paths`: the `mode oid stage\tpath` records the index holds for them. */
316
+ function indexEntries(root, paths, options) {
317
+ return runGit(root, ['ls-files', '-s', '-z', '--', ...paths], options)
318
+ .split('\0')
319
+ .filter((record) => record.length > 0);
320
+ }
321
+ /**
322
+ * Put the index entries of `paths` back to `entries` when they differ from them now — drop what was not
323
+ * there, reinstate what was. Best effort: a failure (a held `index.lock`) is returned as git's
324
+ * explanation rather than thrown, so the caller still reports the commit failure in its own words.
325
+ */
326
+ function restoreIndexIfChanged(root, paths, entries, options) {
327
+ try {
328
+ if (indexEntries(root, paths, options).join('\0') === entries.join('\0'))
329
+ return undefined;
330
+ quietGit(root, ['update-index', '--force-remove', '--', ...paths], options);
331
+ if (entries.length > 0) {
332
+ (0, child_process_1.execFileSync)('git', ['-C', root, 'update-index', '-z', '--index-info'], {
333
+ input: entries.map((entry) => `${entry}\0`).join(''),
334
+ env: gitEnv(options),
335
+ stdio: ['pipe', 'pipe', 'pipe'],
336
+ });
337
+ }
338
+ return undefined;
339
+ }
340
+ catch (error) {
341
+ return gitExplanation(error);
342
+ }
343
+ }
344
+ /**
345
+ * Remove every directory a write created — for each file, from its own directory up to and including the
346
+ * first directory `mkdirSync({recursive})` created for it — **deepest first across all files**, so a
347
+ * directory emptied by removing a deeper one is removed in turn. One that is not empty stays: something
348
+ * else now lives there, and it is not this write's to remove.
349
+ */
350
+ function removeCreatedDirectories(created) {
351
+ const directories = new Set();
352
+ for (const entry of created) {
353
+ for (let directory = (0, path_1.dirname)(entry.file);; directory = (0, path_1.dirname)(directory)) {
354
+ directories.add(directory);
355
+ if (directory === entry.created || (0, path_1.dirname)(directory) === directory)
356
+ break;
357
+ }
358
+ }
359
+ const depth = (path) => path.split(path_1.sep).length;
360
+ for (const directory of [...directories].sort((a, b) => depth(b) - depth(a) || Number(a < b) - Number(a > b))) {
361
+ try {
362
+ (0, fs_1.rmdirSync)(directory);
363
+ }
364
+ catch {
365
+ // Not empty: it holds something this write did not create.
366
+ }
367
+ }
368
+ }
369
+ /** git's explanation of a failed run: its stderr when it wrote one, else the error's own message. */
370
+ function gitExplanation(error) {
371
+ const captured = error.stderr;
372
+ // A commit with nothing to record explains itself on stdout only; the message (`Command failed: git
373
+ // -C <root> …`) is then what there is, and its root is removed by `withoutRoot`.
374
+ return typeof captured === 'string' && captured.trim().length > 0 ? captured : error.message;
375
+ }
376
+ /**
377
+ * `text` on one line, with every spelling of the project root removed — `<root>/x` becomes `x`, a bare
378
+ * `<root>` becomes `.` — so no absolute path reaches the operator (the `bug-251` class).
379
+ */
380
+ function withoutRoot(root, text) {
381
+ // The longer spelling first, so that one spelling being a prefix of the other leaves no fragment.
382
+ const real = (0, fs_1.realpathSync)(root);
383
+ let detail = text;
384
+ for (const spelling of real.length >= root.length ? [real, root] : [root, real]) {
385
+ detail = detail.split(`${spelling}${path_1.sep}`).join('').split(spelling).join('.');
386
+ }
387
+ return detail
388
+ .split('\n')
389
+ .map((line) => line.trim())
390
+ .filter((line) => line.length > 0)
391
+ .join('; ');
392
+ }
78
393
  // --- Read primitives for asserting what a commit CONTAINS (task-088, bug-076) ------------------
79
394
  //
80
395
  // `commitPaths` above bounds a commit by *pathspec*; nothing bounded it by *content*, so a path that
@@ -88,21 +403,61 @@ function commitPaths(root, paths, message, options = {}) {
88
403
  */
89
404
  exports.EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
90
405
  /**
91
- * The content of `path` at revision `rev`, or `null` when the path does not exist there (git exits
92
- * non-zero, which `execFileSync` raises).
406
+ * The content of `path` at revision `rev`, or `null` when git answers with its `fatal:` exit `128`:
407
+ * the path is absent there, or `rev` does not resolve (or `root` is no repository).
408
+ *
409
+ * Read through `runGitRead` (`./git-read`, task-142 review): stderr is captured, never inherited, and
410
+ * the buffer is `GIT_READ_MAX_BUFFER`. Before, a document past Node's 1 MiB default made
411
+ * `execFileSync` throw `ENOBUFS` and a blanket `catch` answered `null` — "absent" for a document that
412
+ * is there, which `memory amend`, the loaders and the write guard turned into a wrong refusal. A
413
+ * failure that is not git's answer — git that cannot be spawned, an answer past the buffer — now
414
+ * throws `StorageError` `E_GIT_READ_FAILED`. Exit `128` stays `null`: git gives it to an absent path
415
+ * and to an unreadable object alike, and this read does not tell them apart.
93
416
  *
94
417
  * `rev` is any revision git accepts before a `:` — a sha, `HEAD`, or the index stage `:0`, which is
95
418
  * how a caller reads what the user has **staged** as opposed to what is in the working tree. The two
96
419
  * can disagree, and the difference is load-bearing: `git add` would silently replace a staged version
97
420
  * with the working-tree one.
421
+ *
422
+ * @throws {@link StorageError} `E_GIT_READ_FAILED` when git cannot be spawned or its answer exceeds
423
+ * the read buffer (task-142 review).
98
424
  */
99
425
  function readPathAtRev(root, rev, path, options = {}) {
100
- try {
101
- return probeGit(root, ['show', `${rev}:${path}`], options);
102
- }
103
- catch {
104
- return null;
105
- }
426
+ const run = (0, git_read_1.runGitRead)(root, ['show', `${rev}:${path}`], gitReadOptions(options, [0, GIT_FATAL]));
427
+ return run.status === GIT_FATAL ? null : run.stdout;
428
+ }
429
+ /**
430
+ * Whether `content`, put through the filters git applies when it stores `path` (`core.autocrlf`,
431
+ * `.gitattributes` `eol`/`text`/`filter`), is exactly the blob `rev` holds at `path` — git's own
432
+ * answer to "is this what was committed", rather than a byte comparison with `git show`, which on a
433
+ * machine that normalizes line endings sees an LF blob where the working tree holds CRLF (task-193
434
+ * review). Compares object ids: `git hash-object --path=<path> --stdin` of the bytes against
435
+ * `git rev-parse <rev>:<path>`. `false` when `rev` does not hold `path`.
436
+ *
437
+ * @param root - The repository.
438
+ * @param rev - The revision whose blob is compared (a commit sha).
439
+ * @param path - Root-relative path, which selects the filters as well as the blob.
440
+ * @param content - The text as it was written to the working tree.
441
+ * @param options - {@link CommitOptions} (the environment git runs with).
442
+ */
443
+ function committedBlobMatches(root, rev, path, content, options = {}) {
444
+ const blob = (0, git_read_1.runGitRead)(root, ['rev-parse', '--verify', '--quiet', `${rev}:${path}`], gitReadOptions(options, [0, 1, GIT_FATAL]));
445
+ if (blob.status !== 0)
446
+ return false;
447
+ const hashed = (0, git_read_1.runGitRead)(root, ['hash-object', `--path=${path}`, '--stdin'], { ...gitReadOptions(options, [0]), input: content });
448
+ return hashed.stdout.trim() === blob.stdout.trim();
449
+ }
450
+ /**
451
+ * Whether git would store `content`, written to the working tree at `path`, as exactly the blob
452
+ * `blob` — `content` put through the filters git applies to `path` (`core.autocrlf`, `.gitattributes`),
453
+ * `blob` taken as-is. The counterpart of {@link committedBlobMatches} for a blob that exists only in
454
+ * memory (`memory submit`'s rendering of the committed document, task-209 review F1): it compares
455
+ * `git hash-object --path=<path> --stdin` with `git hash-object --no-filters --stdin`.
456
+ */
457
+ function storesAsBlob(root, path, content, blob, options = {}) {
458
+ const filtered = (0, git_read_1.runGitRead)(root, ['hash-object', `--path=${path}`, '--stdin'], { ...gitReadOptions(options, [0]), input: content });
459
+ const raw = (0, git_read_1.runGitRead)(root, ['hash-object', '--no-filters', '--stdin'], { ...gitReadOptions(options, [0]), input: blob });
460
+ return filtered.stdout.trim() === raw.stdout.trim();
106
461
  }
107
462
  /**
108
463
  * The two-character `git status --porcelain` code for `path` (index status, then working-tree
@@ -179,23 +534,442 @@ const LS_TREE_BLOB_RECORD = /^\d+ blob [0-9a-f]+\t/;
179
534
  * @param prefix - Root-relative directory to limit the listing to; **omitted or empty lists the whole
180
535
  * tree**, because `git ls-tree -- ''` is an error rather than a match-all. Passed verbatim after
181
536
  * `--`, so a path that looks like a flag is never misread.
182
- * @returns The paths, or `null` when `rev` does not resolve.
537
+ * @returns The paths, or `null` when `rev` does not resolve (git's exit `128`).
538
+ * @throws {@link StorageError} `E_GIT_READ_FAILED` when git cannot be spawned or its answer exceeds
539
+ * the read buffer (task-142 review).
183
540
  */
184
541
  function listPathsAtRev(root, rev, prefix = '', options = {}) {
542
+ return listBlobEntriesAtRev(root, rev, prefix, options)?.map((entry) => entry.path) ?? null;
543
+ }
544
+ /** The mode git records for a symbolic link. */
545
+ const LS_TREE_SYMLINK_MODE = '120000';
546
+ /**
547
+ * {@link listPathsAtRev} with each blob's kind: the same paths, in the same order, each marked when git
548
+ * records it as a **symbolic link** (mode `120000`). A link is a blob, so `listPathsAtRev` lists it and
549
+ * `readPathAtRev` reads back its target text; a reader that must not take that text for a document
550
+ * (the Memory scan, task-171, `bug-189`) asks here instead. Same `null` and same errors as
551
+ * {@link listPathsAtRev}.
552
+ */
553
+ function listBlobEntriesAtRev(root, rev, prefix, options = {}) {
185
554
  const pathspec = prefix.length === 0 ? [] : ['--', prefix];
186
- let records;
187
- try {
188
- // `probeGit`: an unresolvable revision is an expected answer here, and git's `fatal: Not a valid
189
- // object name` must not reach the user's terminal beside the CLI's own message.
190
- records = probeGit(root, ['ls-tree', '-r', '-z', '--full-tree', rev, ...pathspec], options);
191
- }
192
- catch {
555
+ // `runGitRead` (task-142 review): an unresolvable revision is git's exit `128`, an expected answer
556
+ // here, and its `fatal: Not a valid object name` stays in a captured pipe. A listing past Node's
557
+ // 1 MiB default no longer reads as "this revision does not resolve".
558
+ const run = (0, git_read_1.runGitRead)(root, ['ls-tree', '-r', '-z', '--full-tree', rev, ...pathspec], gitReadOptions(options, [0, GIT_FATAL]));
559
+ if (run.status === GIT_FATAL)
193
560
  return null;
194
- }
195
- return records
561
+ return run.stdout
196
562
  .split('\0')
197
563
  .filter((record) => LS_TREE_BLOB_RECORD.test(record))
198
- .map((record) => record.slice(record.indexOf('\t') + 1))
199
- .sort();
564
+ .map((record) => ({ path: record.slice(record.indexOf('\t') + 1), symlink: record.startsWith(`${LS_TREE_SYMLINK_MODE} `) }))
565
+ // Code-unit order, as `listPathsAtRev`'s plain `.sort()` always gave, without a branch.
566
+ .sort((a, b) => Number(a.path > b.path) - Number(a.path < b.path));
567
+ }
568
+ // --- Resolving a revision, and reading MANY paths at it (task-137) -----------------------------
569
+ /**
570
+ * The full 40-hex sha of the **commit** `rev` names, or `null` when it names none: an unknown name, an
571
+ * unborn `HEAD` in a repository with no commits, a tree or a blob (`git rev-parse --verify --quiet`
572
+ * exits `1` for all of them).
573
+ *
574
+ * The `…AtRev` readers (`src/core/loaders.ts`, `src/memory/query.ts`) resolve their revision through
575
+ * this **once**, then read every byte at the sha, so one read cannot mix two commits when a ref moves
576
+ * while it runs. `^{commit}` peels a tag to its commit and refuses anything that is not one.
577
+ *
578
+ * **A failure is not "no such commit"** (task-137 review): a `root` git cannot read as a repository
579
+ * (exit `128`) or a `git` that cannot be spawned throws {@link StorageError} `E_GIT_READ_FAILED`, so a
580
+ * caller never reports a broken environment as a missing revision.
581
+ *
582
+ * The syntax of `rev` is the caller's to check — the core resolver (`resolveRevision`,
583
+ * `src/core/revision.ts`) refuses a malformed one before it gets here, so that nothing shaped like a
584
+ * flag ever reaches git's argument list.
585
+ *
586
+ * @throws {@link StorageError} `E_GIT_READ_FAILED` when git cannot answer at all.
587
+ */
588
+ function resolveCommitAtRev(root, rev, options = {}) {
589
+ const run = (0, child_process_1.spawnSync)('git', ['-C', root, 'rev-parse', '--verify', '--quiet', `${rev}^{commit}`], {
590
+ encoding: 'utf-8',
591
+ env: options.env ? { ...process.env, ...options.env } : process.env,
592
+ stdio: ['ignore', 'pipe', 'pipe'],
593
+ });
594
+ // `--verify --quiet` exits 1, printing nothing on stdout, for a name that resolves to no commit.
595
+ if (run.status === 1)
596
+ return null;
597
+ if (run.error !== undefined || run.status !== 0) {
598
+ const detail = run.error !== undefined ? run.error.message : run.stderr.trim();
599
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git rev-parse ${rev} failed in ${root}: ${detail}`);
600
+ }
601
+ // `--verify` prints exactly one object name on success: the sha.
602
+ return run.stdout.trim();
603
+ }
604
+ /** Large enough for every Memory document of a repository; a larger answer fails loudly, never truncates. */
605
+ const BATCH_READ_MAX_BUFFER = 256 * 1024 * 1024;
606
+ /** One `git cat-file --batch` header for an object it found: `<oid> SP <type> SP <size>`. */
607
+ const CAT_FILE_HEADER = /^[0-9a-f]+ ([a-z]+) (\d+)$/;
608
+ /**
609
+ * The content of each of `paths` at revision `rev`, in input order — the bytes as UTF-8, or `null` when
610
+ * the revision does not hold that path as a **file** — read through **one** `git cat-file --batch`
611
+ * process instead of one `git show` per path. For a file or an absent path that is what
612
+ * {@link readPathAtRev} answers; for a directory it is `null`, where `git show` would print the tree's
613
+ * listing, which is no file's content.
614
+ *
615
+ * Why it exists (task-137): `spec-017` §1.1 reads every Memory document at `HEAD` on every workflow
616
+ * read, and `spec-012` builds an agent context from a commit. On this repository's 664 Memory and plan
617
+ * documents one `git show` per file took 5.49 s, the batch 0.39 s (task-137 design notes).
618
+ *
619
+ * **Fails closed.** A batch that cannot run, exits non-zero or answers past
620
+ * {@link BATCH_READ_MAX_BUFFER} throws {@link StorageError} `E_GIT_READ_FAILED`: a failed read must not
621
+ * pass for "every path is absent". `null` is reserved for a path the revision really does not hold.
622
+ *
623
+ * A path holding a newline or a carriage return cannot travel on the line-based batch protocol (git
624
+ * ends the request at the newline and strips a trailing CR); it is read on its own with
625
+ * {@link readPathAtRev}, which gives the same answer. Pass a resolved sha as `rev` when several calls
626
+ * must see one commit.
627
+ *
628
+ * @param root - Project root (the git repository).
629
+ * @param rev - A revision naming one commit — best a sha already resolved (`resolveCommitAtRev`). It
630
+ * travels inside each request line, so a rev holding a newline, a carriage return, a `:` or a NUL is
631
+ * refused with {@link StorageError} `E_INVALID_REVISION`: a newline would shift every answer onto
632
+ * the wrong path, and a `:` would change which path is read.
633
+ * @param paths - Root-relative POSIX paths.
634
+ * @returns One entry per path, in the same order.
635
+ * @throws {@link StorageError} `E_INVALID_REVISION` for such a rev; `E_GIT_READ_FAILED` when the
636
+ * batch read fails.
637
+ */
638
+ function readPathsAtRev(root, rev, paths, options = {}) {
639
+ if (/[\n\r:\0]/.test(rev)) {
640
+ throw new errors_1.StorageError(errors_1.E_INVALID_REVISION, `revision ${JSON.stringify(rev)} cannot be read in a batch: it holds a newline, a carriage return, a ':' or a NUL`);
641
+ }
642
+ const batched = paths.filter((path) => !/[\n\r]/.test(path));
643
+ const answers = new Map();
644
+ if (batched.length > 0) {
645
+ const run = (0, child_process_1.spawnSync)('git', ['-C', root, 'cat-file', '--batch'], {
646
+ input: batched.map((path) => `${rev}:${path}\n`).join(''),
647
+ env: options.env ? { ...process.env, ...options.env } : process.env,
648
+ maxBuffer: BATCH_READ_MAX_BUFFER,
649
+ stdio: ['pipe', 'pipe', 'pipe'],
650
+ });
651
+ if (run.error !== undefined || run.status !== 0) {
652
+ const detail = run.error !== undefined ? run.error.message : run.stderr.toString('utf-8').trim();
653
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git cat-file --batch at ${rev} failed in ${root}: ${detail}`);
654
+ }
655
+ const out = run.stdout;
656
+ let offset = 0;
657
+ for (const path of batched) {
658
+ const newline = out.indexOf(0x0a, offset);
659
+ if (newline === -1)
660
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git cat-file --batch at ${rev} in ${root}: truncated answer`);
661
+ const header = CAT_FILE_HEADER.exec(out.subarray(offset, newline).toString('utf-8'));
662
+ offset = newline + 1;
663
+ if (header === null) {
664
+ // `<object> missing` (or `ambiguous`): the revision does not hold this path.
665
+ answers.set(path, null);
666
+ continue;
667
+ }
668
+ const size = Number(header[2]);
669
+ // Every found object is followed by its bytes and one LF — a tree included, which is skipped.
670
+ answers.set(path, header[1] === 'blob' ? out.subarray(offset, offset + size).toString('utf-8') : null);
671
+ offset += size + 1;
672
+ }
673
+ }
674
+ return paths.map((path) => (answers.has(path) ? answers.get(path) : readPathAtRev(root, rev, path, options)));
675
+ }
676
+ /**
677
+ * The content of each `<rev>:<path>` request — each at its OWN revision — in input order: the bytes as
678
+ * UTF-8, or `null` when that revision does not hold that path as a file, a revision that names no
679
+ * commit included (the first parent of a root commit, `<sha>^1`). The many-revision counterpart of
680
+ * {@link readPathsAtRev}, with its answers and its refusals, in **one** `git cat-file --batch`.
681
+ *
682
+ * Why it exists (`task-278`, `bug-308`): `reconstructMemoryTransitions` (`src/memory/audit.ts`) reads
683
+ * a document at every commit of its history AND at each commit's first parent; one `git show` per
684
+ * read would double the spawns of the governance check's slowest path, a batch replaces them all.
685
+ *
686
+ * @param root - Project root (the git repository).
687
+ * @param requests - The revisions and paths to read.
688
+ * @param options - {@link CommitOptions} (the environment git runs with).
689
+ * @returns One entry per request, in the same order.
690
+ * @throws {@link StorageError} `E_INVALID_REVISION` for a rev holding a newline, a carriage return, a
691
+ * `:` or a NUL; `E_GIT_READ_FAILED` when the batch read fails.
692
+ */
693
+ function readRevisionPaths(root, requests, options = {}) {
694
+ for (const { rev } of requests) {
695
+ if (/[\n\r:\0]/.test(rev)) {
696
+ throw new errors_1.StorageError(errors_1.E_INVALID_REVISION, `revision ${JSON.stringify(rev)} cannot be read in a batch: it holds a newline, a carriage return, a ':' or a NUL`);
697
+ }
698
+ }
699
+ // A path holding a newline or a carriage return cannot travel on the line-based protocol: read alone.
700
+ const batched = requests.filter(({ path }) => !/[\n\r]/.test(path));
701
+ const objects = catFileBatch(root, batched.map(({ rev, path }) => `${rev}:${path}`), options);
702
+ const answers = new Map();
703
+ batched.forEach((request, index) => {
704
+ const object = objects[index] ?? null;
705
+ answers.set(request, object !== null && object.type === 'blob' ? object.content.toString('utf-8') : null);
706
+ });
707
+ return requests.map((request) => (answers.has(request) ? answers.get(request) : readPathAtRev(root, request.rev, request.path, options)));
708
+ }
709
+ // --- Listing a directory at MANY revisions in a constant number of processes (task-142) -------
710
+ /** The object-type bits of a tree entry's mode (`S_IFMT`). */
711
+ const MODE_TYPE_MASK = 0o170000;
712
+ /** Type bits of a subtree. Compared as bits, not as text: a legacy writer's zero-padded `040000` is a directory too (task-142 review). */
713
+ const MODE_TREE = 0o040000;
714
+ /** Type bits of a gitlink (submodule): an entry of the tree, but not a file anyone can read back. */
715
+ const MODE_GITLINK = 0o160000;
716
+ /** Ask one `git cat-file --batch` for every request, answering in request order. */
717
+ function catFileBatch(root, requests, options) {
718
+ if (requests.length === 0)
719
+ return [];
720
+ const out = (0, git_read_1.runGitReadBytes)(root, ['cat-file', '--batch'], {
721
+ input: requests.map((request) => `${request}\n`).join(''),
722
+ ...(options.env ? { env: options.env } : {}),
723
+ }).stdout;
724
+ const answers = [];
725
+ let offset = 0;
726
+ for (const request of requests) {
727
+ const newline = out.indexOf(0x0a, offset);
728
+ if (newline === -1)
729
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git cat-file --batch in ${root}: truncated answer for ${request}`);
730
+ const header = /^([0-9a-f]+) ([a-z]+) (\d+)$/.exec(out.subarray(offset, newline).toString('utf-8'));
731
+ offset = newline + 1;
732
+ if (header === null) {
733
+ // `<request> missing` (or `ambiguous`): no object answers that name.
734
+ answers.push(null);
735
+ continue;
736
+ }
737
+ const size = Number(header[3]);
738
+ answers.push({ oid: header[1], type: header[2], content: out.subarray(offset, offset + size) });
739
+ offset += size + 1;
740
+ }
741
+ return answers;
742
+ }
743
+ /** The entries of a raw tree object: `<mode> SP <name> NUL <binary oid>`, repeated. */
744
+ function treeEntries(tree, oidBytes) {
745
+ const entries = [];
746
+ let offset = 0;
747
+ while (offset < tree.length) {
748
+ const space = tree.indexOf(0x20, offset);
749
+ const nul = tree.indexOf(0x00, space);
750
+ entries.push({
751
+ mode: tree.subarray(offset, space).toString('utf-8'),
752
+ name: tree.subarray(space + 1, nul).toString('utf-8'),
753
+ oid: tree.subarray(nul + 1, nul + 1 + oidBytes).toString('hex'),
754
+ });
755
+ offset = nul + 1 + oidBytes;
756
+ }
757
+ return entries;
758
+ }
759
+ /**
760
+ * Every **file** any of `revs` holds under `prefix`, as one sorted, de-duplicated list of
761
+ * root-relative POSIX paths — the union of {@link listPathsAtRev} over `revs`, read in a number of git
762
+ * processes that does not depend on how many revisions there are (`bug-178`, absorbed by `task-142`).
763
+ *
764
+ * `memory add`'s id counter reads every local branch and remote-tracking ref (`dl-101` §2 (a)); one
765
+ * `git ls-tree` per ref made its cost grow with the clone's branch count. Here one
766
+ * `git cat-file --batch` resolves every revision's tree at `prefix`, and each further level of the
767
+ * directory costs one more batch, asking only for the subtrees no earlier answer already held — so the
768
+ * process count is the depth of the directory under `prefix` plus one, and branches that share a
769
+ * subtree (most of them, most of the time) read it once.
770
+ *
771
+ * It answers what {@link listPathsAtRev} answers, unioned — compared case by case in
772
+ * `test/storage/list-paths-at-revs.test.ts` rather than claimed by construction: blobs only (a symbolic
773
+ * link is a blob, a gitlink is not a file), a subtree recognised by its mode's **type bits** so a
774
+ * legacy zero-padded `040000` is still a directory, the path bytes as UTF-8, sorted (REQ-SYS-07). The
775
+ * prefix is taken in canonical spelling — leading `./` segments and trailing `/` are dropped — and a
776
+ * prefix naming a file lists that file unless it ends in `/`. Other non-canonical spellings (`..`,
777
+ * `//` inside) are not normalised, and `ls-tree`'s pathspec matching may answer them differently.
778
+ *
779
+ * @param root - Project root (the git repository).
780
+ * @param revs - Revisions naming commits — best full shas. One that names no commit fails the read.
781
+ * @param prefix - Root-relative directory or file, canonical spelling (see above); empty lists whole trees.
782
+ * @returns The union of the paths, sorted.
783
+ * @throws {@link StorageError} `E_GIT_READ_FAILED` when a revision names no commit or tree, or the
784
+ * batch read fails; `E_INVALID_REVISION` for a revision that cannot travel on the batch protocol.
785
+ */
786
+ function listPathsAtRevs(root, revs, prefix = '', options = {}) {
787
+ for (const rev of revs) {
788
+ if (/[\n\r:\0]/.test(rev)) {
789
+ throw new errors_1.StorageError(errors_1.E_INVALID_REVISION, `revision ${JSON.stringify(rev)} cannot be read in a batch: it holds a newline, a carriage return, a ':' or a NUL`);
790
+ }
791
+ }
792
+ // Canonical spelling: `git ls-tree --full-tree -- ./docs/` reads `docs/`, and so must the batch.
793
+ const directory = prefix.replace(/^(?:\.\/+)+/, '').replace(/\/+$/, '');
794
+ if (/[\n\r]/.test(directory)) {
795
+ // Not expressible on the line-based batch protocol: read revision by revision instead.
796
+ const paths = new Set();
797
+ for (const rev of revs) {
798
+ const listed = listPathsAtRev(root, rev, prefix, options);
799
+ if (listed === null)
800
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git ls-tree ${rev} failed in ${root}: the tree could not be listed`);
801
+ for (const path of listed)
802
+ paths.add(path);
803
+ }
804
+ return [...paths].sort();
805
+ }
806
+ // Round one: each revision's root tree (which proves it names a commit or a tree) and, under a
807
+ // prefix, its tree at that prefix — absent when the revision has no such directory.
808
+ const requests = revs.flatMap((rev) => (directory.length === 0 ? [`${rev}^{tree}`] : [`${rev}^{tree}`, `${rev}:${directory}`]));
809
+ const answers = catFileBatch(root, requests, options);
810
+ const paths = new Set();
811
+ const cache = new Map();
812
+ let frontier = [];
813
+ let oidBytes = 20;
814
+ for (let index = 0; index < requests.length; index += 1) {
815
+ const answer = answers[index] ?? null;
816
+ const request = requests[index];
817
+ if (request.endsWith('^{tree}')) {
818
+ if (answer === null || answer.type !== 'tree') {
819
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git cat-file --batch in ${root}: ${request.slice(0, -'^{tree}'.length)} names no commit whose tree can be listed`);
820
+ }
821
+ oidBytes = answer.oid.length / 2;
822
+ if (directory.length > 0)
823
+ continue;
824
+ }
825
+ else if (answer !== null && answer.type === 'blob' && !prefix.endsWith('/')) {
826
+ // A prefix naming a file lists that file, as `ls-tree -- <file>` does; with a trailing `/` it
827
+ // names a directory, and a file there lists nothing.
828
+ paths.add(directory);
829
+ continue;
830
+ }
831
+ else if (answer === null || answer.type !== 'tree') {
832
+ continue;
833
+ }
834
+ const tree = answer;
835
+ cache.set(tree.oid, tree.content);
836
+ frontier.push({ oid: tree.oid, dir: directory });
837
+ }
838
+ // Each later round reads one level deeper, fetching only the subtrees not already read.
839
+ const seen = new Set();
840
+ while (frontier.length > 0) {
841
+ const unread = [...new Set(frontier.map((node) => node.oid).filter((oid) => !cache.has(oid)))].sort();
842
+ catFileBatch(root, unread, options).forEach((answer, index) => {
843
+ if (answer === null)
844
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git cat-file --batch in ${root}: tree ${unread[index]} is missing`);
845
+ cache.set(answer.oid, answer.content);
846
+ });
847
+ const next = [];
848
+ for (const node of frontier) {
849
+ const key = `${node.oid}\0${node.dir}`;
850
+ if (seen.has(key))
851
+ continue;
852
+ seen.add(key);
853
+ for (const entry of treeEntries(cache.get(node.oid), oidBytes)) {
854
+ const path = node.dir.length === 0 ? entry.name : `${node.dir}/${entry.name}`;
855
+ const type = Number.parseInt(entry.mode, 8) & MODE_TYPE_MASK;
856
+ if (type === MODE_TREE)
857
+ next.push({ oid: entry.oid, dir: path });
858
+ else if (type !== MODE_GITLINK)
859
+ paths.add(path);
860
+ }
861
+ }
862
+ frontier = next;
863
+ }
864
+ return [...paths].sort();
865
+ }
866
+ /**
867
+ * The blobs each of `revs` holds under `directory`, one sorted list per revision, in input order —
868
+ * {@link listPathsAtRevs}' batched walk (task-142, `bug-178`), keeping per revision what it holds and
869
+ * each blob's oid. The walk asks one `git cat-file --batch` per directory level for the trees no earlier
870
+ * answer held, so revisions that share a subtree read it once, and the process count is the directory's
871
+ * depth plus one whatever the number of revisions (task-274, the WIP-limit count across refs).
872
+ *
873
+ * A revision without `directory` holds nothing there. Gitlinks are skipped; a symbolic link is listed,
874
+ * marked. `directory` is root-relative, without a trailing `/`; empty means the whole tree.
875
+ *
876
+ * @throws {@link StorageError} `E_INVALID_REVISION` for a revision that cannot travel on the batch
877
+ * protocol; `E_GIT_READ_FAILED` when a revision names no commit or tree, or the batch read fails.
878
+ */
879
+ function listBlobsAtRevs(root, revs, directory, options = {}) {
880
+ for (const rev of revs) {
881
+ if (/[\n\r:\0]/.test(rev)) {
882
+ throw new errors_1.StorageError(errors_1.E_INVALID_REVISION, `revision ${JSON.stringify(rev)} cannot be read in a batch: it holds a newline, a carriage return, a ':' or a NUL`);
883
+ }
884
+ }
885
+ if (/[\n\r]/.test(directory)) {
886
+ throw new errors_1.StorageError(errors_1.E_INVALID_REVISION, `directory ${JSON.stringify(directory)} cannot be read in a batch: it holds a newline or a carriage return`);
887
+ }
888
+ const answers = catFileBatch(root, revs.flatMap((rev) => [`${rev}^{tree}`, directory.length === 0 ? `${rev}^{tree}` : `${rev}:${directory}`]), options);
889
+ const trees = new Map();
890
+ const roots = revs.map((rev, index) => {
891
+ const commit = answers[2 * index] ?? null;
892
+ if (commit === null || commit.type !== 'tree') {
893
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git cat-file --batch in ${root}: ${rev} names no commit whose tree can be listed`);
894
+ }
895
+ const at = answers[2 * index + 1] ?? null;
896
+ if (at === null || at.type !== 'tree')
897
+ return null;
898
+ trees.set(at.oid, at.content);
899
+ return at.oid;
900
+ });
901
+ const oidBytes = (roots.find((oid) => oid !== null) ?? '0'.repeat(40)).length / 2;
902
+ // Read every subtree, one batch per level, each tree once.
903
+ let frontier = [...new Set(roots.filter((oid) => oid !== null))];
904
+ while (frontier.length > 0) {
905
+ const next = new Set();
906
+ for (const oid of frontier) {
907
+ for (const entry of treeEntries(trees.get(oid), oidBytes)) {
908
+ if ((Number.parseInt(entry.mode, 8) & MODE_TYPE_MASK) === MODE_TREE && !trees.has(entry.oid))
909
+ next.add(entry.oid);
910
+ }
911
+ }
912
+ const unread = [...next].sort();
913
+ catFileBatch(root, unread, options).forEach((answer, index) => {
914
+ if (answer === null)
915
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git cat-file --batch in ${root}: tree ${unread[index]} is missing`);
916
+ trees.set(answer.oid, answer.content);
917
+ });
918
+ frontier = unread;
919
+ }
920
+ // Flatten each tree once, relative to itself; a revision's listing is its root tree's, prefixed.
921
+ const flat = new Map();
922
+ const flatten = (oid) => {
923
+ const known = flat.get(oid);
924
+ if (known !== undefined)
925
+ return known;
926
+ const out = [];
927
+ for (const entry of treeEntries(trees.get(oid), oidBytes)) {
928
+ const type = Number.parseInt(entry.mode, 8) & MODE_TYPE_MASK;
929
+ if (type === MODE_TREE) {
930
+ for (const inner of flatten(entry.oid))
931
+ out.push({ ...inner, path: `${entry.name}/${inner.path}` });
932
+ }
933
+ else if (type !== MODE_GITLINK) {
934
+ out.push({ path: entry.name, oid: entry.oid, symlink: entry.mode === LS_TREE_SYMLINK_MODE });
935
+ }
936
+ }
937
+ flat.set(oid, out);
938
+ return out;
939
+ };
940
+ return roots.map((oid) => oid === null
941
+ ? []
942
+ : flatten(oid)
943
+ .map((blob) => ({ ...blob, path: directory.length === 0 ? blob.path : `${directory}/${blob.path}` }))
944
+ .sort((a, b) => Number(a.path > b.path) - Number(a.path < b.path)));
945
+ }
946
+ /**
947
+ * The content of each blob `oids` names, as UTF-8, read through one `git cat-file --batch` (task-274).
948
+ * Duplicates are read once.
949
+ *
950
+ * @throws {@link StorageError} `E_GIT_READ_FAILED` when an oid names no blob or the batch read fails.
951
+ */
952
+ function readBlobs(root, oids, options = {}) {
953
+ const unique = [...new Set(oids)].sort();
954
+ const contents = new Map();
955
+ catFileBatch(root, unique, options).forEach((answer, index) => {
956
+ if (answer === null || answer.type !== 'blob')
957
+ throw new errors_1.StorageError(errors_1.E_GIT_READ_FAILED, `git cat-file --batch in ${root}: blob ${unique[index]} is missing`);
958
+ contents.set(unique[index], answer.content.toString('utf-8'));
959
+ });
960
+ return contents;
961
+ }
962
+ /**
963
+ * The best common ancestor of `base` with each of `revs` (`git merge-base`), in input order, or `null`
964
+ * for a revision that shares no history with `base` (task-274: a ref's copy of an element counts only
965
+ * when the ref changed it since this commit). One `git merge-base` per revision.
966
+ *
967
+ * @throws {@link StorageError} `E_GIT_READ_FAILED` when git fails for another reason.
968
+ */
969
+ function mergeBasesWith(root, base, revs, options = {}) {
970
+ return revs.map((rev) => {
971
+ const run = (0, git_read_1.runGitRead)(root, ['merge-base', base, rev], gitReadOptions(options, [0, 1]));
972
+ return run.status === 1 ? null : run.stdout.trim();
973
+ });
200
974
  }
201
975
  //# sourceMappingURL=commit.js.map