@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.13

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 (901) hide show
  1. package/CHANGELOG.md +3448 -0
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +47 -23
  5. package/QUICKSTART.md +189 -12
  6. package/README.md +274 -65
  7. package/ROADMAP.md +572 -31
  8. package/SCHEMA.md +1709 -155
  9. package/WORKFLOW.md +298 -72
  10. package/dist/cli.js +26 -8
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt/adopt.js +5 -10
  13. package/dist/commands/adopt/adopt.js.map +1 -1
  14. package/dist/commands/adopt/render.js +50 -9
  15. package/dist/commands/adopt/render.js.map +1 -1
  16. package/dist/commands/archive/archive.js +16 -19
  17. package/dist/commands/archive/archive.js.map +1 -1
  18. package/dist/commands/archive/closing.js +42 -0
  19. package/dist/commands/archive/closing.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/asyncapi.js +36 -42
  21. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  23. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  24. package/dist/commands/archive/plan/contracts/openapi.js +77 -43
  25. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  26. package/dist/commands/archive/plan/contracts/verdict-lines.js +103 -0
  27. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  28. package/dist/commands/archive/plan/document/head.js +151 -0
  29. package/dist/commands/archive/plan/document/head.js.map +1 -0
  30. package/dist/commands/archive/plan/document/links.js +284 -0
  31. package/dist/commands/archive/plan/document/links.js.map +1 -0
  32. package/dist/commands/archive/plan/document/run.js +136 -0
  33. package/dist/commands/archive/plan/document/run.js.map +1 -0
  34. package/dist/commands/archive/plan/gate.js +21 -19
  35. package/dist/commands/archive/plan/gate.js.map +1 -1
  36. package/dist/commands/archive/plan/landscape.js +167 -30
  37. package/dist/commands/archive/plan/landscape.js.map +1 -1
  38. package/dist/commands/archive/plan/model/extending.js +85 -0
  39. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  40. package/dist/commands/archive/plan/refusal.js +14 -19
  41. package/dist/commands/archive/plan/refusal.js.map +1 -1
  42. package/dist/commands/archive/plan/specs.js +137 -56
  43. package/dist/commands/archive/plan/specs.js.map +1 -1
  44. package/dist/commands/archive/plan/state.js +3 -0
  45. package/dist/commands/archive/plan/state.js.map +1 -1
  46. package/dist/commands/archive/run.js +134 -30
  47. package/dist/commands/archive/run.js.map +1 -1
  48. package/dist/commands/archive/shipped.js +49 -0
  49. package/dist/commands/archive/shipped.js.map +1 -0
  50. package/dist/commands/context/context.js +85 -18
  51. package/dist/commands/context/context.js.map +1 -1
  52. package/dist/commands/context/print.js +29 -7
  53. package/dist/commands/context/print.js.map +1 -1
  54. package/dist/commands/delta/delta.js +102 -14
  55. package/dist/commands/delta/delta.js.map +1 -1
  56. package/dist/commands/delta/print.js +17 -1
  57. package/dist/commands/delta/print.js.map +1 -1
  58. package/dist/commands/dependencies.js +3 -7
  59. package/dist/commands/dependencies.js.map +1 -1
  60. package/dist/commands/diff/diff.js +20 -27
  61. package/dist/commands/diff/diff.js.map +1 -1
  62. package/dist/commands/doctor.js +60 -1
  63. package/dist/commands/doctor.js.map +1 -1
  64. package/dist/commands/explain/explain.js +89 -13
  65. package/dist/commands/explain/explain.js.map +1 -1
  66. package/dist/commands/explore.js +16 -10
  67. package/dist/commands/explore.js.map +1 -1
  68. package/dist/commands/gate/gate.js +45 -8
  69. package/dist/commands/gate/gate.js.map +1 -1
  70. package/dist/commands/gate/print.js +15 -3
  71. package/dist/commands/gate/print.js.map +1 -1
  72. package/dist/commands/gherkin/commit.js +13 -54
  73. package/dist/commands/gherkin/commit.js.map +1 -1
  74. package/dist/commands/gherkin/gherkin.js +36 -20
  75. package/dist/commands/gherkin/gherkin.js.map +1 -1
  76. package/dist/commands/gherkin/reconcile.js +10 -13
  77. package/dist/commands/gherkin/reconcile.js.map +1 -1
  78. package/dist/commands/init/first-hour.js +44 -0
  79. package/dist/commands/init/first-hour.js.map +1 -1
  80. package/dist/commands/init/init.js +18 -22
  81. package/dist/commands/init/init.js.map +1 -1
  82. package/dist/commands/list/campaign/campaign.js +4 -6
  83. package/dist/commands/list/campaign/campaign.js.map +1 -1
  84. package/dist/commands/list/json.js +5 -7
  85. package/dist/commands/list/json.js.map +1 -1
  86. package/dist/commands/list/list.js +44 -29
  87. package/dist/commands/list/list.js.map +1 -1
  88. package/dist/commands/list/print.js +52 -9
  89. package/dist/commands/list/print.js.map +1 -1
  90. package/dist/commands/list/suites/suites.js +140 -0
  91. package/dist/commands/list/suites/suites.js.map +1 -0
  92. package/dist/commands/list/views.js +37 -34
  93. package/dist/commands/list/views.js.map +1 -1
  94. package/dist/commands/mcp/dispatch.js +91 -36
  95. package/dist/commands/mcp/dispatch.js.map +1 -1
  96. package/dist/commands/mcp/mcp.js +56 -15
  97. package/dist/commands/mcp/mcp.js.map +1 -1
  98. package/dist/commands/mcp/serve.js.map +1 -1
  99. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  100. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  101. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  102. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  103. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  104. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  105. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  106. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  107. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  108. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  109. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  110. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  111. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  112. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  113. package/dist/commands/new/capability-template.js +118 -0
  114. package/dist/commands/new/capability-template.js.map +1 -0
  115. package/dist/commands/new/commit.js +8 -22
  116. package/dist/commands/new/commit.js.map +1 -1
  117. package/dist/commands/new/modifies.js +206 -0
  118. package/dist/commands/new/modifies.js.map +1 -0
  119. package/dist/commands/new/new.js +59 -14
  120. package/dist/commands/new/new.js.map +1 -1
  121. package/dist/commands/new/templates.js +48 -124
  122. package/dist/commands/new/templates.js.map +1 -1
  123. package/dist/commands/open.js +3 -7
  124. package/dist/commands/open.js.map +1 -1
  125. package/dist/commands/policy/gate.js +27 -0
  126. package/dist/commands/policy/gate.js.map +1 -1
  127. package/dist/commands/rebase/plan.js +2 -2
  128. package/dist/commands/rebase/plan.js.map +1 -1
  129. package/dist/commands/rebase/rebase.js +60 -35
  130. package/dist/commands/rebase/rebase.js.map +1 -1
  131. package/dist/commands/seed/commit.js +55 -26
  132. package/dist/commands/seed/commit.js.map +1 -1
  133. package/dist/commands/seed/plan.js +158 -13
  134. package/dist/commands/seed/plan.js.map +1 -1
  135. package/dist/commands/seed/seed.js +13 -11
  136. package/dist/commands/seed/seed.js.map +1 -1
  137. package/dist/commands/show/feature.js +9 -5
  138. package/dist/commands/show/feature.js.map +1 -1
  139. package/dist/commands/show/introduced.js +157 -0
  140. package/dist/commands/show/introduced.js.map +1 -0
  141. package/dist/commands/show/marks.js +35 -6
  142. package/dist/commands/show/marks.js.map +1 -1
  143. package/dist/commands/show/service.js +41 -33
  144. package/dist/commands/show/service.js.map +1 -1
  145. package/dist/commands/show/show.js +17 -7
  146. package/dist/commands/show/show.js.map +1 -1
  147. package/dist/commands/status/fleet-next.js +90 -0
  148. package/dist/commands/status/fleet-next.js.map +1 -0
  149. package/dist/commands/status/json.js +2 -0
  150. package/dist/commands/status/json.js.map +1 -1
  151. package/dist/commands/status/print.js +89 -37
  152. package/dist/commands/status/print.js.map +1 -1
  153. package/dist/commands/status/status.js +3 -7
  154. package/dist/commands/status/status.js.map +1 -1
  155. package/dist/commands/steps/steps.js +103 -23
  156. package/dist/commands/steps/steps.js.map +1 -1
  157. package/dist/commands/subsystem/edit.js +7 -7
  158. package/dist/commands/subsystem/edit.js.map +1 -1
  159. package/dist/commands/subsystem/move.js +106 -46
  160. package/dist/commands/subsystem/move.js.map +1 -1
  161. package/dist/commands/subsystem/query.js +2 -4
  162. package/dist/commands/subsystem/query.js.map +1 -1
  163. package/dist/commands/subsystem/subsystem.js +4 -7
  164. package/dist/commands/subsystem/subsystem.js.map +1 -1
  165. package/dist/commands/subsystem/sync.js +289 -74
  166. package/dist/commands/subsystem/sync.js.map +1 -1
  167. package/dist/commands/subsystem/txn/exclude.js +132 -0
  168. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  169. package/dist/commands/subsystem/txn/links.js +128 -0
  170. package/dist/commands/subsystem/txn/links.js.map +1 -0
  171. package/dist/commands/subsystem/txn/report.js +174 -0
  172. package/dist/commands/subsystem/txn/report.js.map +1 -0
  173. package/dist/commands/subsystem/txn/txn.js +147 -45
  174. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  175. package/dist/commands/subsystem/txn/views.js +200 -21
  176. package/dist/commands/subsystem/txn/views.js.map +1 -1
  177. package/dist/commands/tasks/actions.js +107 -0
  178. package/dist/commands/tasks/actions.js.map +1 -0
  179. package/dist/commands/tasks/render.js +253 -0
  180. package/dist/commands/tasks/render.js.map +1 -0
  181. package/dist/commands/tasks/tasks.js +119 -0
  182. package/dist/commands/tasks/tasks.js.map +1 -0
  183. package/dist/commands/unarchive/manifest.js +42 -26
  184. package/dist/commands/unarchive/manifest.js.map +1 -1
  185. package/dist/commands/unarchive/restore.js +83 -33
  186. package/dist/commands/unarchive/restore.js.map +1 -1
  187. package/dist/commands/unarchive/unarchive.js +22 -30
  188. package/dist/commands/unarchive/unarchive.js.map +1 -1
  189. package/dist/commands/validate/arch-coverage.js +78 -27
  190. package/dist/commands/validate/arch-coverage.js.map +1 -1
  191. package/dist/commands/validate/checks/fleet-shape.js +34 -126
  192. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  193. package/dist/commands/validate/checks/health-deps.js +9 -6
  194. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  195. package/dist/commands/validate/checks/introduced.js +102 -0
  196. package/dist/commands/validate/checks/introduced.js.map +1 -0
  197. package/dist/commands/validate/checks/requirements.js +18 -8
  198. package/dist/commands/validate/checks/requirements.js.map +1 -1
  199. package/dist/commands/validate/checks/sources.js +11 -2
  200. package/dist/commands/validate/checks/sources.js.map +1 -1
  201. package/dist/commands/validate/coverage/covers-after.js +105 -0
  202. package/dist/commands/validate/coverage/covers-after.js.map +1 -0
  203. package/dist/commands/validate/feature.js +26 -20
  204. package/dist/commands/validate/feature.js.map +1 -1
  205. package/dist/commands/validate/fleet/census.js +37 -13
  206. package/dist/commands/validate/fleet/census.js.map +1 -1
  207. package/dist/commands/validate/fleet/landscape.js +144 -126
  208. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  209. package/dist/commands/validate/fleet/load.js +66 -0
  210. package/dist/commands/validate/fleet/load.js.map +1 -1
  211. package/dist/commands/validate/fleet/map/attest.js +98 -0
  212. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  213. package/dist/commands/validate/fleet/map/bindings.js +107 -0
  214. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  215. package/dist/commands/validate/fleet/map/consumers.js +244 -0
  216. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  217. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  218. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  219. package/dist/commands/validate/fleet/map/usecases.js +54 -0
  220. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  221. package/dist/commands/validate/fleet/obligations.js +44 -14
  222. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  223. package/dist/commands/validate/fleet/scorecard/print.js +10 -2
  224. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  225. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  226. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  227. package/dist/commands/validate/fleet/usecases/place.js.map +1 -1
  228. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  229. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  230. package/dist/commands/validate/fleet/views/ids.js +50 -15
  231. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  232. package/dist/commands/validate/fleet/views/projects.js +278 -0
  233. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  234. package/dist/commands/validate/fleet/views/root-documents.js +83 -0
  235. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  236. package/dist/commands/validate/fleet/views/stale.js +40 -47
  237. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  238. package/dist/commands/validate/links/corpus.js +1 -4
  239. package/dist/commands/validate/links/corpus.js.map +1 -1
  240. package/dist/commands/validate/links/glossary.js.map +1 -1
  241. package/dist/commands/validate/render/repeats.js +63 -0
  242. package/dist/commands/validate/render/repeats.js.map +1 -0
  243. package/dist/commands/validate/render/rollups.js +115 -0
  244. package/dist/commands/validate/render/rollups.js.map +1 -0
  245. package/dist/commands/validate/render/text.js +199 -0
  246. package/dist/commands/validate/render/text.js.map +1 -0
  247. package/dist/commands/validate/report.js +36 -160
  248. package/dist/commands/validate/report.js.map +1 -1
  249. package/dist/commands/validate/scope.js +113 -0
  250. package/dist/commands/validate/scope.js.map +1 -0
  251. package/dist/commands/validate/service/api.js +21 -107
  252. package/dist/commands/validate/service/api.js.map +1 -1
  253. package/dist/commands/validate/service/contracts/generated.js +134 -0
  254. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  255. package/dist/commands/validate/service/events/events.js +36 -14
  256. package/dist/commands/validate/service/events/events.js.map +1 -1
  257. package/dist/commands/validate/service/events/message-producers.js +1 -1
  258. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  259. package/dist/commands/validate/service/evidence-pins.js +56 -10
  260. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  261. package/dist/commands/validate/service/model/diverged.js +226 -0
  262. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  263. package/dist/commands/validate/service/model/grade.js +163 -0
  264. package/dist/commands/validate/service/model/grade.js.map +1 -0
  265. package/dist/commands/validate/service/service.js +121 -61
  266. package/dist/commands/validate/service/service.js.map +1 -1
  267. package/dist/commands/validate/service/specs.js +88 -28
  268. package/dist/commands/validate/service/specs.js.map +1 -1
  269. package/dist/commands/validate/service/spine.js +38 -3
  270. package/dist/commands/validate/service/spine.js.map +1 -1
  271. package/dist/commands/validate/service/usecases/tags.js +113 -0
  272. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  273. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  274. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  275. package/dist/commands/validate/validate.js +85 -60
  276. package/dist/commands/validate/validate.js.map +1 -1
  277. package/dist/commands/verify/cross/diff.js +2 -6
  278. package/dist/commands/verify/cross/diff.js.map +1 -1
  279. package/dist/commands/verify/evidence/contract.js +1 -1
  280. package/dist/commands/verify/evidence/contract.js.map +1 -1
  281. package/dist/commands/verify/evidence/read.js +12 -3
  282. package/dist/commands/verify/evidence/read.js.map +1 -1
  283. package/dist/commands/verify/frozen.js +45 -23
  284. package/dist/commands/verify/frozen.js.map +1 -1
  285. package/dist/commands/verify/record.js +69 -17
  286. package/dist/commands/verify/record.js.map +1 -1
  287. package/dist/commands/verify/report.js +27 -44
  288. package/dist/commands/verify/report.js.map +1 -1
  289. package/dist/commands/verify/results.js +112 -30
  290. package/dist/commands/verify/results.js.map +1 -1
  291. package/dist/commands/verify/verify.js +55 -52
  292. package/dist/commands/verify/verify.js.map +1 -1
  293. package/dist/commands/vouch/pack/pack.js +2 -2
  294. package/dist/commands/vouch/pack/pack.js.map +1 -1
  295. package/dist/commands/vouch/pack/print.js +1 -6
  296. package/dist/commands/vouch/pack/print.js.map +1 -1
  297. package/dist/commands/vouch/pack/sections.js +1 -2
  298. package/dist/commands/vouch/pack/sections.js.map +1 -1
  299. package/dist/commands/vouch/run.js +26 -81
  300. package/dist/commands/vouch/run.js.map +1 -1
  301. package/dist/commands/vouch/sample/plan.js +20 -34
  302. package/dist/commands/vouch/sample/plan.js.map +1 -1
  303. package/dist/commands/vouch/vet/verify.js +39 -7
  304. package/dist/commands/vouch/vet/verify.js.map +1 -1
  305. package/dist/commands/vouch/vouch.js +10 -17
  306. package/dist/commands/vouch/vouch.js.map +1 -1
  307. package/dist/core/agent/agents-md/command-map.js +81 -106
  308. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  309. package/dist/core/agent/agents-md/map/explain.js +21 -9
  310. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  311. package/dist/core/agent/agents-md/map/lenses/context.js +17 -8
  312. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  313. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  314. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  315. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  316. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  317. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  318. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  319. package/dist/core/agent/agents-md/map/status.js +24 -8
  320. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  321. package/dist/core/agent/agents-md/map/subsystem.js +80 -8
  322. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  323. package/dist/core/agent/agents-md/map/usecases.js +88 -10
  324. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  325. package/dist/core/agent/agents-md/refusals.js +49 -22
  326. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  327. package/dist/core/agent/agents-md.js +39 -6
  328. package/dist/core/agent/agents-md.js.map +1 -1
  329. package/dist/core/agent/agents-stamp.js +10 -0
  330. package/dist/core/agent/agents-stamp.js.map +1 -1
  331. package/dist/core/agent/protocol.js +1 -1
  332. package/dist/core/agent/protocol.js.map +1 -1
  333. package/dist/core/agent/scaffold.js +10 -15
  334. package/dist/core/agent/scaffold.js.map +1 -1
  335. package/dist/core/agent/support/report.js +46 -5
  336. package/dist/core/agent/support/report.js.map +1 -1
  337. package/dist/core/agent/tools/dialects.js +1 -0
  338. package/dist/core/agent/tools/dialects.js.map +1 -1
  339. package/dist/core/agent/tools/registry.js +41 -76
  340. package/dist/core/agent/tools/registry.js.map +1 -1
  341. package/dist/core/agent/workflows/adopt.js +158 -11
  342. package/dist/core/agent/workflows/adopt.js.map +1 -1
  343. package/dist/core/agent/workflows/check.js +78 -39
  344. package/dist/core/agent/workflows/check.js.map +1 -1
  345. package/dist/core/agent/workflows/closing.js +88 -27
  346. package/dist/core/agent/workflows/closing.js.map +1 -1
  347. package/dist/core/agent/workflows/feature.js +128 -13
  348. package/dist/core/agent/workflows/feature.js.map +1 -1
  349. package/dist/core/agent/workflows/implement.js +96 -13
  350. package/dist/core/agent/workflows/implement.js.map +1 -1
  351. package/dist/core/agent/workflows/reference/authoring.js +106 -4
  352. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  353. package/dist/core/agent/workflows/reference/codes.js +8 -6
  354. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  355. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  356. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  357. package/dist/core/agent/workflows/reference/drawing/drawing.js +253 -0
  358. package/dist/core/agent/workflows/reference/drawing/drawing.js.map +1 -0
  359. package/dist/core/agent/workflows/reference/reference.js +6 -3
  360. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  361. package/dist/core/agent/workflows/reference/spine.js +68 -27
  362. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  363. package/dist/core/asyncapi/baseline/plan.js +14 -81
  364. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  365. package/dist/core/asyncapi/digest.js +30 -25
  366. package/dist/core/asyncapi/digest.js.map +1 -1
  367. package/dist/core/asyncapi/merge/components.js +5 -4
  368. package/dist/core/asyncapi/merge/components.js.map +1 -1
  369. package/dist/core/asyncapi/merge/error.js +7 -2
  370. package/dist/core/asyncapi/merge/error.js.map +1 -1
  371. package/dist/core/asyncapi/merge/markers.js +32 -50
  372. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  373. package/dist/core/asyncapi/merge/merge.js +33 -53
  374. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  375. package/dist/core/asyncapi/merge/pin.js +12 -36
  376. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  377. package/dist/core/asyncapi/model.js.map +1 -1
  378. package/dist/core/asyncapi/read.js +36 -12
  379. package/dist/core/asyncapi/read.js.map +1 -1
  380. package/dist/core/brief/brief.js +96 -36
  381. package/dist/core/brief/brief.js.map +1 -1
  382. package/dist/core/brief/checks.js +138 -3
  383. package/dist/core/brief/checks.js.map +1 -1
  384. package/dist/core/brief/landscape.js +209 -130
  385. package/dist/core/brief/landscape.js.map +1 -1
  386. package/dist/core/brief/map/extend.js +84 -0
  387. package/dist/core/brief/map/extend.js.map +1 -0
  388. package/dist/core/brief/map/owed.js +266 -0
  389. package/dist/core/brief/map/owed.js.map +1 -0
  390. package/dist/core/brief/targets.js +109 -31
  391. package/dist/core/brief/targets.js.map +1 -1
  392. package/dist/core/brief/unchecked.js +14 -3
  393. package/dist/core/brief/unchecked.js.map +1 -1
  394. package/dist/core/c4/arch.js +90 -182
  395. package/dist/core/c4/arch.js.map +1 -1
  396. package/dist/core/c4/direction/direction.js +114 -0
  397. package/dist/core/c4/direction/direction.js.map +1 -0
  398. package/dist/core/c4/likec4.js +169 -14
  399. package/dist/core/c4/likec4.js.map +1 -1
  400. package/dist/core/c4/locate/unlocated.js +150 -0
  401. package/dist/core/c4/locate/unlocated.js.map +1 -0
  402. package/dist/core/c4/parsed/deployment.js +160 -0
  403. package/dist/core/c4/parsed/deployment.js.map +1 -0
  404. package/dist/core/c4/parsed/dynamic-views.js.map +1 -1
  405. package/dist/core/c4/parsed/values.js +54 -1
  406. package/dist/core/c4/parsed/values.js.map +1 -1
  407. package/dist/core/c4/parsed/view-ids.js +39 -1
  408. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  409. package/dist/core/c4/prefetch/fleet.js +134 -0
  410. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  411. package/dist/core/c4/project/architecture.js +123 -7
  412. package/dist/core/c4/project/architecture.js.map +1 -1
  413. package/dist/core/c4/project/batch.js +140 -0
  414. package/dist/core/c4/project/batch.js.map +1 -0
  415. package/dist/core/c4/project/documents.js +43 -12
  416. package/dist/core/c4/project/documents.js.map +1 -1
  417. package/dist/core/c4/project/load.js +79 -43
  418. package/dist/core/c4/project/load.js.map +1 -1
  419. package/dist/core/c4/project/staged.js +277 -0
  420. package/dist/core/c4/project/staged.js.map +1 -0
  421. package/dist/core/c4/resolve/attested.js +0 -0
  422. package/dist/core/c4/resolve/attested.js.map +1 -0
  423. package/dist/core/c4/resolve/external.js +51 -0
  424. package/dist/core/c4/resolve/external.js.map +1 -0
  425. package/dist/core/c4/resolve/service.js +78 -17
  426. package/dist/core/c4/resolve/service.js.map +1 -1
  427. package/dist/core/c4/resolve/steps.js +290 -0
  428. package/dist/core/c4/resolve/steps.js.map +1 -0
  429. package/dist/core/c4/resolve/systems.js +154 -0
  430. package/dist/core/c4/resolve/systems.js.map +1 -0
  431. package/dist/core/c4/root-project/exclude.js +352 -0
  432. package/dist/core/c4/root-project/exclude.js.map +1 -0
  433. package/dist/core/c4/seed/template.js +65 -1
  434. package/dist/core/c4/seed/template.js.map +1 -1
  435. package/dist/core/c4/service-model/created/models.js +96 -0
  436. package/dist/core/c4/service-model/created/models.js.map +1 -0
  437. package/dist/core/c4/service-model/documents.js +47 -0
  438. package/dist/core/c4/service-model/documents.js.map +1 -0
  439. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  440. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  441. package/dist/core/c4/service-model/load.js +165 -0
  442. package/dist/core/c4/service-model/load.js.map +1 -0
  443. package/dist/core/c4/service-model/renderer.js +128 -0
  444. package/dist/core/c4/service-model/renderer.js.map +1 -0
  445. package/dist/core/c4/service-model/shape.js +106 -0
  446. package/dist/core/c4/service-model/shape.js.map +1 -0
  447. package/dist/core/c4/service-model/slice.js +188 -0
  448. package/dist/core/c4/service-model/slice.js.map +1 -0
  449. package/dist/core/c4/source-mask.js +297 -10
  450. package/dist/core/c4/source-mask.js.map +1 -1
  451. package/dist/core/c4/source-scan.js +222 -141
  452. package/dist/core/c4/source-scan.js.map +1 -1
  453. package/dist/core/c4/splice/authored-source.js +79 -13
  454. package/dist/core/c4/splice/authored-source.js.map +1 -1
  455. package/dist/core/c4/splice/contract.js +20 -0
  456. package/dist/core/c4/splice/contract.js.map +1 -1
  457. package/dist/core/c4/splice/delta-blocks.js +20 -20
  458. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  459. package/dist/core/c4/splice/identity/declared.js +87 -0
  460. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  461. package/dist/core/c4/splice/identity/edges.js +37 -0
  462. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  463. package/dist/core/c4/splice/identity/existing.js +80 -0
  464. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  465. package/dist/core/c4/splice/identity/interior.js +80 -0
  466. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  467. package/dist/core/c4/splice/identity/owner.js +50 -0
  468. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  469. package/dist/core/c4/splice/landscape-merge.js +182 -114
  470. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  471. package/dist/core/c4/splice/model/merge.js +151 -0
  472. package/dist/core/c4/splice/model/merge.js.map +1 -0
  473. package/dist/core/c4/splice/placement.js +160 -56
  474. package/dist/core/c4/splice/placement.js.map +1 -1
  475. package/dist/core/c4/splice/text/spot.js +131 -0
  476. package/dist/core/c4/splice/text/spot.js.map +1 -0
  477. package/dist/core/c4/workspace.js +13 -19
  478. package/dist/core/c4/workspace.js.map +1 -1
  479. package/dist/core/capabilities/capabilities.js +6 -6
  480. package/dist/core/capabilities/capabilities.js.map +1 -1
  481. package/dist/core/capabilities/delta/doc.js +7 -10
  482. package/dist/core/capabilities/delta/doc.js.map +1 -1
  483. package/dist/core/capabilities/delta/uncovered.js +2 -2
  484. package/dist/core/capabilities/delta/uncovered.js.map +1 -1
  485. package/dist/core/capabilities/findings.js +18 -22
  486. package/dist/core/capabilities/findings.js.map +1 -1
  487. package/dist/core/capabilities/realizes/findings.js +12 -19
  488. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  489. package/dist/core/capabilities/realizes/join.js +0 -20
  490. package/dist/core/capabilities/realizes/join.js.map +1 -1
  491. package/dist/core/capabilities/rollup.js +6 -0
  492. package/dist/core/capabilities/rollup.js.map +1 -1
  493. package/dist/core/capabilities/tree.js +6 -28
  494. package/dist/core/capabilities/tree.js.map +1 -1
  495. package/dist/core/capabilities/usecase-join.js +3 -13
  496. package/dist/core/capabilities/usecase-join.js.map +1 -1
  497. package/dist/core/coherence/authoring/scaffold.js +11 -3
  498. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  499. package/dist/core/coherence/authoring/sentinels.js +2 -1
  500. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  501. package/dist/core/coherence/calls/called.js +52 -0
  502. package/dist/core/coherence/calls/called.js.map +1 -0
  503. package/dist/core/coherence/coherence.js +103 -22
  504. package/dist/core/coherence/coherence.js.map +1 -1
  505. package/dist/core/coherence/consumed/operations.js +70 -0
  506. package/dist/core/coherence/consumed/operations.js.map +1 -0
  507. package/dist/core/coherence/consumed/refusal.js +38 -0
  508. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  509. package/dist/core/coherence/covers/health.js +85 -0
  510. package/dist/core/coherence/covers/health.js.map +1 -0
  511. package/dist/core/coherence/declared.js +60 -34
  512. package/dist/core/coherence/declared.js.map +1 -1
  513. package/dist/core/coherence/events/declared.js +44 -59
  514. package/dist/core/coherence/events/declared.js.map +1 -1
  515. package/dist/core/coherence/events/events.js +147 -16
  516. package/dist/core/coherence/events/events.js.map +1 -1
  517. package/dist/core/coherence/events/grades.js +31 -8
  518. package/dist/core/coherence/events/grades.js.map +1 -1
  519. package/dist/core/coherence/events/lookups.js +28 -87
  520. package/dist/core/coherence/events/lookups.js.map +1 -1
  521. package/dist/core/coherence/events/produced.js +100 -0
  522. package/dist/core/coherence/events/produced.js.map +1 -0
  523. package/dist/core/coherence/lookups.js +18 -78
  524. package/dist/core/coherence/lookups.js.map +1 -1
  525. package/dist/core/coherence/retired/archived.js +285 -0
  526. package/dist/core/coherence/retired/archived.js.map +1 -0
  527. package/dist/core/coherence/retired/event-pair.js +62 -0
  528. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  529. package/dist/core/coherence/retired/merged.js +79 -0
  530. package/dist/core/coherence/retired/merged.js.map +1 -0
  531. package/dist/core/coherence/retired/retirement.js +49 -0
  532. package/dist/core/coherence/retired/retirement.js.map +1 -0
  533. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  534. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  535. package/dist/core/coherence/retired/revival/revived.js +297 -0
  536. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  537. package/dist/core/consumers/after.js +113 -0
  538. package/dist/core/consumers/after.js.map +1 -0
  539. package/dist/core/consumers/consumers.js +296 -0
  540. package/dist/core/consumers/consumers.js.map +1 -0
  541. package/dist/core/consumers/current.js +211 -0
  542. package/dist/core/consumers/current.js.map +1 -0
  543. package/dist/core/consumers/scan.js +46 -0
  544. package/dist/core/consumers/scan.js.map +1 -0
  545. package/dist/core/consumers/state.js +27 -0
  546. package/dist/core/consumers/state.js.map +1 -0
  547. package/dist/core/delta/delta.js +51 -6
  548. package/dist/core/delta/delta.js.map +1 -1
  549. package/dist/core/dependencies/fanin.js +2 -2
  550. package/dist/core/dependencies/fanin.js.map +1 -1
  551. package/dist/core/deployment/delta.js +191 -0
  552. package/dist/core/deployment/delta.js.map +1 -0
  553. package/dist/core/deployment/objects.js +145 -0
  554. package/dist/core/deployment/objects.js.map +1 -0
  555. package/dist/core/diff/base-git.js +94 -12
  556. package/dist/core/diff/base-git.js.map +1 -1
  557. package/dist/core/diff/base-state.js +49 -21
  558. package/dist/core/diff/base-state.js.map +1 -1
  559. package/dist/core/diff/joins.js +136 -0
  560. package/dist/core/diff/joins.js.map +1 -0
  561. package/dist/core/diff/scope/changed-paths.js +22 -6
  562. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  563. package/dist/core/diff/scope/edges.js +82 -0
  564. package/dist/core/diff/scope/edges.js.map +1 -0
  565. package/dist/core/diff/semantic.js +100 -31
  566. package/dist/core/diff/semantic.js.map +1 -1
  567. package/dist/core/docs.js +31 -22
  568. package/dist/core/docs.js.map +1 -1
  569. package/dist/core/doctor/agents.js +34 -18
  570. package/dist/core/doctor/agents.js.map +1 -1
  571. package/dist/core/doctor/config.js +7 -3
  572. package/dist/core/doctor/config.js.map +1 -1
  573. package/dist/core/doctor/doctor.js +91 -11
  574. package/dist/core/doctor/doctor.js.map +1 -1
  575. package/dist/core/doctor/report.js.map +1 -1
  576. package/dist/core/doctor/reports/scan.js +242 -0
  577. package/dist/core/doctor/reports/scan.js.map +1 -0
  578. package/dist/core/document/apply.js +158 -11
  579. package/dist/core/document/apply.js.map +1 -1
  580. package/dist/core/document/frontmatter.js +55 -0
  581. package/dist/core/document/frontmatter.js.map +1 -1
  582. package/dist/core/document/parse.js +29 -6
  583. package/dist/core/document/parse.js.map +1 -1
  584. package/dist/core/document/scenarios.js +17 -0
  585. package/dist/core/document/scenarios.js.map +1 -1
  586. package/dist/core/document/spec.js +4 -11
  587. package/dist/core/document/spec.js.map +1 -1
  588. package/dist/core/envelope/config.js +10 -0
  589. package/dist/core/envelope/config.js.map +1 -1
  590. package/dist/core/envelope/json.js +34 -0
  591. package/dist/core/envelope/json.js.map +1 -1
  592. package/dist/core/explain/families.js +26 -21
  593. package/dist/core/explain/families.js.map +1 -1
  594. package/dist/core/explain/lookup.js +14 -1
  595. package/dist/core/explain/lookup.js.map +1 -1
  596. package/dist/core/explain/refusals.js +5 -5
  597. package/dist/core/explain/refusals.js.map +1 -1
  598. package/dist/core/explain/terms/documents.js +227 -0
  599. package/dist/core/explain/terms/documents.js.map +1 -0
  600. package/dist/core/explain/terms/done-check.js +144 -0
  601. package/dist/core/explain/terms/done-check.js.map +1 -0
  602. package/dist/core/explain/terms/entry.js +14 -0
  603. package/dist/core/explain/terms/entry.js.map +1 -0
  604. package/dist/core/explain/terms/stages.js +71 -0
  605. package/dist/core/explain/terms/stages.js.map +1 -0
  606. package/dist/core/explain/terms/tasks.js +97 -0
  607. package/dist/core/explain/terms/tasks.js.map +1 -0
  608. package/dist/core/explain/terms.js +68 -109
  609. package/dist/core/explain/terms.js.map +1 -1
  610. package/dist/core/explore/explore.js +40 -10
  611. package/dist/core/explore/explore.js.map +1 -1
  612. package/dist/core/fleet-context.js +81 -95
  613. package/dist/core/fleet-context.js.map +1 -1
  614. package/dist/core/gate/checks.js +11 -4
  615. package/dist/core/gate/checks.js.map +1 -1
  616. package/dist/core/gate/partners.js +69 -28
  617. package/dist/core/gate/partners.js.map +1 -1
  618. package/dist/core/gate/report.js.map +1 -1
  619. package/dist/core/gate/verification.js +75 -12
  620. package/dist/core/gate/verification.js.map +1 -1
  621. package/dist/core/gherkin/stale.js +13 -37
  622. package/dist/core/gherkin/stale.js.map +1 -1
  623. package/dist/core/glossary/delta.js +10 -7
  624. package/dist/core/glossary/delta.js.map +1 -1
  625. package/dist/core/glossary/tree.js +6 -19
  626. package/dist/core/glossary/tree.js.map +1 -1
  627. package/dist/core/kernel/concurrency.js +19 -0
  628. package/dist/core/kernel/concurrency.js.map +1 -1
  629. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  630. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  631. package/dist/core/kernel/path-safety.js +38 -0
  632. package/dist/core/kernel/path-safety.js.map +1 -1
  633. package/dist/core/kernel/records.js +11 -0
  634. package/dist/core/kernel/records.js.map +1 -1
  635. package/dist/core/links/backlinks.js +1 -3
  636. package/dist/core/links/backlinks.js.map +1 -1
  637. package/dist/core/links/corpus.js +51 -37
  638. package/dist/core/links/corpus.js.map +1 -1
  639. package/dist/core/links/findings.js.map +1 -1
  640. package/dist/core/links/parse.js +38 -9
  641. package/dist/core/links/parse.js.map +1 -1
  642. package/dist/core/mcp/argv.js +101 -0
  643. package/dist/core/mcp/argv.js.map +1 -1
  644. package/dist/core/mcp/author-tools.js +2 -1
  645. package/dist/core/mcp/author-tools.js.map +1 -1
  646. package/dist/core/mcp/protocol.js +2 -2
  647. package/dist/core/mcp/protocol.js.map +1 -1
  648. package/dist/core/mcp/tools.js +43 -33
  649. package/dist/core/mcp/tools.js.map +1 -1
  650. package/dist/core/openapi/baseline/gate.js +53 -12
  651. package/dist/core/openapi/baseline/gate.js.map +1 -1
  652. package/dist/core/openapi/baseline/plan.js +40 -10
  653. package/dist/core/openapi/baseline/plan.js.map +1 -1
  654. package/dist/core/openapi/baseline/record.js +0 -1
  655. package/dist/core/openapi/baseline/record.js.map +1 -1
  656. package/dist/core/openapi/contract-text.js +33 -0
  657. package/dist/core/openapi/contract-text.js.map +1 -0
  658. package/dist/core/openapi/contract-tree.js +301 -0
  659. package/dist/core/openapi/contract-tree.js.map +1 -0
  660. package/dist/core/openapi/depth.js +38 -24
  661. package/dist/core/openapi/depth.js.map +1 -1
  662. package/dist/core/openapi/digest.js +52 -2
  663. package/dist/core/openapi/digest.js.map +1 -1
  664. package/dist/core/openapi/doc.js +12 -4
  665. package/dist/core/openapi/doc.js.map +1 -1
  666. package/dist/core/openapi/merge/components.js +25 -60
  667. package/dist/core/openapi/merge/components.js.map +1 -1
  668. package/dist/core/openapi/merge/error.js +3 -13
  669. package/dist/core/openapi/merge/error.js.map +1 -1
  670. package/dist/core/openapi/merge/markers.js +26 -30
  671. package/dist/core/openapi/merge/markers.js.map +1 -1
  672. package/dist/core/openapi/merge/merge.js +28 -52
  673. package/dist/core/openapi/merge/merge.js.map +1 -1
  674. package/dist/core/openapi/merge/pin.js +17 -35
  675. package/dist/core/openapi/merge/pin.js.map +1 -1
  676. package/dist/core/openspec/decide/verdict.js.map +1 -1
  677. package/dist/core/openspec/inventory.js +4 -5
  678. package/dist/core/openspec/inventory.js.map +1 -1
  679. package/dist/core/openspec/model/mapping.js +30 -28
  680. package/dist/core/openspec/model/mapping.js.map +1 -1
  681. package/dist/core/openspec/model/model.js +12 -7
  682. package/dist/core/openspec/model/model.js.map +1 -1
  683. package/dist/core/openspec/scan/changes.js +10 -4
  684. package/dist/core/openspec/scan/changes.js.map +1 -1
  685. package/dist/core/openspec/scan/config.js +7 -16
  686. package/dist/core/openspec/scan/config.js.map +1 -1
  687. package/dist/core/openspec/scan/shape.js +63 -19
  688. package/dist/core/openspec/scan/shape.js.map +1 -1
  689. package/dist/core/openspec/scan/walk.js +21 -38
  690. package/dist/core/openspec/scan/walk.js.map +1 -1
  691. package/dist/core/pack/living.js +43 -4
  692. package/dist/core/pack/living.js.map +1 -1
  693. package/dist/core/pack/pack.js +50 -6
  694. package/dist/core/pack/pack.js.map +1 -1
  695. package/dist/core/projection/arch-slice.js +2 -2
  696. package/dist/core/projection/arch-slice.js.map +1 -1
  697. package/dist/core/projection/introduced.js +14 -0
  698. package/dist/core/projection/introduced.js.map +1 -0
  699. package/dist/core/provenance/findings.js +48 -12
  700. package/dist/core/provenance/findings.js.map +1 -1
  701. package/dist/core/provenance/git.js +33 -57
  702. package/dist/core/provenance/git.js.map +1 -1
  703. package/dist/core/provenance/gitq/moves.js +71 -114
  704. package/dist/core/provenance/gitq/moves.js.map +1 -1
  705. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  706. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  707. package/dist/core/provenance/sources.js +23 -6
  708. package/dist/core/provenance/sources.js.map +1 -1
  709. package/dist/core/provenance/stamp.js +5 -17
  710. package/dist/core/provenance/stamp.js.map +1 -1
  711. package/dist/core/repo/authored/paths.js +18 -0
  712. package/dist/core/repo/authored/paths.js.map +1 -1
  713. package/dist/core/repo/entries.js.map +1 -1
  714. package/dist/core/repo/paths.js +92 -6
  715. package/dist/core/repo/paths.js.map +1 -1
  716. package/dist/core/repo/repo.js +74 -14
  717. package/dist/core/repo/repo.js.map +1 -1
  718. package/dist/core/repo/tree/find.js +12 -3
  719. package/dist/core/repo/tree/find.js.map +1 -1
  720. package/dist/core/repo/tree/fs.js +27 -1
  721. package/dist/core/repo/tree/fs.js.map +1 -1
  722. package/dist/core/repo/tree/render/body.js +258 -0
  723. package/dist/core/repo/tree/render/body.js.map +1 -0
  724. package/dist/core/repo/tree/render/projects.js +182 -0
  725. package/dist/core/repo/tree/render/projects.js.map +1 -0
  726. package/dist/core/repo/tree/render/service/header.js +125 -0
  727. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  728. package/dist/core/repo/tree/render/service/peers.js +28 -0
  729. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  730. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  731. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  732. package/dist/core/repo/tree/render/service/views.js +78 -0
  733. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  734. package/dist/core/repo/tree/render/stale.js +42 -0
  735. package/dist/core/repo/tree/render/stale.js.map +1 -0
  736. package/dist/core/repo/tree/render/text.js +84 -0
  737. package/dist/core/repo/tree/render/text.js.map +1 -0
  738. package/dist/core/repo/tree/render/views.js +293 -0
  739. package/dist/core/repo/tree/render/views.js.map +1 -0
  740. package/dist/core/repo/tree/walk.js +53 -32
  741. package/dist/core/repo/tree/walk.js.map +1 -1
  742. package/dist/core/results.js +47 -12
  743. package/dist/core/results.js.map +1 -1
  744. package/dist/core/scaffold/landscape.js +121 -13
  745. package/dist/core/scaffold/landscape.js.map +1 -1
  746. package/dist/core/scaffold/legacy.js +271 -0
  747. package/dist/core/scaffold/legacy.js.map +1 -0
  748. package/dist/core/staging/commit.js +26 -15
  749. package/dist/core/staging/commit.js.map +1 -1
  750. package/dist/core/staging/lock.js +74 -12
  751. package/dist/core/staging/lock.js.map +1 -1
  752. package/dist/core/staging/recovery/intent.js +2 -5
  753. package/dist/core/staging/recovery/intent.js.map +1 -1
  754. package/dist/core/staging/recovery/recover.js +84 -6
  755. package/dist/core/staging/recovery/recover.js.map +1 -1
  756. package/dist/core/staging/snapshot/manifest.js +188 -0
  757. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  758. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +39 -45
  759. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  760. package/dist/core/staging/txn/forward.js +24 -14
  761. package/dist/core/staging/txn/forward.js.map +1 -1
  762. package/dist/core/staging/writes.js.map +1 -1
  763. package/dist/core/status/actions/execution.js +11 -0
  764. package/dist/core/status/actions/execution.js.map +1 -1
  765. package/dist/core/status/contracts.js +39 -15
  766. package/dist/core/status/contracts.js.map +1 -1
  767. package/dist/core/status/feature/artifacts.js +33 -8
  768. package/dist/core/status/feature/artifacts.js.map +1 -1
  769. package/dist/core/status/feature/feature.js +24 -17
  770. package/dist/core/status/feature/feature.js.map +1 -1
  771. package/dist/core/status/feature/next.js +20 -7
  772. package/dist/core/status/feature/next.js.map +1 -1
  773. package/dist/core/status/feature/words.js +96 -0
  774. package/dist/core/status/feature/words.js.map +1 -0
  775. package/dist/core/status/fleet/fleet.js +43 -14
  776. package/dist/core/status/fleet/fleet.js.map +1 -1
  777. package/dist/core/status/fleet/next.js +153 -58
  778. package/dist/core/status/fleet/next.js.map +1 -1
  779. package/dist/core/status/report.js.map +1 -1
  780. package/dist/core/status/scan.js.map +1 -1
  781. package/dist/core/status/verification.js +30 -15
  782. package/dist/core/status/verification.js.map +1 -1
  783. package/dist/core/tasks/packet.js +199 -0
  784. package/dist/core/tasks/packet.js.map +1 -0
  785. package/dist/core/tasks/record.js +43 -0
  786. package/dist/core/tasks/record.js.map +1 -0
  787. package/dist/core/tasks/sources.js +181 -0
  788. package/dist/core/tasks/sources.js.map +1 -0
  789. package/dist/core/tasks/stamps.js +76 -0
  790. package/dist/core/tasks/stamps.js.map +1 -0
  791. package/dist/core/tasks/unit.js +0 -0
  792. package/dist/core/tasks/unit.js.map +1 -0
  793. package/dist/core/usecases/capability.js +11 -14
  794. package/dist/core/usecases/capability.js.map +1 -1
  795. package/dist/core/usecases/delta/claims.js +31 -7
  796. package/dist/core/usecases/delta/claims.js.map +1 -1
  797. package/dist/core/usecases/delta/flows.js +5 -7
  798. package/dist/core/usecases/delta/flows.js.map +1 -1
  799. package/dist/core/usecases/delta/modified.js +315 -0
  800. package/dist/core/usecases/delta/modified.js.map +1 -0
  801. package/dist/core/usecases/delta/overlay.js +32 -191
  802. package/dist/core/usecases/delta/overlay.js.map +1 -1
  803. package/dist/core/usecases/fleet.js +195 -36
  804. package/dist/core/usecases/fleet.js.map +1 -1
  805. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/capability-tag.js +8 -9
  806. package/dist/core/usecases/grade/capability-tag.js.map +1 -0
  807. package/dist/core/usecases/grade/feature.js +88 -0
  808. package/dist/core/usecases/grade/feature.js.map +1 -0
  809. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/requirement-tag.js +3 -4
  810. package/dist/core/usecases/grade/requirement-tag.js.map +1 -0
  811. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/steps.js +102 -33
  812. package/dist/core/usecases/grade/steps.js.map +1 -0
  813. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/usecases.js +23 -10
  814. package/dist/core/usecases/grade/usecases.js.map +1 -0
  815. package/dist/core/usecases/operations.js +26 -14
  816. package/dist/core/usecases/operations.js.map +1 -1
  817. package/dist/core/usecases/place.js.map +1 -1
  818. package/dist/core/usecases/service/exclude.js +119 -0
  819. package/dist/core/usecases/service/exclude.js.map +1 -0
  820. package/dist/core/usecases/service/flows.js +164 -0
  821. package/dist/core/usecases/service/flows.js.map +1 -0
  822. package/dist/core/usecases/suites/participants.js +83 -0
  823. package/dist/core/usecases/suites/participants.js.map +1 -0
  824. package/dist/core/usecases/suites/suites.js +45 -0
  825. package/dist/core/usecases/suites/suites.js.map +1 -0
  826. package/dist/core/verify/answers.js +12 -5
  827. package/dist/core/verify/answers.js.map +1 -1
  828. package/dist/core/verify/build.js +102 -82
  829. package/dist/core/verify/build.js.map +1 -1
  830. package/dist/core/verify/checklist.js +70 -12
  831. package/dist/core/verify/checklist.js.map +1 -1
  832. package/dist/core/verify/location.js +67 -0
  833. package/dist/core/verify/location.js.map +1 -0
  834. package/dist/core/verify/pins/lint.js +3 -2
  835. package/dist/core/verify/pins/lint.js.map +1 -1
  836. package/dist/core/verify/pins/pin.js +1 -1
  837. package/dist/core/verify/pins/pin.js.map +1 -1
  838. package/dist/core/verify/read/file.js +224 -0
  839. package/dist/core/verify/read/file.js.map +1 -0
  840. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  841. package/dist/core/verify/read/shape.js.map +1 -0
  842. package/dist/core/verify/read/union.js +105 -0
  843. package/dist/core/verify/read/union.js.map +1 -0
  844. package/dist/core/verify/record.js +35 -12
  845. package/dist/core/verify/record.js.map +1 -1
  846. package/dist/core/verify/store/commit.js +40 -30
  847. package/dist/core/verify/store/commit.js.map +1 -1
  848. package/dist/core/verify/store/render.js +8 -4
  849. package/dist/core/verify/store/render.js.map +1 -1
  850. package/dist/core/vocabulary/issue.js +4 -0
  851. package/dist/core/vocabulary/issue.js.map +1 -1
  852. package/dist/core/vocabulary/maturity.js +75 -4
  853. package/dist/core/vocabulary/maturity.js.map +1 -1
  854. package/dist/core/vocabulary/report.js +16 -1
  855. package/dist/core/vocabulary/report.js.map +1 -1
  856. package/examples/README.md +54 -11
  857. package/examples/docs/architecture/landscape.likec4 +62 -6
  858. package/examples/docs/architecture/services.likec4 +70 -0
  859. package/examples/docs/architecture/subsystems.likec4 +7 -0
  860. package/examples/docs/architecture/usecases/order-notification.likec4 +16 -7
  861. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  862. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  863. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +4 -1
  864. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  865. package/examples/docs/likec4.config.json +0 -1
  866. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  867. package/examples/docs/services/order-service/arch.spec.md +13 -6
  868. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  869. package/examples/docs/services/order-service/model.likec4 +58 -70
  870. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  871. package/examples/docs/services/payment-service/model.likec4 +38 -37
  872. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  873. package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
  874. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  875. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  876. package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
  877. package/package.json +1 -1
  878. package/dist/commands/validate/checks/vocabulary.js +0 -31
  879. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  880. package/dist/commands/validate/fleet/usecases/capability-tag.js.map +0 -1
  881. package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +0 -1
  882. package/dist/commands/validate/fleet/usecases/steps.js.map +0 -1
  883. package/dist/commands/validate/fleet/usecases/usecases.js.map +0 -1
  884. package/dist/core/agent/agents-md/artifacts.js +0 -220
  885. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  886. package/dist/core/agent/agents-md/cycle.js +0 -165
  887. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  888. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  889. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  890. package/dist/core/agent/agents-md/spine.js +0 -40
  891. package/dist/core/agent/agents-md/spine.js.map +0 -1
  892. package/dist/core/coherence/events/merged.js +0 -40
  893. package/dist/core/coherence/events/merged.js.map +0 -1
  894. package/dist/core/diff/current-state.js +0 -96
  895. package/dist/core/diff/current-state.js.map +0 -1
  896. package/dist/core/diff/victims.js +0 -247
  897. package/dist/core/diff/victims.js.map +0 -1
  898. package/dist/core/repo/tree/views.js +0 -172
  899. package/dist/core/repo/tree/views.js.map +0 -1
  900. package/dist/core/staging/snapshot.js.map +0 -1
  901. package/dist/core/verify/file.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"commit.js","sourceRoot":"","sources":["../../../src/core/staging/commit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The commit itself, in three steps: stage every new version beside its\n * target, swap them in, roll back what already went in when a later step fails.\n *\n * This is what `archive` and `unarchive` share, and it moved out of archive.ts\n * verbatim once unarchive became its second consumer. The MERGE computations\n * stayed with archive, which is still their only caller — the split was about a\n * second CONSUMER, never about archive being long.\n *\n * `StagingRaceError` belongs here rather than with the lock, though both are\n * about a second writer: the lock is taken before a plan is computed, while\n * this is the compare-and-set immediately before each rename — the second line\n * of defence for exactly the window two racers who both broke the same stale\n * lock would land in.\n */\nimport { existsSync } from \"node:fs\";\nimport { link, mkdir, readFile, rename, rm, rmdir, unlink, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { sameBytes, toBytes, type PlannedWrite, type StagedWrite } from \"./writes.js\";\n\n/**\n * The target changed between the pre-image staging read and the swap.\n *\n * The pre-image is what a rollback restores and what `unarchive` is handed, so\n * swapping over somebody else's newer bytes would both lose their work and\n * arm a rollback that writes a third version nobody wrote. Commands map this to\n * `merge-failed`: nothing of theirs went in, the docs are as the other writer\n * left them, and re-running can work.\n */\nexport class StagingRaceError extends Error {\n override readonly name = \"StagingRaceError\";\n\n constructor(readonly path: string) {\n super(`${path} changed while this command was planning its merge — nothing was written; re-run`);\n }\n}\n\n/**\n * Write every new version to a temp file beside its target, remembering what the\n * target said first. Nothing observable changes here: on failure the temp files\n * are removed and the docs are exactly as they were.\n *\n * The pre-image is read eagerly and deliberately: it is what a rollback restores\n * and what `unarchive` is given, so a file we cannot read is a file we cannot\n * safely rewrite, and the merge must stop before it starts.\n */\nexport async function stageWrites(writes: PlannedWrite[]): Promise<StagedWrite[]> {\n const staged: StagedWrite[] = [];\n try {\n for (const [i, write] of writes.entries()) {\n // mkdir(recursive) reports the topmost directory it had to create — the\n // handle a rollback needs to put the tree back the way it found it.\n const createdDir = (await mkdir(dirname(write.path), { recursive: true })) ?? null;\n const before = existsSync(write.path) ? await readFile(write.path) : null;\n const content = toBytes(write.content);\n let tmp: string | null = null;\n if (content !== null) {\n tmp = tempPath(write.path, i);\n await writeFile(tmp, content);\n }\n staged.push({ write, content, tmp, before, createdDir, swapped: false });\n }\n } catch (err) {\n await discardStaged(staged);\n throw err;\n }\n return staged;\n}\n\n/**\n * Swap the staged versions in, one file at a time. Each swap is a rename(2)\n * within a single directory, so a concurrent reader sees either the old bytes or\n * the new ones — never a half-written file. Across files it is not atomic: the\n * caller rolls back what has already gone in.\n *\n * Every swap re-reads its target first and compares it with the pre-image the\n * plan was computed from — a compare-and-set on the file's whole content. It is\n * half a CAS on purpose: read and rename cannot be one syscall, so a writer\n * landing inside that window still wins. What it does buy is the window that\n * actually matters — the seconds a plan spends parsing LikeC4 and merging YAML\n * between the pre-image read and this swap — and it costs one read per file.\n */\nexport async function swapStaged(staged: StagedWrite[]): Promise<void> {\n for (const s of staged) {\n const now = existsSync(s.write.path) ? await readFile(s.write.path) : null;\n if (!sameBytes(now, s.before)) throw new StagingRaceError(s.write.path);\n if (s.tmp === null) {\n if (s.before !== null) await unlink(s.write.path);\n } else if (s.write.exclusive === true) {\n // link(2) is the same-filesystem, atomic no-clobber counterpart to rename:\n // it fails with EEXIST instead of replacing a file created after planning.\n await link(s.tmp, s.write.path);\n s.swapped = true;\n await unlink(s.tmp);\n s.tmp = null;\n continue;\n } else {\n await rename(s.tmp, s.write.path);\n }\n s.swapped = true;\n }\n}\n\n/**\n * Put back every file that was already swapped, newest first, from the bytes read\n * before the swap. Returns the paths it could NOT restore — the caller must say so\n * out loud rather than report a clean failure over a half-merged repo.\n *\n * A rollback only ever un-does ITS OWN write: if the file no longer holds the\n * bytes this run put there, somebody else has written it since, and restoring\n * the pre-image would destroy their work while reporting a clean failure. That\n * file is left exactly as found and named in the failures, which is what turns\n * the caller's answer from `merge-failed` (\"trust the repo\") into\n * `rollback-incomplete` (\"look at it by hand\") — the honest answer, because the\n * repo now holds one run's writes half-reverted next to another's.\n */\nexport async function rollbackStaged(staged: StagedWrite[]): Promise<string[]> {\n const failures: string[] = [];\n for (const s of [...staged].reverse()) {\n if (!s.swapped) continue;\n try {\n const now = existsSync(s.write.path) ? await readFile(s.write.path) : null;\n if (!sameBytes(now, s.content)) {\n failures.push(`${s.write.path} (changed by another writer after this run wrote it — left as found)`);\n continue;\n }\n if (s.before === null) await rm(s.write.path, { force: true });\n else await atomicWrite(s.write.path, s.before);\n s.swapped = false;\n } catch (err) {\n failures.push(`${s.write.path} (${message(err)})`);\n }\n }\n await discardStaged(staged);\n return failures;\n}\n\n/** Write `content` to `path` through a temp file in the same directory. */\nexport async function atomicWrite(path: string, content: Buffer): Promise<void> {\n const tmp = tempPath(path, 0);\n await writeFile(tmp, content);\n await rename(tmp, path);\n}\n\n/** A hidden sibling of `path`: same directory, so the rename never crosses a filesystem. */\nexport function tempPath(path: string, n: number): string {\n return join(dirname(path), `.${basename(path)}.loam-${process.pid}-${n}-${Date.now()}.tmp`);\n}\n\n/** The shape `tempPath` writes — what a scan for leftovers of a killed run matches on. */\nexport const TEMP_FILE_RE = /^\\..*\\.loam-\\d+-\\d+-\\d+\\.tmp$/;\n\n/**\n * Everything staging left on disk: the temp files, and the directories it made\n * for them.\n *\n * Directories go by rmdir, never by recursive remove, and the walk stops the\n * moment one is not empty. The recursive form used to be reached for because\n * \"we created this directory, so it is ours\" — but a directory created at plan\n * time is shared by the time the rollback runs: a second archive can have put a\n * whole feature inside `features/archive/` in between, and `rm -r` on our own\n * created root took their work with it, silently, on OUR failure path.\n */\nasync function discardStaged(staged: StagedWrite[]): Promise<void> {\n for (const s of staged) if (s.tmp !== null) await quietRm(s.tmp);\n const roots = staged.filter((s) => s.createdDir !== null).map((s) => resolve(s.createdDir!));\n for (const s of [...staged].reverse()) {\n const root = roots.find((candidate) => contains(candidate, s.write.path));\n if (root !== undefined) await quietPruneEmptyParents(dirname(s.write.path), root);\n }\n}\n\nfunction contains(parent: string, candidate: string): boolean {\n const rel = relative(parent, resolve(candidate));\n return rel === \"\" || (!rel.startsWith(`..${sep}`) && rel !== \"..\" && !isAbsolute(rel));\n}\n\n/**\n * Undo the directories a write had to create, from `start` up to and including\n * `boundary`. Remove only directories that are still EMPTY: anything else in\n * there arrived after we made it, and belongs to whoever put it there.\n */\n/** Remove `dir` if it is empty. Best effort — a directory left standing is noise, not corruption. */\nexport async function quietRmdir(dir: string): Promise<void> {\n try {\n await rmdir(dir);\n } catch {\n // Not empty, or not ours.\n }\n}\n\nexport async function quietPruneEmptyParents(start: string, boundary: string): Promise<void> {\n let cursor = resolve(start);\n const root = resolve(boundary);\n while (contains(root, cursor)) {\n try {\n await rmdir(cursor);\n } catch {\n return;\n }\n if (cursor === root) return;\n const parent = dirname(cursor);\n if (parent === cursor) return;\n cursor = parent;\n }\n}\n\nexport async function quietRm(path: string): Promise<void> {\n try {\n await rm(path, { recursive: true, force: true });\n } catch {\n // Best effort — a leftover temp file is noise, not corruption.\n }\n}\n\n/**\n * The one spelling of the sentence a rollback failure prints, parameterised\n * only by the verb for what the files were mid-way through becoming. It\n * existed in three near-copies (archive's \"half-merged\", vouch's\n * \"half-stamped\", verify's \"half-written\") — the exact shape whose fix lands\n * in one copy while an operator greps their logs for another.\n */\nexport function rollbackMessage(err: unknown, failures: string[], what: string): string {\n return `${message(err)} — ROLLBACK INCOMPLETE, these files may be half-${what} and need checking by hand: ${failures.join(\", \")}`;\n}\n\n/** Say what the failure cost: nothing, or a repo that needs looking at by hand. */\nexport function rollbackError(err: unknown, failures: string[]): Error {\n if (failures.length === 0) {\n return new Error(`${message(err)} — the living docs were rolled back, nothing was merged`);\n }\n return new Error(\n rollbackMessage(err, failures, \"merged\"),\n );\n}\n\nexport function message(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n"]}
1
+ {"version":3,"file":"commit.js","sourceRoot":"","sources":["../../../src/core/staging/commit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The commit itself, in three steps: stage every new version beside its\n * target, swap them in, roll back what already went in when a later step fails.\n *\n * This is what `archive` and `unarchive` share, and it moved out of archive.ts\n * verbatim once unarchive became its second consumer. The MERGE computations\n * stayed with archive, which is still their only caller — the split was about a\n * second CONSUMER, never about archive being long.\n *\n * `StagingRaceError` belongs here rather than with the lock, though both are\n * about a second writer: the lock is taken before a plan is computed, while\n * this is the compare-and-set immediately before each rename — the second line\n * of defence for exactly the window two racers who both broke the same stale\n * lock would land in.\n */\nimport { existsSync } from \"node:fs\";\nimport { link, mkdir, readFile, rename, rm, rmdir, unlink, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { isPathInside, withoutNamespacePrefix } from \"../kernel/path-safety.js\";\nimport { sameBytes, toBytes, type PlannedWrite, type StagedWrite } from \"./writes.js\";\n\n/**\n * The target changed between the pre-image staging read and the swap.\n *\n * The pre-image is what a rollback restores and what `unarchive` is handed, so\n * swapping over somebody else's newer bytes would both lose their work and\n * arm a rollback that writes a third version nobody wrote. Commands map this to\n * `merge-failed`: nothing of theirs went in, the docs are as the other writer\n * left them, and re-running can work.\n */\nexport class StagingRaceError extends Error {\n override readonly name = \"StagingRaceError\";\n\n constructor(readonly path: string) {\n super(`${path} changed while this command was planning its merge — nothing was written; re-run`);\n }\n}\n\n/**\n * Write every new version to a temp file beside its target, remembering what the\n * target said first. Nothing observable changes here: on failure the temp files\n * are removed and the docs are exactly as they were.\n *\n * The pre-image is read eagerly and deliberately: it is what a rollback restores\n * and what `unarchive` is given, so a file we cannot read is a file we cannot\n * safely rewrite, and the merge must stop before it starts.\n */\nexport async function stageWrites(writes: PlannedWrite[]): Promise<StagedWrite[]> {\n const staged: StagedWrite[] = [];\n try {\n for (const [i, write] of writes.entries()) {\n const createdDir = await mkdirReportingCreated(dirname(write.path));\n const before = existsSync(write.path) ? await readFile(write.path) : null;\n const content = toBytes(write.content);\n let tmp: string | null = null;\n if (content !== null) {\n tmp = tempPath(write.path, i);\n await writeFile(tmp, content);\n }\n staged.push({ write, content, tmp, before, createdDir, swapped: false });\n }\n } catch (err) {\n await discardStaged(staged);\n throw err;\n }\n return staged;\n}\n\n/**\n * Swap the staged versions in, one file at a time. Each swap is a rename(2)\n * within a single directory, so a concurrent reader sees either the old bytes or\n * the new ones — never a half-written file. Across files it is not atomic: the\n * caller rolls back what has already gone in.\n *\n * Every swap re-reads its target first and compares it with the pre-image the\n * plan was computed from — a compare-and-set on the file's whole content. It is\n * half a CAS on purpose: read and rename cannot be one syscall, so a writer\n * landing inside that window still wins. What it does buy is the window that\n * actually matters — the seconds a plan spends parsing LikeC4 and merging YAML\n * between the pre-image read and this swap — and it costs one read per file.\n */\nexport async function swapStaged(staged: StagedWrite[]): Promise<void> {\n for (const s of staged) {\n const now = existsSync(s.write.path) ? await readFile(s.write.path) : null;\n if (!sameBytes(now, s.before)) throw new StagingRaceError(s.write.path);\n if (s.tmp === null) {\n if (s.before !== null) await unlink(s.write.path);\n } else if (s.write.exclusive === true) {\n // link(2) is the same-filesystem, atomic no-clobber counterpart to rename:\n // it fails with EEXIST instead of replacing a file created after planning.\n await link(s.tmp, s.write.path);\n s.swapped = true;\n await unlink(s.tmp);\n s.tmp = null;\n continue;\n } else {\n await rename(s.tmp, s.write.path);\n }\n s.swapped = true;\n }\n}\n\n/**\n * Put back every file that was already swapped, newest first, from the bytes read\n * before the swap. Returns the paths it could NOT restore — the caller must say so\n * out loud rather than report a clean failure over a half-merged repo.\n *\n * A rollback only ever un-does ITS OWN write: if the file no longer holds the\n * bytes this run put there, somebody else has written it since, and restoring\n * the pre-image would destroy their work while reporting a clean failure. That\n * file is left exactly as found and named in the failures, which is what turns\n * the caller's answer from `merge-failed` (\"trust the repo\") into\n * `rollback-incomplete` (\"look at it by hand\") — the honest answer, because the\n * repo now holds one run's writes half-reverted next to another's.\n */\nexport async function rollbackStaged(staged: StagedWrite[]): Promise<string[]> {\n const failures: string[] = [];\n for (const s of [...staged].reverse()) {\n if (!s.swapped) continue;\n try {\n const now = existsSync(s.write.path) ? await readFile(s.write.path) : null;\n if (!sameBytes(now, s.content)) {\n failures.push(`${s.write.path} (changed by another writer after this run wrote it — left as found)`);\n continue;\n }\n if (s.before === null) await rm(s.write.path, { force: true });\n else await atomicWrite(s.write.path, s.before);\n s.swapped = false;\n } catch (err) {\n failures.push(`${s.write.path} (${message(err)})`);\n }\n }\n await discardStaged(staged);\n return failures;\n}\n\n/**\n * `mkdir -p`, reporting the topmost directory it had to create — the handle a\n * rollback needs to put the tree back the way it found it — or null when every\n * one already existed.\n *\n * The report is never handed on raw. On Windows, Node spells it with a `\\\\?\\`\n * prefix that no other path loam holds carries, and a rollback comparing the\n * two spellings found nothing to prune (`withoutNamespacePrefix` has the\n * measurement). Every caller that keeps a created directory for its rollback\n * takes it from here, so the plain spelling is decided once.\n */\nexport async function mkdirReportingCreated(dir: string): Promise<string | null> {\n const created = await mkdir(dir, { recursive: true });\n return created === undefined ? null : withoutNamespacePrefix(created);\n}\n\n/** Write `content` to `path` through a temp file in the same directory. */\nexport async function atomicWrite(path: string, content: Buffer): Promise<void> {\n const tmp = tempPath(path, 0);\n await writeFile(tmp, content);\n await rename(tmp, path);\n}\n\n/** A hidden sibling of `path`: same directory, so the rename never crosses a filesystem. */\nexport function tempPath(path: string, n: number): string {\n return join(dirname(path), `.${basename(path)}.loam-${process.pid}-${n}-${Date.now()}.tmp`);\n}\n\n/** The shape `tempPath` writes — what a scan for leftovers of a killed run matches on. */\nexport const TEMP_FILE_RE = /^\\..*\\.loam-\\d+-\\d+-\\d+\\.tmp$/;\n\n/**\n * Everything staging left on disk: the temp files, and the directories it made\n * for them.\n *\n * Directories go by rmdir, never by recursive remove, and the walk stops the\n * moment one is not empty. The recursive form used to be reached for because\n * \"we created this directory, so it is ours\" — but a directory created at plan\n * time is shared by the time the rollback runs: a second archive can have put a\n * whole feature inside `features/archive/` in between, and `rm -r` on our own\n * created root took their work with it, silently, on OUR failure path.\n */\nasync function discardStaged(staged: StagedWrite[]): Promise<void> {\n for (const s of staged) if (s.tmp !== null) await quietRm(s.tmp);\n const roots = staged.filter((s) => s.createdDir !== null).map((s) => resolve(s.createdDir!));\n for (const s of [...staged].reverse()) {\n const root = roots.find((candidate) => isPathInside(candidate, s.write.path));\n if (root !== undefined) await quietPruneEmptyParents(dirname(s.write.path), root);\n }\n}\n\n/** Remove `dir` if it is empty. Best effort — a directory left standing is noise, not corruption. */\nexport async function quietRmdir(dir: string): Promise<void> {\n try {\n await rmdir(dir);\n } catch {\n // Not empty, or not ours.\n }\n}\n\n/**\n * Undo the directories a write had to create, from `start` up to and including\n * `boundary` — or, for unarchive, the ones a restore's deletes left empty.\n * Remove only directories that are still EMPTY: anything else in there arrived\n * after we made it, and belongs to whoever put it there.\n */\nexport async function quietPruneEmptyParents(start: string, boundary: string): Promise<void> {\n let cursor = resolve(start);\n const root = resolve(boundary);\n while (isPathInside(root, cursor)) {\n try {\n await rmdir(cursor);\n } catch {\n return;\n }\n if (cursor === root) return;\n const parent = dirname(cursor);\n if (parent === cursor) return;\n cursor = parent;\n }\n}\n\nexport async function quietRm(path: string): Promise<void> {\n try {\n await rm(path, { recursive: true, force: true });\n } catch {\n // Best effort — a leftover temp file is noise, not corruption.\n }\n}\n\n/**\n * The one spelling of the sentence a rollback failure prints, parameterised\n * only by the verb for what the files were mid-way through becoming. It\n * existed in three near-copies (archive's \"half-merged\", vouch's\n * \"half-stamped\", verify's \"half-written\") — the exact shape whose fix lands\n * in one copy while an operator greps their logs for another.\n */\nexport function rollbackMessage(err: unknown, failures: string[], what: string): string {\n return `${message(err)} — ROLLBACK INCOMPLETE, these files may be half-${what} and need checking by hand: ${failures.join(\", \")}`;\n}\n\n/** Say what the failure cost: nothing, or a repo that needs looking at by hand. */\nexport function rollbackError(err: unknown, failures: string[]): Error {\n if (failures.length === 0) {\n return new Error(`${message(err)} — the living docs were rolled back, nothing was merged`);\n }\n return new Error(\n rollbackMessage(err, failures, \"merged\"),\n );\n}\n\nexport function message(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n"]}
@@ -8,7 +8,7 @@
8
8
  * `lockIsStale` only reports it to `doctor`. They were written as twins and
9
9
  * stay together so they cannot drift into two definitions of a dead holder.
10
10
  */
11
- import { link, readFile, unlink, writeFile } from "node:fs/promises";
11
+ import { link, readFile, rename, writeFile } from "node:fs/promises";
12
12
  import { hostname } from "node:os";
13
13
  import { join } from "node:path";
14
14
  // Every `isRecord` below guards a record loam itself wrote and is reading back
@@ -61,18 +61,24 @@ export class DocsBusyError extends Error {
61
61
  * microsecond. A create that dies before its link leaves only a staging temp
62
62
  * `doctor` already names as litter. A holder that died without releasing would
63
63
  * otherwise wedge the repo forever, so a lock naming a pid on THIS host that
64
- * no longer exists is broken once and retried — two racers could both break
65
- * the same stale lock, which is why the pre-image check in `swapStaged`
66
- * remains the second line of defence rather than an optimisation.
64
+ * no longer exists is broken once and retried. Breaking removes only the
65
+ * instance that was judged dead (see `breakStaleLock`), never a successor's
66
+ * live lock. This comment used to accept that two racers could both break the
67
+ * same stale lock, trusting the pre-image check in `swapStaged` as the second
68
+ * line of defence. That check is still kept, and is still not an
69
+ * optimisation. But it guards only the swap. A second holder's RECOVERY reads
70
+ * the first holder's in-flight journal as an interrupted commit and sweeps
71
+ * its staged temps, and no compare-and-set sits in front of that.
67
72
  */
68
73
  /**
69
74
  * Distinguishes concurrent acquires INSIDE one process. `tempPath` salts with
70
75
  * pid and a millisecond timestamp, which is unique across processes and not
71
76
  * within one: two in-process acquirers in the same millisecond shared a temp
72
77
  * name, the first's cleanup deleted it under the second, and the second's
73
- * link(2) surfaced ENOENT as `internal`. A bare counter is module-level
74
- * mutable state, tolerated here because it feeds nothing but name uniqueness —
75
- * no behaviour ever branches on its value.
78
+ * link(2) surfaced ENOENT as `internal`. The private name a stale-lock break
79
+ * takes the lock to is salted from the same counter, for the same reason. A
80
+ * bare counter is module-level mutable state, tolerated here because it feeds
81
+ * nothing but name uniqueness — no behaviour ever branches on its value.
76
82
  */
77
83
  let lockAcquireSeq = 0;
78
84
  export async function acquireDocsLock(docsDir) {
@@ -166,20 +172,76 @@ function pidGone(pid) {
166
172
  return err.code === "ESRCH";
167
173
  }
168
174
  }
169
- /** Remove a lock whose holder is a dead process on this same host. Returns whether it did. */
175
+ /**
176
+ * Remove a lock whose holder is a dead process on this same host, and only the
177
+ * lock INSTANCE that verdict was about. Returns whether it did.
178
+ *
179
+ * The verdict comes from a read, the removal acts on a path, and other writers
180
+ * move in between. The removal used to be a bare unlink(2) of the path. Under
181
+ * CPU load a waiter read the holder's payload, then the holder released
182
+ * NORMALLY and exited, then a third writer linked a fresh lock, and only then
183
+ * did kill(pid, 0) answer ESRCH. The unlink deleted that third writer's LIVE
184
+ * lock. Two `loam gherkin` runs then shared one commit window, and the
185
+ * second's recovery read the first's in-flight journal as "interrupted" and
186
+ * swept its staged temps. The first's rename failed ENOENT
187
+ * (test/stress-gherkin.test.ts found it; test/lock-break.test.ts opens each
188
+ * window on purpose).
189
+ *
190
+ * So the removal is bound to the judged instance, twice:
191
+ * - the lock is re-read AFTER the verdict and must still name the judged
192
+ * holder. A holder that released normally removed its file before it
193
+ * exited, so a dead holder's lock still in place can only be a crashed
194
+ * one's, which is the one lock a break is for;
195
+ * - it is then TAKEN by rename(2) to a private name, which exactly one of
196
+ * several concurrent breakers can win, and judged in hand. If what moved is
197
+ * not the judged instance (another waiter broke the same crashed lock first
198
+ * and linked its own in between), it goes back by link(2), which never
199
+ * overwrites.
200
+ * One gap stays open, between that take and the put-back. A third writer can
201
+ * link in it, and then the displaced lock stays down. Or the displaced holder
202
+ * can release in it, and then the put-back restores a lock nobody holds, which
203
+ * stands until that process exits. Either needs a crashed holder AND two
204
+ * breakers racing for its lock. The gap is named here rather than closed,
205
+ * because closing it needs a kernel lock (flock) that Node does not expose and
206
+ * loam will not add a dependency for.
207
+ */
170
208
  async function breakStaleLock(path) {
171
- const holder = await readLockHolder(path);
209
+ const judged = await readLockHolder(path);
172
210
  // Unreadable is deliberately NOT breakable: a lock we cannot understand is
173
211
  // the one case where guessing is worst. `doctor` reports it as damage.
174
- if (holder.kind !== "held" || holder.host !== hostname() || !pidGone(holder.pid))
212
+ if (judged.kind !== "held" || judged.host !== hostname() || !pidGone(judged.pid))
213
+ return false;
214
+ if (!sameHolder(await readLockHolder(path), judged))
175
215
  return false;
216
+ lockAcquireSeq += 1;
217
+ const taken = tempPath(path, lockAcquireSeq);
176
218
  try {
177
- await unlink(path);
178
- return true;
219
+ await rename(path, taken);
179
220
  }
180
221
  catch {
222
+ // Another breaker took it first, or the platform refused the rename (a
223
+ // Windows handle still open on it). Either way the next link(2) attempt
224
+ // decides, not this one.
181
225
  return false;
182
226
  }
227
+ if (sameHolder(await readLockHolder(taken), judged)) {
228
+ await quietRm(taken);
229
+ return true;
230
+ }
231
+ try {
232
+ await link(taken, path);
233
+ }
234
+ catch {
235
+ // The gap named above: a third writer linked in between, and putting the
236
+ // displaced lock back would overwrite a live one. It stays down. Its holder
237
+ // is unprotected from here, and nothing this process can do fixes that.
238
+ }
239
+ await quietRm(taken);
240
+ return false;
241
+ }
242
+ /** Is `now` the very lock instance `judged` was read from? Pid, host and timestamp are its identity. */
243
+ function sameHolder(now, judged) {
244
+ return now.kind === "held" && now.pid === judged.pid && now.host === judged.host && now.at === judged.at;
183
245
  }
184
246
  export async function describeLock(path) {
185
247
  const holder = await readLockHolder(path);
@@ -1 +1 @@
1
- {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/core/staging/lock.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The docs repo's advisory lock: take it, describe its holder, and break it\n * when that holder is a pid on this host that no longer exists.\n *\n * A module of its own because it is the only rule here that spans the whole\n * fleet rather than one file, and because the two readers of a held lock have\n * to agree on what \"stale\" means — `breakStaleLock` acts on the answer,\n * `lockIsStale` only reports it to `doctor`. They were written as twins and\n * stay together so they cannot drift into two definitions of a dead holder.\n */\nimport { link, readFile, unlink, writeFile } from \"node:fs/promises\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\n// Every `isRecord` below guards a record loam itself wrote and is reading back\n// — the lock, the snapshot manifest — whose fields are asked for on the very\n// next line, inside a REFUSAL path. `JSON.parse` answers with any JSON value,\n// not with an object, and that is where a thrown `TypeError` costs the most: it\n// does not make the command fail safely, it replaces a designed refusal with\n// `internal`.\nimport { isRecord } from \"../kernel/records.js\";\nimport { quietRm, tempPath } from \"./commit.js\";\n\n/** The lock file, in the docs repo root — one writer at a time across the whole fleet. */\nexport const DOCS_LOCK = \".loam-lock\";\n\n/**\n * How long a WAITING writer sits out a live holder before answering\n * `docs-busy`. One spelling for every writer that waits — including gherkin,\n * whose lock guards the SERVICE repo's emission root rather than the docs\n * repo: long enough to cover another writer's sub-second commit window with a\n * margin for a loaded host, bounded so a wedged lock cannot hang anyone.\n */\nexport const LOCK_WAIT_MS = 5_000;\n\n/** Another writer holds the docs repo. Commands map this to the `docs-busy` envelope code. */\nexport class DocsBusyError extends Error {\n override readonly name = \"DocsBusyError\";\n\n constructor(\n readonly lockPath: string,\n holder: string,\n ) {\n super(\n `another loam command is writing this docs repo (${holder}) — nothing was read or written. ` +\n `Wait for it to finish, or remove ${lockPath} if no such command is running.`,\n );\n }\n}\n\n/**\n * Take the docs repo's advisory lock for the whole plan+commit window, and\n * return the release.\n *\n * Advisory, and deliberately coarse: the destructive commands write the SAME\n * three shared files (a service spec, a service contract, the one landscape),\n * so two of them overlapping is not a merge to be resolved but a merge one of\n * them computed against a snapshot that no longer exists. Two archives used to\n * plan against the same landscape bytes and the second's splice silently\n * dropped the first's additions — exit 0 on both, and a `validate` that stayed\n * green because the surviving document was perfectly coherent.\n *\n * link(2) over a fully-written sibling is the primitive: as atomic as the\n * O_EXCL create it replaced, needs no daemon, and leaves a file a human can\n * read and delete — but unlike open-then-write, the lock file can never be\n * OBSERVED empty. `writeFile(path, …, \"wx\")` published existence first and\n * payload a syscall later, and in that window (or after a crash inside it) the\n * lock named no holder: `doctor` now grades exactly that shape as damage whose\n * fix is deletion, so the healthy path must never wear it, even for a\n * microsecond. A create that dies before its link leaves only a staging temp\n * `doctor` already names as litter. A holder that died without releasing would\n * otherwise wedge the repo forever, so a lock naming a pid on THIS host that\n * no longer exists is broken once and retried — two racers could both break\n * the same stale lock, which is why the pre-image check in `swapStaged`\n * remains the second line of defence rather than an optimisation.\n */\n/**\n * Distinguishes concurrent acquires INSIDE one process. `tempPath` salts with\n * pid and a millisecond timestamp, which is unique across processes and not\n * within one: two in-process acquirers in the same millisecond shared a temp\n * name, the first's cleanup deleted it under the second, and the second's\n * link(2) surfaced ENOENT as `internal`. A bare counter is module-level\n * mutable state, tolerated here because it feeds nothing but name uniqueness —\n * no behaviour ever branches on its value.\n */\nlet lockAcquireSeq = 0;\n\nexport async function acquireDocsLock(docsDir: string): Promise<() => Promise<void>> {\n const path = join(docsDir, DOCS_LOCK);\n const payload = `${JSON.stringify({ pid: process.pid, host: hostname(), at: new Date().toISOString() })}\\n`;\n lockAcquireSeq += 1;\n const tmp = tempPath(path, lockAcquireSeq);\n await writeFile(tmp, payload);\n try {\n for (let attempt = 0; ; attempt += 1) {\n try {\n await link(tmp, path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n if (attempt === 0 && (await breakStaleLock(path))) continue;\n throw new DocsBusyError(path, await describeLock(path));\n }\n let released = false;\n return async () => {\n if (released) return;\n released = true;\n await quietRm(path);\n };\n }\n } finally {\n await quietRm(tmp);\n }\n}\n\n/**\n * Take the docs lock, waiting out a live holder instead of refusing on first\n * contact. Same lock, different caller contract: `archive` refuses fast because\n * its holder may be mid-merge for seconds and its caller can re-run, but two\n * `verify --record` runs for different services of one feature are BOTH\n * supposed to land — the second must wait for the first's sub-second window and\n * then merge over the record it left, or one service's attestation is refused\n * for no reason a user can see. The wait is bounded: a holder that outlives\n * `waitMs` yields the same `DocsBusyError` the fast form throws, so the caller\n * maps it to `docs-busy` either way and nothing hangs on a wedged lock.\n */\nexport async function acquireDocsLockWaiting(docsDir: string, waitMs: number): Promise<() => Promise<void>> {\n const deadline = Date.now() + waitMs;\n for (;;) {\n try {\n return await acquireDocsLock(docsDir);\n } catch (err) {\n if (!(err instanceof DocsBusyError) || Date.now() >= deadline) throw err;\n }\n // Short enough that a released lock is picked up before a human notices;\n // long enough that the poll does not busy-spin the directory.\n await new Promise((resolve) => setTimeout(resolve, 25));\n }\n}\n\n/**\n * One read of the lock file, one interpretation, shared by every question\n * asked about it. These used to be four separate parse-and-guard copies\n * (`breakStaleLock`, `describeLock`, `lockIsStale`, `lockUnreadable`), each\n * catching to a different answer — which is how `doctor` came to read the file\n * three times in a row and report three different states of a lock that was\n * released between its reads.\n *\n * `absent` and `unreadable` are kept apart on purpose: a lock that is GONE is\n * the ordinary end of every command, while a lock that exists and cannot name\n * a holder — empty bytes from a crash between create and flush, JSON that is\n * not a record, a record with no string `host` or numeric `pid` — is damage:\n * `breakStaleLock` rightly refuses to guess about it, so nothing will ever\n * release it. A different host's lock is legible and simply not ours to judge.\n */\ntype LockHolder =\n | { kind: \"absent\" }\n | { kind: \"unreadable\" }\n | { kind: \"held\"; host: string; pid: number; at: string | undefined };\n\nasync function readLockHolder(path: string): Promise<LockHolder> {\n let text: string;\n try {\n text = await readFile(path, \"utf8\");\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"ENOENT\" ? { kind: \"absent\" } : { kind: \"unreadable\" };\n }\n let holder: unknown;\n try {\n holder = JSON.parse(text);\n } catch {\n return { kind: \"unreadable\" };\n }\n // A lock file holding `null` (or a bare string, or an array) is JSON this\n // parses and nothing else understands. The dereference below used to run\n // outside the guard in `acquireDocsLock`'s breaker and threw, replacing the\n // `docs-busy` refusal — the designed answer — with an `internal` envelope.\n if (!isRecord(holder) || typeof holder.host !== \"string\" || typeof holder.pid !== \"number\") {\n return { kind: \"unreadable\" };\n }\n return { kind: \"held\", host: holder.host, pid: holder.pid, at: typeof holder.at === \"string\" ? holder.at : undefined };\n}\n\n/** Does this pid no longer exist on this host? Signal 0 asks without delivering; EPERM is alive-but-somebody-else's. */\nfunction pidGone(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return false;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"ESRCH\";\n }\n}\n\n/** Remove a lock whose holder is a dead process on this same host. Returns whether it did. */\nasync function breakStaleLock(path: string): Promise<boolean> {\n const holder = await readLockHolder(path);\n // Unreadable is deliberately NOT breakable: a lock we cannot understand is\n // the one case where guessing is worst. `doctor` reports it as damage.\n if (holder.kind !== \"held\" || holder.host !== hostname() || !pidGone(holder.pid)) return false;\n try {\n await unlink(path);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function describeLock(path: string): Promise<string> {\n const holder = await readLockHolder(path);\n return holder.kind === \"held\" ? `pid ${holder.pid} on ${holder.host} since ${String(holder.at)}` : `see ${path}`;\n}\n\n/**\n * The lock's whole state for `doctor`, from ONE read. Null means no lock.\n * Three sequential reads used to answer this — exists, describe, stale — and a\n * lock released between them made `doctor` describe a file that was gone,\n * escalating an ordinary sub-second commit window into a blocker.\n */\nexport async function lockResidue(path: string): Promise<{ holder: string; stale: boolean; unreadable: boolean } | null> {\n const holder = await readLockHolder(path);\n if (holder.kind === \"absent\") return null;\n if (holder.kind === \"unreadable\") return { holder: `see ${path}`, stale: false, unreadable: true };\n return {\n holder: `pid ${holder.pid} on ${holder.host} since ${String(holder.at)}`,\n stale: holder.host === hostname() && pidGone(holder.pid),\n unreadable: false,\n };\n}\n\n"]}
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/core/staging/lock.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The docs repo's advisory lock: take it, describe its holder, and break it\n * when that holder is a pid on this host that no longer exists.\n *\n * A module of its own because it is the only rule here that spans the whole\n * fleet rather than one file, and because the two readers of a held lock have\n * to agree on what \"stale\" means — `breakStaleLock` acts on the answer,\n * `lockIsStale` only reports it to `doctor`. They were written as twins and\n * stay together so they cannot drift into two definitions of a dead holder.\n */\nimport { link, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\n// Every `isRecord` below guards a record loam itself wrote and is reading back\n// — the lock, the snapshot manifest — whose fields are asked for on the very\n// next line, inside a REFUSAL path. `JSON.parse` answers with any JSON value,\n// not with an object, and that is where a thrown `TypeError` costs the most: it\n// does not make the command fail safely, it replaces a designed refusal with\n// `internal`.\nimport { isRecord } from \"../kernel/records.js\";\nimport { quietRm, tempPath } from \"./commit.js\";\n\n/** The lock file, in the docs repo root — one writer at a time across the whole fleet. */\nexport const DOCS_LOCK = \".loam-lock\";\n\n/**\n * How long a WAITING writer sits out a live holder before answering\n * `docs-busy`. One spelling for every writer that waits — including gherkin,\n * whose lock guards the SERVICE repo's emission root rather than the docs\n * repo: long enough to cover another writer's sub-second commit window with a\n * margin for a loaded host, bounded so a wedged lock cannot hang anyone.\n */\nexport const LOCK_WAIT_MS = 5_000;\n\n/** Another writer holds the docs repo. Commands map this to the `docs-busy` envelope code. */\nexport class DocsBusyError extends Error {\n override readonly name = \"DocsBusyError\";\n\n constructor(\n readonly lockPath: string,\n holder: string,\n ) {\n super(\n `another loam command is writing this docs repo (${holder}) — nothing was read or written. ` +\n `Wait for it to finish, or remove ${lockPath} if no such command is running.`,\n );\n }\n}\n\n/**\n * Take the docs repo's advisory lock for the whole plan+commit window, and\n * return the release.\n *\n * Advisory, and deliberately coarse: the destructive commands write the SAME\n * three shared files (a service spec, a service contract, the one landscape),\n * so two of them overlapping is not a merge to be resolved but a merge one of\n * them computed against a snapshot that no longer exists. Two archives used to\n * plan against the same landscape bytes and the second's splice silently\n * dropped the first's additions — exit 0 on both, and a `validate` that stayed\n * green because the surviving document was perfectly coherent.\n *\n * link(2) over a fully-written sibling is the primitive: as atomic as the\n * O_EXCL create it replaced, needs no daemon, and leaves a file a human can\n * read and delete — but unlike open-then-write, the lock file can never be\n * OBSERVED empty. `writeFile(path, …, \"wx\")` published existence first and\n * payload a syscall later, and in that window (or after a crash inside it) the\n * lock named no holder: `doctor` now grades exactly that shape as damage whose\n * fix is deletion, so the healthy path must never wear it, even for a\n * microsecond. A create that dies before its link leaves only a staging temp\n * `doctor` already names as litter. A holder that died without releasing would\n * otherwise wedge the repo forever, so a lock naming a pid on THIS host that\n * no longer exists is broken once and retried. Breaking removes only the\n * instance that was judged dead (see `breakStaleLock`), never a successor's\n * live lock. This comment used to accept that two racers could both break the\n * same stale lock, trusting the pre-image check in `swapStaged` as the second\n * line of defence. That check is still kept, and is still not an\n * optimisation. But it guards only the swap. A second holder's RECOVERY reads\n * the first holder's in-flight journal as an interrupted commit and sweeps\n * its staged temps, and no compare-and-set sits in front of that.\n */\n/**\n * Distinguishes concurrent acquires INSIDE one process. `tempPath` salts with\n * pid and a millisecond timestamp, which is unique across processes and not\n * within one: two in-process acquirers in the same millisecond shared a temp\n * name, the first's cleanup deleted it under the second, and the second's\n * link(2) surfaced ENOENT as `internal`. The private name a stale-lock break\n * takes the lock to is salted from the same counter, for the same reason. A\n * bare counter is module-level mutable state, tolerated here because it feeds\n * nothing but name uniqueness — no behaviour ever branches on its value.\n */\nlet lockAcquireSeq = 0;\n\nexport async function acquireDocsLock(docsDir: string): Promise<() => Promise<void>> {\n const path = join(docsDir, DOCS_LOCK);\n const payload = `${JSON.stringify({ pid: process.pid, host: hostname(), at: new Date().toISOString() })}\\n`;\n lockAcquireSeq += 1;\n const tmp = tempPath(path, lockAcquireSeq);\n await writeFile(tmp, payload);\n try {\n for (let attempt = 0; ; attempt += 1) {\n try {\n await link(tmp, path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n if (attempt === 0 && (await breakStaleLock(path))) continue;\n throw new DocsBusyError(path, await describeLock(path));\n }\n let released = false;\n return async () => {\n if (released) return;\n released = true;\n await quietRm(path);\n };\n }\n } finally {\n await quietRm(tmp);\n }\n}\n\n/**\n * Take the docs lock, waiting out a live holder instead of refusing on first\n * contact. Same lock, different caller contract: `archive` refuses fast because\n * its holder may be mid-merge for seconds and its caller can re-run, but two\n * `verify --record` runs for different services of one feature are BOTH\n * supposed to land — the second must wait for the first's sub-second window and\n * then merge over the record it left, or one service's attestation is refused\n * for no reason a user can see. The wait is bounded: a holder that outlives\n * `waitMs` yields the same `DocsBusyError` the fast form throws, so the caller\n * maps it to `docs-busy` either way and nothing hangs on a wedged lock.\n */\nexport async function acquireDocsLockWaiting(docsDir: string, waitMs: number): Promise<() => Promise<void>> {\n const deadline = Date.now() + waitMs;\n for (;;) {\n try {\n return await acquireDocsLock(docsDir);\n } catch (err) {\n if (!(err instanceof DocsBusyError) || Date.now() >= deadline) throw err;\n }\n // Short enough that a released lock is picked up before a human notices;\n // long enough that the poll does not busy-spin the directory.\n await new Promise((resolve) => setTimeout(resolve, 25));\n }\n}\n\n/**\n * One read of the lock file, one interpretation, shared by every question\n * asked about it. These used to be four separate parse-and-guard copies\n * (`breakStaleLock`, `describeLock`, `lockIsStale`, `lockUnreadable`), each\n * catching to a different answer — which is how `doctor` came to read the file\n * three times in a row and report three different states of a lock that was\n * released between its reads.\n *\n * `absent` and `unreadable` are kept apart on purpose: a lock that is GONE is\n * the ordinary end of every command, while a lock that exists and cannot name\n * a holder — empty bytes from a crash between create and flush, JSON that is\n * not a record, a record with no string `host` or numeric `pid` — is damage:\n * `breakStaleLock` rightly refuses to guess about it, so nothing will ever\n * release it. A different host's lock is legible and simply not ours to judge.\n */\ntype LockHolder =\n | { kind: \"absent\" }\n | { kind: \"unreadable\" }\n | { kind: \"held\"; host: string; pid: number; at: string | undefined };\n\nasync function readLockHolder(path: string): Promise<LockHolder> {\n let text: string;\n try {\n text = await readFile(path, \"utf8\");\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"ENOENT\" ? { kind: \"absent\" } : { kind: \"unreadable\" };\n }\n let holder: unknown;\n try {\n holder = JSON.parse(text);\n } catch {\n return { kind: \"unreadable\" };\n }\n // A lock file holding `null` (or a bare string, or an array) is JSON this\n // parses and nothing else understands. The dereference below used to run\n // outside the guard in `acquireDocsLock`'s breaker and threw, replacing the\n // `docs-busy` refusal — the designed answer — with an `internal` envelope.\n if (!isRecord(holder) || typeof holder.host !== \"string\" || typeof holder.pid !== \"number\") {\n return { kind: \"unreadable\" };\n }\n return { kind: \"held\", host: holder.host, pid: holder.pid, at: typeof holder.at === \"string\" ? holder.at : undefined };\n}\n\n/** Does this pid no longer exist on this host? Signal 0 asks without delivering; EPERM is alive-but-somebody-else's. */\nfunction pidGone(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return false;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"ESRCH\";\n }\n}\n\n/**\n * Remove a lock whose holder is a dead process on this same host, and only the\n * lock INSTANCE that verdict was about. Returns whether it did.\n *\n * The verdict comes from a read, the removal acts on a path, and other writers\n * move in between. The removal used to be a bare unlink(2) of the path. Under\n * CPU load a waiter read the holder's payload, then the holder released\n * NORMALLY and exited, then a third writer linked a fresh lock, and only then\n * did kill(pid, 0) answer ESRCH. The unlink deleted that third writer's LIVE\n * lock. Two `loam gherkin` runs then shared one commit window, and the\n * second's recovery read the first's in-flight journal as \"interrupted\" and\n * swept its staged temps. The first's rename failed ENOENT\n * (test/stress-gherkin.test.ts found it; test/lock-break.test.ts opens each\n * window on purpose).\n *\n * So the removal is bound to the judged instance, twice:\n * - the lock is re-read AFTER the verdict and must still name the judged\n * holder. A holder that released normally removed its file before it\n * exited, so a dead holder's lock still in place can only be a crashed\n * one's, which is the one lock a break is for;\n * - it is then TAKEN by rename(2) to a private name, which exactly one of\n * several concurrent breakers can win, and judged in hand. If what moved is\n * not the judged instance (another waiter broke the same crashed lock first\n * and linked its own in between), it goes back by link(2), which never\n * overwrites.\n * One gap stays open, between that take and the put-back. A third writer can\n * link in it, and then the displaced lock stays down. Or the displaced holder\n * can release in it, and then the put-back restores a lock nobody holds, which\n * stands until that process exits. Either needs a crashed holder AND two\n * breakers racing for its lock. The gap is named here rather than closed,\n * because closing it needs a kernel lock (flock) that Node does not expose and\n * loam will not add a dependency for.\n */\nasync function breakStaleLock(path: string): Promise<boolean> {\n const judged = await readLockHolder(path);\n // Unreadable is deliberately NOT breakable: a lock we cannot understand is\n // the one case where guessing is worst. `doctor` reports it as damage.\n if (judged.kind !== \"held\" || judged.host !== hostname() || !pidGone(judged.pid)) return false;\n if (!sameHolder(await readLockHolder(path), judged)) return false;\n lockAcquireSeq += 1;\n const taken = tempPath(path, lockAcquireSeq);\n try {\n await rename(path, taken);\n } catch {\n // Another breaker took it first, or the platform refused the rename (a\n // Windows handle still open on it). Either way the next link(2) attempt\n // decides, not this one.\n return false;\n }\n if (sameHolder(await readLockHolder(taken), judged)) {\n await quietRm(taken);\n return true;\n }\n try {\n await link(taken, path);\n } catch {\n // The gap named above: a third writer linked in between, and putting the\n // displaced lock back would overwrite a live one. It stays down. Its holder\n // is unprotected from here, and nothing this process can do fixes that.\n }\n await quietRm(taken);\n return false;\n}\n\n/** Is `now` the very lock instance `judged` was read from? Pid, host and timestamp are its identity. */\nfunction sameHolder(now: LockHolder, judged: Extract<LockHolder, { kind: \"held\" }>): boolean {\n return now.kind === \"held\" && now.pid === judged.pid && now.host === judged.host && now.at === judged.at;\n}\n\nexport async function describeLock(path: string): Promise<string> {\n const holder = await readLockHolder(path);\n return holder.kind === \"held\" ? `pid ${holder.pid} on ${holder.host} since ${String(holder.at)}` : `see ${path}`;\n}\n\n/**\n * The lock's whole state for `doctor`, from ONE read. Null means no lock.\n * Three sequential reads used to answer this — exists, describe, stale — and a\n * lock released between them made `doctor` describe a file that was gone,\n * escalating an ordinary sub-second commit window into a blocker.\n */\nexport async function lockResidue(path: string): Promise<{ holder: string; stale: boolean; unreadable: boolean } | null> {\n const holder = await readLockHolder(path);\n if (holder.kind === \"absent\") return null;\n if (holder.kind === \"unreadable\") return { holder: `see ${path}`, stale: false, unreadable: true };\n return {\n holder: `pid ${holder.pid} on ${holder.host} since ${String(holder.at)}`,\n stale: holder.host === hostname() && pidGone(holder.pid),\n unreadable: false,\n };\n}\n\n"]}
@@ -22,11 +22,8 @@ const COMMIT_INTENT_VERSION = 1;
22
22
  /**
23
23
  * Write the intent record and get it onto the disk before the first swap.
24
24
  *
25
- * fsync, not just write: the point of the record is to survive the crash that
26
- * interrupts the commit, and a record still sitting in the page cache when the
27
- * machine dies describes nothing. The directory entry is fsynced too where the
28
- * platform allows opening a directory — without that the file's bytes can be
29
- * durable while its name is not.
25
+ * fsync, not just write — through `writeDurable`, whose doc says why: the
26
+ * point of the record is to survive the crash that interrupts the commit.
30
27
  */
31
28
  export async function writeCommitIntent(docsDir, spec, staged) {
32
29
  const intent = {
@@ -1 +1 @@
1
- {"version":3,"file":"intent.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/intent.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `.loam-commit` journal — written before the first swap, cleared after the\n * last — and the record that makes an interrupted commit detectable at all.\n *\n * Reading and writing the record is kept apart from acting on it\n * (`./recover.ts`) because a corrupt record has to stay describable: `doctor`\n * reports a `.loam-commit` it cannot parse, and a module that parsed and\n * repaired in one step would have no way to say that without also attempting\n * the repair.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { isPathInside } from \"../../kernel/path-safety.js\";\nimport { COMMIT_INTENT, writeDurable } from \"../interrupted.js\";\nimport { isDigest, sha256, type StagedWrite } from \"../writes.js\";\nimport { quietRm } from \"../commit.js\";\n\n/** The version-1 record: archive's and unarchive's snapshot-backed journal. */\nconst COMMIT_INTENT_VERSION = 1;\n\nexport interface CommitIntentFile {\n /** Docs-repo-relative path, forward slashes. */\n path: string;\n /** sha256 of the bytes before the swap; null when the swap creates the file. */\n before: string | null;\n /** sha256 of the bytes the swap writes; null when the swap deletes the file. */\n after: string | null;\n}\n\nexport interface CommitIntent {\n version: number;\n /** Which command was committing — the sentence a human reads, not a branch. */\n command: \"archive\" | \"unarchive\";\n /**\n * The state a repair returns the docs to. `archive` undoes (its rollback path\n * would have restored the pre-images anyway); `unarchive` finishes (its target\n * bytes ARE the pre-images, and the merged text it was replacing is recorded\n * nowhere else).\n */\n restore: \"before\" | \"after\";\n pid: number;\n host: string;\n at: string;\n feature: string;\n /** Docs-repo-relative feature directory the commit's final move takes FROM. */\n moveFrom: string;\n /** …and TO. Both landed ⇒ every earlier step landed too. */\n moveTo: string;\n files: CommitIntentFile[];\n}\n\n/**\n * Write the intent record and get it onto the disk before the first swap.\n *\n * fsync, not just write: the point of the record is to survive the crash that\n * interrupts the commit, and a record still sitting in the page cache when the\n * machine dies describes nothing. The directory entry is fsynced too where the\n * platform allows opening a directory — without that the file's bytes can be\n * durable while its name is not.\n */\nexport async function writeCommitIntent(\n docsDir: string,\n spec: {\n command: CommitIntent[\"command\"];\n restore: CommitIntent[\"restore\"];\n feature: string;\n moveFrom: string;\n moveTo: string;\n },\n staged: StagedWrite[],\n): Promise<void> {\n const intent: CommitIntent = {\n version: COMMIT_INTENT_VERSION,\n command: spec.command,\n restore: spec.restore,\n pid: process.pid,\n host: hostname(),\n at: new Date().toISOString(),\n feature: spec.feature,\n moveFrom: repoPath(docsDir, spec.moveFrom),\n moveTo: repoPath(docsDir, spec.moveTo),\n files: staged.map((s) => ({\n path: repoPath(docsDir, s.write.path),\n before: s.before === null ? null : sha256(s.before),\n after: s.content === null ? null : sha256(s.content),\n })),\n };\n await writeDurable(join(docsDir, COMMIT_INTENT), JSON.stringify(intent, null, 2) + \"\\n\");\n}\n\n/** The commit finished (or was rolled back): there is nothing left to recover from. */\nexport async function clearCommitIntent(docsDir: string): Promise<void> {\n await quietRm(join(docsDir, COMMIT_INTENT));\n}\n\n/** The intent record as written, or null when it is absent or unreadable. */\nexport async function readCommitIntent(docsDir: string): Promise<CommitIntent | null> {\n const path = join(docsDir, COMMIT_INTENT);\n if (!existsSync(path)) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(path, \"utf8\"));\n } catch {\n return null;\n }\n const i = parsed as Partial<CommitIntent>;\n if (i === null || typeof i !== \"object\") return null;\n if (i.version !== COMMIT_INTENT_VERSION) return null;\n if (i.command !== \"archive\" && i.command !== \"unarchive\") return null;\n if (i.restore !== \"before\" && i.restore !== \"after\") return null;\n if (typeof i.feature !== \"string\" || typeof i.moveFrom !== \"string\" || typeof i.moveTo !== \"string\") return null;\n if (!Array.isArray(i.files)) return null;\n // The journal is untrusted input, and recovery writes over what it names —\n // the same containment the version-2 reader enforces, because an entry\n // escaping the docs repo would turn repair into an arbitrary write.\n const inside = (rel: string): boolean => isPathInside(join(docsDir), join(docsDir, ...rel.split(\"/\")));\n if (!inside(i.moveFrom) || !inside(i.moveTo)) return null;\n for (const f of i.files) {\n if (typeof f?.path !== \"string\") return null;\n if (!isDigestOrNull(f.before) || !isDigestOrNull(f.after)) return null;\n if (!inside(f.path)) return null;\n }\n return i as CommitIntent;\n}\n\nfunction isDigestOrNull(value: unknown): boolean {\n return value === null || isDigest(value);\n}\n\n"]}
1
+ {"version":3,"file":"intent.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/intent.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `.loam-commit` journal — written before the first swap, cleared after the\n * last — and the record that makes an interrupted commit detectable at all.\n *\n * Reading and writing the record is kept apart from acting on it\n * (`./recover.ts`) because a corrupt record has to stay describable: `doctor`\n * reports a `.loam-commit` it cannot parse, and a module that parsed and\n * repaired in one step would have no way to say that without also attempting\n * the repair.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { isPathInside } from \"../../kernel/path-safety.js\";\nimport { COMMIT_INTENT, writeDurable } from \"../interrupted.js\";\nimport { isDigest, sha256, type StagedWrite } from \"../writes.js\";\nimport { quietRm } from \"../commit.js\";\n\n/** The version-1 record: archive's and unarchive's snapshot-backed journal. */\nconst COMMIT_INTENT_VERSION = 1;\n\nexport interface CommitIntentFile {\n /** Docs-repo-relative path, forward slashes. */\n path: string;\n /** sha256 of the bytes before the swap; null when the swap creates the file. */\n before: string | null;\n /** sha256 of the bytes the swap writes; null when the swap deletes the file. */\n after: string | null;\n}\n\nexport interface CommitIntent {\n version: number;\n /** Which command was committing — the sentence a human reads, not a branch. */\n command: \"archive\" | \"unarchive\";\n /**\n * The state a repair returns the docs to. `archive` undoes (its rollback path\n * would have restored the pre-images anyway); `unarchive` finishes (its target\n * bytes ARE the pre-images, and the merged text it was replacing is recorded\n * nowhere else).\n */\n restore: \"before\" | \"after\";\n pid: number;\n host: string;\n at: string;\n feature: string;\n /** Docs-repo-relative feature directory the commit's final move takes FROM. */\n moveFrom: string;\n /** …and TO. Both landed ⇒ every earlier step landed too. */\n moveTo: string;\n files: CommitIntentFile[];\n}\n\n/**\n * Write the intent record and get it onto the disk before the first swap.\n *\n * fsync, not just write — through `writeDurable`, whose doc says why: the\n * point of the record is to survive the crash that interrupts the commit.\n */\nexport async function writeCommitIntent(\n docsDir: string,\n spec: {\n command: CommitIntent[\"command\"];\n restore: CommitIntent[\"restore\"];\n feature: string;\n moveFrom: string;\n moveTo: string;\n },\n staged: StagedWrite[],\n): Promise<void> {\n const intent: CommitIntent = {\n version: COMMIT_INTENT_VERSION,\n command: spec.command,\n restore: spec.restore,\n pid: process.pid,\n host: hostname(),\n at: new Date().toISOString(),\n feature: spec.feature,\n moveFrom: repoPath(docsDir, spec.moveFrom),\n moveTo: repoPath(docsDir, spec.moveTo),\n files: staged.map((s) => ({\n path: repoPath(docsDir, s.write.path),\n before: s.before === null ? null : sha256(s.before),\n after: s.content === null ? null : sha256(s.content),\n })),\n };\n await writeDurable(join(docsDir, COMMIT_INTENT), JSON.stringify(intent, null, 2) + \"\\n\");\n}\n\n/** The commit finished (or was rolled back): there is nothing left to recover from. */\nexport async function clearCommitIntent(docsDir: string): Promise<void> {\n await quietRm(join(docsDir, COMMIT_INTENT));\n}\n\n/** The intent record as written, or null when it is absent or unreadable. */\nexport async function readCommitIntent(docsDir: string): Promise<CommitIntent | null> {\n const path = join(docsDir, COMMIT_INTENT);\n if (!existsSync(path)) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(path, \"utf8\"));\n } catch {\n return null;\n }\n const i = parsed as Partial<CommitIntent>;\n if (i === null || typeof i !== \"object\") return null;\n if (i.version !== COMMIT_INTENT_VERSION) return null;\n if (i.command !== \"archive\" && i.command !== \"unarchive\") return null;\n if (i.restore !== \"before\" && i.restore !== \"after\") return null;\n if (typeof i.feature !== \"string\" || typeof i.moveFrom !== \"string\" || typeof i.moveTo !== \"string\") return null;\n if (!Array.isArray(i.files)) return null;\n // The journal is untrusted input, and recovery writes over what it names —\n // the same containment the version-2 reader enforces, because an entry\n // escaping the docs repo would turn repair into an arbitrary write.\n const inside = (rel: string): boolean => isPathInside(join(docsDir), join(docsDir, ...rel.split(\"/\")));\n if (!inside(i.moveFrom) || !inside(i.moveTo)) return null;\n for (const f of i.files) {\n if (typeof f?.path !== \"string\") return null;\n if (!isDigestOrNull(f.before) || !isDigestOrNull(f.after)) return null;\n if (!inside(f.path)) return null;\n }\n return i as CommitIntent;\n}\n\nfunction isDigestOrNull(value: unknown): boolean {\n return value === null || isDigest(value);\n}\n\n"]}
@@ -10,15 +10,23 @@
10
10
  * way, a file in NEITHER recorded state was written by somebody else since,
11
11
  * so recovery refuses and names it rather than choosing which of two truths
12
12
  * to destroy.
13
+ *
14
+ * The journaled writers reach it through `openCommitWindow` below — the lock,
15
+ * then this — so the two refusals that can keep a commit window shut are
16
+ * spelled once rather than once per writer. Archive and unarchive are the
17
+ * exception: they call it directly, inside their plan, under a lock they took
18
+ * themselves, and throw its refusal to a handler that maps it.
13
19
  */
14
20
  import { existsSync } from "node:fs";
15
21
  import { readFile, rm } from "node:fs/promises";
16
22
  import { dirname, join } from "node:path";
17
23
  import { repoPath } from "../../envelope/json.js";
24
+ import { acrossRename } from "../../kernel/path-safety.js";
18
25
  import { sha256 } from "../writes.js";
19
26
  import { atomicWrite, quietRmdir } from "../commit.js";
20
- import { SNAPSHOT_DIR } from "../snapshot.js";
27
+ import { PRE_IMAGE_SUFFIX, SNAPSHOT_DIR } from "../snapshot/manifest.js";
21
28
  import { COMMIT_INTENT, InterruptedCommitError, sweepPidTemps, } from "../interrupted.js";
29
+ import { acquireDocsLock, acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from "../lock.js";
22
30
  import { readTxnIntent } from "../txn/journal.js";
23
31
  import { recoverForward } from "../txn/forward.js";
24
32
  import { clearCommitIntent, readCommitIntent, } from "./intent.js";
@@ -85,11 +93,29 @@ export async function recoverInterruptedCommit(docsDir) {
85
93
  // the two ends of the move depending on how far the interrupted run got.
86
94
  // Deliberately NOT the snapshot's (service, artifact) re-keying: the journal
87
95
  // and the snapshot were written seconds apart under the same docs lock, so
88
- // the journal's literal paths and the pre-image keys cannot have diverged —
89
- // a service directory moved by hand between the crash and this repair leaves
90
- // its files "in neither recorded state", which is the refusal below, not a
91
- // resolution question.
96
+ // no service directory can have moved between the journal's literal paths
97
+ // and the pre-image keys — one moved by hand between the crash and this
98
+ // repair leaves its files "in neither recorded state", which is the refusal
99
+ // below, not a resolution question. The one directory that DOES move between
100
+ // them is the commit's own, and that is the second path tried below.
101
+ //
102
+ // Both pre-image NAMES are tried, the suffixed one first: a snapshot this
103
+ // loam wrote stores `<path>` + PRE_IMAGE_SUFFIX (version 4), while a commit
104
+ // an older loam was killed in left one under the living name. Trying both is
105
+ // safe for the same reason the two ends are: whichever is found must still
106
+ // hash to the digest the journal recorded, or the repair refuses below.
107
+ //
108
+ // And both PATHS: the literal one and its image across the move. The
109
+ // feature's own documents are the one place the journal and the snapshot do
110
+ // disagree, by construction — archive journals them where it writes them,
111
+ // inside the feature as it stands, and the snapshot records them where the
112
+ // move leaves them (`../snapshot/snapshot.ts`, `movesTo`), because that is
113
+ // where an undo finds them. A killed archive whose relinked intent was
114
+ // already swapped is therefore repaired from `features/archive/<FEAT>/…`'s
115
+ // pre-image, and refusing it as "gone" made the retry impossible.
92
116
  const roots = [join(moveFrom, SNAPSHOT_DIR, "files"), join(moveTo, SNAPSHOT_DIR, "files")];
117
+ const across = (path) => repoPath(docsDir, acrossRename(target(path), moveFrom, moveTo));
118
+ const names = (path) => [...new Set([path, across(path)])].flatMap((p) => [`${p}${PRE_IMAGE_SUFFIX}`, p]);
93
119
  const repaired = [];
94
120
  for (const f of pending) {
95
121
  const file = target(f.path);
@@ -100,7 +126,7 @@ export async function recoverInterruptedCommit(docsDir) {
100
126
  repaired.push(f.path);
101
127
  continue;
102
128
  }
103
- const source = roots.map((r) => join(r, ...f.path.split("/"))).find((p) => existsSync(p));
129
+ const source = roots.flatMap((r) => names(f.path).map((n) => join(r, ...n.split("/")))).find((p) => existsSync(p));
104
130
  if (source === undefined) {
105
131
  throw new InterruptedCommitError(`a \`loam ${intent.command} ${intent.feature}\` was interrupted mid-commit and ${f.path} is half-written, but ` +
106
132
  `the pre-image that would repair it is gone. Restore ${f.path} from version control, delete ` +
@@ -123,4 +149,56 @@ async function finishRecovery(docsDir, intent) {
123
149
  await sweepPidTemps(intent.files.map((f) => dirname(join(docsDir, ...f.path.split("/")))), intent.pid);
124
150
  await clearCommitIntent(docsDir);
125
151
  }
152
+ /**
153
+ * Open a writer's commit window: take the lock in `root` — the docs repo, or
154
+ * gherkin's emission root in the service repo — then refuse or repair a
155
+ * predecessor's interrupted commit before this run reads a byte.
156
+ *
157
+ * `waitMs` is how long to sit out a live holder before answering `docs-busy`,
158
+ * and 0 refuses on first contact; `acquireDocsLockWaiting` says which writers
159
+ * wait and why archive does not.
160
+ *
161
+ * Each writer here used to spell this sequence out by hand, and the copy in
162
+ * `verify` once let its recovery refusal keep the lock — one bad answer set
163
+ * became a wedged docs repo for everyone else. So the refusal releases HERE,
164
+ * before it is returned, and an error that is neither refusal releases before
165
+ * it propagates.
166
+ */
167
+ export async function openCommitWindow(root, waitMs) {
168
+ let release;
169
+ try {
170
+ release = waitMs > 0 ? await acquireDocsLockWaiting(root, waitMs) : await acquireDocsLock(root);
171
+ }
172
+ catch (err) {
173
+ if (!(err instanceof DocsBusyError))
174
+ throw err;
175
+ return { ok: false, code: "docs-busy", message: err.message };
176
+ }
177
+ try {
178
+ return { ok: true, recovered: await recoverInterruptedCommit(root), release };
179
+ }
180
+ catch (err) {
181
+ await release();
182
+ if (!(err instanceof InterruptedCommitError))
183
+ throw err;
184
+ return { ok: false, code: "commit-interrupted", message: err.message };
185
+ }
186
+ }
187
+ /**
188
+ * Recover a predecessor's interrupted commit NOW and hand the lock straight
189
+ * back — for a writer whose slow half runs UNLOCKED over the very bytes a
190
+ * half-commit corrupts (vouch's verification, gherkin's reconcile); each says
191
+ * at its call site why recovery inside its own commit window comes too late.
192
+ * Cheap when there is nothing to do: no journal, no lock, no I/O beyond one
193
+ * existsSync.
194
+ */
195
+ export async function recoverIfJournaled(root) {
196
+ if (!existsSync(join(root, COMMIT_INTENT)))
197
+ return { ok: true, recovered: null };
198
+ const opened = await openCommitWindow(root, LOCK_WAIT_MS);
199
+ if (!opened.ok)
200
+ return opened;
201
+ await opened.release();
202
+ return { ok: true, recovered: opened.recovered };
203
+ }
126
204
  //# sourceMappingURL=recover.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"recover.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/recover.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,aAAa,EAEb,sBAAsB,EACtB,aAAa,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,iBAAiB,EACjB,gBAAgB,GAGjB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,OAAe;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,sEAAsE;QACtE,8DAA8D;QAC9D,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACtD,MAAM,IAAI,sBAAsB,CAC9B,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,qFAAqF;YAC7G,6EAA6E,aAAa,gBAAgB,CAC7G,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,6EAA6E;IAC7E,mDAAmD;IACnD,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChD,MAAM,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACtC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAClG,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACzE,MAAM,MAAM,GAAG,CAAC,CAAmB,EAAiB,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC1G,MAAM,KAAK,GAAG,CAAC,CAAmB,EAAiB,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACzG,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACnE,IAAI,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC;YAAE,SAAS;QAChC,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;YACjC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,sBAAsB,CAC9B,YAAY,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,sCAAsC,MAAM,CAAC,GAAG,OAAO,MAAM,CAAC,IAAI,IAAI;YAChH,GAAG,MAAM,CAAC,EAAE,SAAS,OAAO,CAAC,MAAM,yEAAyE;YAC5G,yDAAyD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,iCAAiC;YAC5G,oEAAoE,aAAa,gBAAgB,CACpG,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACtC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IACnG,CAAC;IAED,4EAA4E;IAC5E,yEAAyE;IACzE,6EAA6E;IAC7E,2EAA2E;IAC3E,4EAA4E;IAC5E,6EAA6E;IAC7E,2EAA2E;IAC3E,uBAAuB;IACvB,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3F,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAChC,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YAChC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACtB,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1F,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,sBAAsB,CAC9B,YAAY,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,qCAAqC,CAAC,CAAC,IAAI,wBAAwB;gBAC7G,uDAAuD,CAAC,CAAC,IAAI,gCAAgC;gBAC7F,GAAG,aAAa,gBAAgB,CACnC,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;YAC3B,MAAM,IAAI,sBAAsB,CAC9B,YAAY,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,qCAAqC,CAAC,CAAC,IAAI,wBAAwB;gBAC7G,2GAA2G;gBAC3G,yBAAyB,CAAC,CAAC,IAAI,iCAAiC,aAAa,gBAAgB,CAChG,CAAC;QACJ,CAAC;QACD,MAAM,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC/B,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IACD,MAAM,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACtC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AAC7F,CAAC;AAED,yFAAyF;AACzF,KAAK,UAAU,cAAc,CAAC,OAAe,EAAE,MAAoB;IACjE,MAAM,aAAa,CACjB,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EACrE,MAAM,CAAC,GAAG,CACX,CAAC;IACF,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/**\n * Refuse or repair an interrupted commit, before this run reads anything.\n *\n * Split from `./intent.ts` because this is the only module on the write path\n * that writes bytes nobody asked for in this run. Two journal versions, two\n * doctrines, one dispatch: a version-1 record (archive/unarchive) is repaired\n * BACKWARD from the snapshot pre-images, each verified against the digest the\n * record captured; a version-2 record is handed to `txn/forward.ts`, which\n * rolls FORWARD from the staged temps under the same verification. Either\n * way, a file in NEITHER recorded state was written by somebody else since,\n * so recovery refuses and names it rather than choosing which of two truths\n * to destroy.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { sha256 } from \"../writes.js\";\nimport { atomicWrite, quietRmdir } from \"../commit.js\";\nimport { SNAPSHOT_DIR } from \"../snapshot.js\";\nimport {\n COMMIT_INTENT,\n type CommitRecovery,\n InterruptedCommitError,\n sweepPidTemps,\n} from \"../interrupted.js\";\nimport { readTxnIntent } from \"../txn/journal.js\";\nimport { recoverForward } from \"../txn/forward.js\";\nimport {\n clearCommitIntent,\n readCommitIntent,\n type CommitIntent,\n type CommitIntentFile,\n} from \"./intent.js\";\n\n/**\n * Refuse or repair before this run reads anything. Returns null when there is\n * nothing to recover.\n *\n * Repair is only ever the bytes the interrupted run already had on disk: each\n * file still in the wrong state is rewritten from the snapshot pre-image and\n * only after that pre-image's own sha256 matches the digest the record captured\n * before the crash. A file in NEITHER state was written by somebody else since,\n * so this refuses and names it rather than choosing which of two truths to\n * destroy — the same doctrine `rollbackStaged` follows when it leaves a file as\n * found.\n */\nexport async function recoverInterruptedCommit(docsDir: string): Promise<CommitRecovery | null> {\n const path = join(docsDir, COMMIT_INTENT);\n if (!existsSync(path)) return null;\n const intent = await readCommitIntent(docsDir);\n if (intent === null) {\n // Version 2 — the smaller transaction's journal. Same file, different\n // recovery: forward from the staged temps, in txn/forward.ts.\n const txn = await readTxnIntent(docsDir);\n if (txn !== null) return recoverForward(docsDir, txn);\n throw new InterruptedCommitError(\n `${repoPath(docsDir, path)} records a commit this loam cannot read, so it cannot tell a half-merged docs repo ` +\n `from a healthy one. Check the living docs against version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n const moveFrom = join(docsDir, ...intent.moveFrom.split(\"/\"));\n const moveTo = join(docsDir, ...intent.moveTo.split(\"/\"));\n // The move is the commit's LAST step. If it landed, every swap before it did\n // too, and only the record's own removal was lost.\n if (existsSync(moveTo) && !existsSync(moveFrom)) {\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"completed\", repaired: [] };\n }\n\n const target = (rel: string): string => join(docsDir, ...rel.split(\"/\"));\n const wanted = (f: CommitIntentFile): string | null => (intent.restore === \"before\" ? f.before : f.after);\n const other = (f: CommitIntentFile): string | null => (intent.restore === \"before\" ? f.after : f.before);\n const pending: CommitIntentFile[] = [];\n const unknown: string[] = [];\n for (const f of intent.files) {\n const file = target(f.path);\n const now = existsSync(file) ? sha256(await readFile(file)) : null;\n if (now === wanted(f)) continue;\n if (now === other(f)) pending.push(f);\n else unknown.push(f.path);\n }\n if (unknown.length > 0) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit (pid ${intent.pid} on ${intent.host}, ` +\n `${intent.at}) and ${unknown.length} of the file(s) it was writing now hold bytes that are neither what it ` +\n `found nor what it wrote — somebody edited them since: ${unknown.join(\", \")}. loam will not choose between ` +\n `those two truths. Reconcile them against version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n if (pending.length === 0) {\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"consistent\", repaired: [] };\n }\n\n // The pre-images travel inside the feature directory, so they are at one of\n // the two ends of the move depending on how far the interrupted run got.\n // Deliberately NOT the snapshot's (service, artifact) re-keying: the journal\n // and the snapshot were written seconds apart under the same docs lock, so\n // the journal's literal paths and the pre-image keys cannot have diverged —\n // a service directory moved by hand between the crash and this repair leaves\n // its files \"in neither recorded state\", which is the refusal below, not a\n // resolution question.\n const roots = [join(moveFrom, SNAPSHOT_DIR, \"files\"), join(moveTo, SNAPSHOT_DIR, \"files\")];\n const repaired: string[] = [];\n for (const f of pending) {\n const file = target(f.path);\n const want = wanted(f);\n if (want === null) {\n await rm(file, { force: true });\n await quietRmdir(dirname(file));\n repaired.push(f.path);\n continue;\n }\n const source = roots.map((r) => join(r, ...f.path.split(\"/\"))).find((p) => existsSync(p));\n if (source === undefined) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit and ${f.path} is half-written, but ` +\n `the pre-image that would repair it is gone. Restore ${f.path} from version control, delete ` +\n `${COMMIT_INTENT}, then re-run.`,\n );\n }\n const bytes = await readFile(source);\n if (sha256(bytes) !== want) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit and ${f.path} is half-written, but ` +\n `the pre-image that would repair it no longer matches the digest recorded before the commit — it has been ` +\n `edited since. Restore ${f.path} from version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n await atomicWrite(file, bytes);\n repaired.push(f.path);\n }\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"repaired\", repaired };\n}\n\n/** Drop the record, and (via the shared pid-scoped sweep) the killed run's own temps. */\nasync function finishRecovery(docsDir: string, intent: CommitIntent): Promise<void> {\n await sweepPidTemps(\n intent.files.map((f) => dirname(join(docsDir, ...f.path.split(\"/\")))),\n intent.pid,\n );\n await clearCommitIntent(docsDir);\n}\n\n\n"]}
1
+ {"version":3,"file":"recover.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/recover.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Refuse or repair an interrupted commit, before this run reads anything.\n *\n * Split from `./intent.ts` because this is the only module on the write path\n * that writes bytes nobody asked for in this run. Two journal versions, two\n * doctrines, one dispatch: a version-1 record (archive/unarchive) is repaired\n * BACKWARD from the snapshot pre-images, each verified against the digest the\n * record captured; a version-2 record is handed to `txn/forward.ts`, which\n * rolls FORWARD from the staged temps under the same verification. Either\n * way, a file in NEITHER recorded state was written by somebody else since,\n * so recovery refuses and names it rather than choosing which of two truths\n * to destroy.\n *\n * The journaled writers reach it through `openCommitWindow` below — the lock,\n * then this — so the two refusals that can keep a commit window shut are\n * spelled once rather than once per writer. Archive and unarchive are the\n * exception: they call it directly, inside their plan, under a lock they took\n * themselves, and throw its refusal to a handler that maps it.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { acrossRename } from \"../../kernel/path-safety.js\";\nimport { sha256 } from \"../writes.js\";\nimport { atomicWrite, quietRmdir } from \"../commit.js\";\nimport { PRE_IMAGE_SUFFIX, SNAPSHOT_DIR } from \"../snapshot/manifest.js\";\nimport {\n COMMIT_INTENT,\n type CommitRecovery,\n InterruptedCommitError,\n sweepPidTemps,\n} from \"../interrupted.js\";\nimport { acquireDocsLock, acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../lock.js\";\nimport { readTxnIntent } from \"../txn/journal.js\";\nimport { recoverForward } from \"../txn/forward.js\";\nimport {\n clearCommitIntent,\n readCommitIntent,\n type CommitIntent,\n type CommitIntentFile,\n} from \"./intent.js\";\n\n/**\n * Refuse or repair before this run reads anything. Returns null when there is\n * nothing to recover.\n *\n * Repair is only ever the bytes the interrupted run already had on disk: each\n * file still in the wrong state is rewritten from the snapshot pre-image and\n * only after that pre-image's own sha256 matches the digest the record captured\n * before the crash. A file in NEITHER state was written by somebody else since,\n * so this refuses and names it rather than choosing which of two truths to\n * destroy — the same doctrine `rollbackStaged` follows when it leaves a file as\n * found.\n */\nexport async function recoverInterruptedCommit(docsDir: string): Promise<CommitRecovery | null> {\n const path = join(docsDir, COMMIT_INTENT);\n if (!existsSync(path)) return null;\n const intent = await readCommitIntent(docsDir);\n if (intent === null) {\n // Version 2 — the smaller transaction's journal. Same file, different\n // recovery: forward from the staged temps, in txn/forward.ts.\n const txn = await readTxnIntent(docsDir);\n if (txn !== null) return recoverForward(docsDir, txn);\n throw new InterruptedCommitError(\n `${repoPath(docsDir, path)} records a commit this loam cannot read, so it cannot tell a half-merged docs repo ` +\n `from a healthy one. Check the living docs against version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n const moveFrom = join(docsDir, ...intent.moveFrom.split(\"/\"));\n const moveTo = join(docsDir, ...intent.moveTo.split(\"/\"));\n // The move is the commit's LAST step. If it landed, every swap before it did\n // too, and only the record's own removal was lost.\n if (existsSync(moveTo) && !existsSync(moveFrom)) {\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"completed\", repaired: [] };\n }\n\n const target = (rel: string): string => join(docsDir, ...rel.split(\"/\"));\n const wanted = (f: CommitIntentFile): string | null => (intent.restore === \"before\" ? f.before : f.after);\n const other = (f: CommitIntentFile): string | null => (intent.restore === \"before\" ? f.after : f.before);\n const pending: CommitIntentFile[] = [];\n const unknown: string[] = [];\n for (const f of intent.files) {\n const file = target(f.path);\n const now = existsSync(file) ? sha256(await readFile(file)) : null;\n if (now === wanted(f)) continue;\n if (now === other(f)) pending.push(f);\n else unknown.push(f.path);\n }\n if (unknown.length > 0) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit (pid ${intent.pid} on ${intent.host}, ` +\n `${intent.at}) and ${unknown.length} of the file(s) it was writing now hold bytes that are neither what it ` +\n `found nor what it wrote — somebody edited them since: ${unknown.join(\", \")}. loam will not choose between ` +\n `those two truths. Reconcile them against version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n if (pending.length === 0) {\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"consistent\", repaired: [] };\n }\n\n // The pre-images travel inside the feature directory, so they are at one of\n // the two ends of the move depending on how far the interrupted run got.\n // Deliberately NOT the snapshot's (service, artifact) re-keying: the journal\n // and the snapshot were written seconds apart under the same docs lock, so\n // no service directory can have moved between the journal's literal paths\n // and the pre-image keys — one moved by hand between the crash and this\n // repair leaves its files \"in neither recorded state\", which is the refusal\n // below, not a resolution question. The one directory that DOES move between\n // them is the commit's own, and that is the second path tried below.\n //\n // Both pre-image NAMES are tried, the suffixed one first: a snapshot this\n // loam wrote stores `<path>` + PRE_IMAGE_SUFFIX (version 4), while a commit\n // an older loam was killed in left one under the living name. Trying both is\n // safe for the same reason the two ends are: whichever is found must still\n // hash to the digest the journal recorded, or the repair refuses below.\n //\n // And both PATHS: the literal one and its image across the move. The\n // feature's own documents are the one place the journal and the snapshot do\n // disagree, by construction — archive journals them where it writes them,\n // inside the feature as it stands, and the snapshot records them where the\n // move leaves them (`../snapshot/snapshot.ts`, `movesTo`), because that is\n // where an undo finds them. A killed archive whose relinked intent was\n // already swapped is therefore repaired from `features/archive/<FEAT>/…`'s\n // pre-image, and refusing it as \"gone\" made the retry impossible.\n const roots = [join(moveFrom, SNAPSHOT_DIR, \"files\"), join(moveTo, SNAPSHOT_DIR, \"files\")];\n const across = (path: string): string => repoPath(docsDir, acrossRename(target(path), moveFrom, moveTo));\n const names = (path: string): string[] =>\n [...new Set([path, across(path)])].flatMap((p) => [`${p}${PRE_IMAGE_SUFFIX}`, p]);\n const repaired: string[] = [];\n for (const f of pending) {\n const file = target(f.path);\n const want = wanted(f);\n if (want === null) {\n await rm(file, { force: true });\n await quietRmdir(dirname(file));\n repaired.push(f.path);\n continue;\n }\n const source = roots.flatMap((r) => names(f.path).map((n) => join(r, ...n.split(\"/\")))).find((p) => existsSync(p));\n if (source === undefined) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit and ${f.path} is half-written, but ` +\n `the pre-image that would repair it is gone. Restore ${f.path} from version control, delete ` +\n `${COMMIT_INTENT}, then re-run.`,\n );\n }\n const bytes = await readFile(source);\n if (sha256(bytes) !== want) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit and ${f.path} is half-written, but ` +\n `the pre-image that would repair it no longer matches the digest recorded before the commit — it has been ` +\n `edited since. Restore ${f.path} from version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n await atomicWrite(file, bytes);\n repaired.push(f.path);\n }\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"repaired\", repaired };\n}\n\n/** Drop the record, and (via the shared pid-scoped sweep) the killed run's own temps. */\nasync function finishRecovery(docsDir: string, intent: CommitIntent): Promise<void> {\n await sweepPidTemps(\n intent.files.map((f) => dirname(join(docsDir, ...f.path.split(\"/\")))),\n intent.pid,\n );\n await clearCommitIntent(docsDir);\n}\n\n/** The lock is held and a predecessor's interrupted commit is resolved: this run may read, then write. */\nexport interface OpenCommitWindow {\n ok: true;\n recovered: CommitRecovery | null;\n /** Idempotent. The caller's one obligation, in a `finally`. */\n release: () => Promise<void>;\n}\n\n/** The window did not open. Nothing was read or written, and no lock is held. */\nexport interface CommitWindowRefusal {\n ok: false;\n code: \"docs-busy\" | \"commit-interrupted\";\n message: string;\n}\n\n/**\n * Open a writer's commit window: take the lock in `root` — the docs repo, or\n * gherkin's emission root in the service repo — then refuse or repair a\n * predecessor's interrupted commit before this run reads a byte.\n *\n * `waitMs` is how long to sit out a live holder before answering `docs-busy`,\n * and 0 refuses on first contact; `acquireDocsLockWaiting` says which writers\n * wait and why archive does not.\n *\n * Each writer here used to spell this sequence out by hand, and the copy in\n * `verify` once let its recovery refusal keep the lock — one bad answer set\n * became a wedged docs repo for everyone else. So the refusal releases HERE,\n * before it is returned, and an error that is neither refusal releases before\n * it propagates.\n */\nexport async function openCommitWindow(root: string, waitMs: number): Promise<OpenCommitWindow | CommitWindowRefusal> {\n let release: () => Promise<void>;\n try {\n release = waitMs > 0 ? await acquireDocsLockWaiting(root, waitMs) : await acquireDocsLock(root);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n return { ok: false, code: \"docs-busy\", message: err.message };\n }\n try {\n return { ok: true, recovered: await recoverInterruptedCommit(root), release };\n } catch (err) {\n await release();\n if (!(err instanceof InterruptedCommitError)) throw err;\n return { ok: false, code: \"commit-interrupted\", message: err.message };\n }\n}\n\n/**\n * Recover a predecessor's interrupted commit NOW and hand the lock straight\n * back — for a writer whose slow half runs UNLOCKED over the very bytes a\n * half-commit corrupts (vouch's verification, gherkin's reconcile); each says\n * at its call site why recovery inside its own commit window comes too late.\n * Cheap when there is nothing to do: no journal, no lock, no I/O beyond one\n * existsSync.\n */\nexport async function recoverIfJournaled(\n root: string,\n): Promise<{ ok: true; recovered: CommitRecovery | null } | CommitWindowRefusal> {\n if (!existsSync(join(root, COMMIT_INTENT))) return { ok: true, recovered: null };\n const opened = await openCommitWindow(root, LOCK_WAIT_MS);\n if (!opened.ok) return opened;\n await opened.release();\n return { ok: true, recovered: opened.recovered };\n}\n\n"]}