@ybotok/loam 0.1.0-beta.3 → 0.2.0-alpha.2

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 (1036) hide show
  1. package/CHANGELOG.md +2635 -86
  2. package/COMPARISON.md +80 -63
  3. package/CONTRIBUTING.md +84 -0
  4. package/MIGRATING-from-OpenSpec.md +213 -54
  5. package/QUICKSTART.md +125 -0
  6. package/README.md +450 -150
  7. package/ROADMAP.md +599 -0
  8. package/SCHEMA.md +2355 -256
  9. package/SECURITY.md +21 -3
  10. package/WORKFLOW.md +701 -0
  11. package/dist/cli.js +113 -19
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/adopt/adopt.js +209 -0
  14. package/dist/commands/adopt/adopt.js.map +1 -0
  15. package/dist/commands/adopt/render.js +268 -0
  16. package/dist/commands/adopt/render.js.map +1 -0
  17. package/dist/commands/archive/archive.js +77 -0
  18. package/dist/commands/archive/archive.js.map +1 -0
  19. package/dist/commands/archive/plan/contracts/asyncapi.js +178 -0
  20. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -0
  21. package/dist/commands/archive/plan/contracts/openapi.js +159 -0
  22. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -0
  23. package/dist/commands/archive/plan/gate.js +323 -0
  24. package/dist/commands/archive/plan/gate.js.map +1 -0
  25. package/dist/commands/archive/plan/landscape.js +180 -0
  26. package/dist/commands/archive/plan/landscape.js.map +1 -0
  27. package/dist/commands/archive/plan/refusal.js +111 -0
  28. package/dist/commands/archive/plan/refusal.js.map +1 -0
  29. package/dist/commands/archive/plan/specs.js +237 -0
  30. package/dist/commands/archive/plan/specs.js.map +1 -0
  31. package/dist/commands/archive/plan/state.js +27 -0
  32. package/dist/commands/archive/plan/state.js.map +1 -0
  33. package/dist/commands/archive/run.js +252 -0
  34. package/dist/commands/archive/run.js.map +1 -0
  35. package/dist/commands/context/context.js +99 -0
  36. package/dist/commands/context/context.js.map +1 -0
  37. package/dist/commands/context/print.js +283 -0
  38. package/dist/commands/context/print.js.map +1 -0
  39. package/dist/commands/delta/delta.js +279 -0
  40. package/dist/commands/delta/delta.js.map +1 -0
  41. package/dist/commands/delta/print.js +205 -0
  42. package/dist/commands/delta/print.js.map +1 -0
  43. package/dist/commands/dependencies.js +9 -7
  44. package/dist/commands/dependencies.js.map +1 -1
  45. package/dist/commands/diff/diff.js +93 -0
  46. package/dist/commands/diff/diff.js.map +1 -0
  47. package/dist/commands/diff/print.js +38 -0
  48. package/dist/commands/diff/print.js.map +1 -0
  49. package/dist/commands/doctor.js +5 -4
  50. package/dist/commands/doctor.js.map +1 -1
  51. package/dist/commands/explain/explain.js +151 -0
  52. package/dist/commands/explain/explain.js.map +1 -0
  53. package/dist/commands/explore.js +29 -24
  54. package/dist/commands/explore.js.map +1 -1
  55. package/dist/commands/gate/gate.js +115 -0
  56. package/dist/commands/gate/gate.js.map +1 -0
  57. package/dist/commands/gate/print.js +54 -0
  58. package/dist/commands/gate/print.js.map +1 -0
  59. package/dist/commands/gherkin/commit.js +146 -0
  60. package/dist/commands/gherkin/commit.js.map +1 -0
  61. package/dist/commands/gherkin/gherkin.js +246 -0
  62. package/dist/commands/gherkin/gherkin.js.map +1 -0
  63. package/dist/commands/gherkin/reconcile.js +149 -0
  64. package/dist/commands/gherkin/reconcile.js.map +1 -0
  65. package/dist/commands/gherkin/render.js +77 -0
  66. package/dist/commands/gherkin/render.js.map +1 -0
  67. package/dist/commands/init/example.js +204 -0
  68. package/dist/commands/init/example.js.map +1 -0
  69. package/dist/commands/init/first-hour.js +28 -0
  70. package/dist/commands/init/first-hour.js.map +1 -0
  71. package/dist/commands/{init.js → init/init.js} +146 -71
  72. package/dist/commands/init/init.js.map +1 -0
  73. package/dist/commands/init/mcp.js +131 -0
  74. package/dist/commands/init/mcp.js.map +1 -0
  75. package/dist/commands/init/options.js +78 -0
  76. package/dist/commands/init/options.js.map +1 -0
  77. package/dist/commands/instructions.js +72 -14
  78. package/dist/commands/instructions.js.map +1 -1
  79. package/dist/commands/list/campaign/campaign.js +65 -0
  80. package/dist/commands/list/campaign/campaign.js.map +1 -0
  81. package/dist/commands/list/campaign/owners.js +65 -0
  82. package/dist/commands/list/campaign/owners.js.map +1 -0
  83. package/dist/commands/list/json.js +157 -0
  84. package/dist/commands/list/json.js.map +1 -0
  85. package/dist/commands/list/list.js +343 -0
  86. package/dist/commands/list/list.js.map +1 -0
  87. package/dist/commands/list/print.js +301 -0
  88. package/dist/commands/list/print.js.map +1 -0
  89. package/dist/commands/list/review.js +120 -0
  90. package/dist/commands/list/review.js.map +1 -0
  91. package/dist/commands/list/views.js +105 -0
  92. package/dist/commands/list/views.js.map +1 -0
  93. package/dist/commands/mcp/dispatch.js +183 -0
  94. package/dist/commands/mcp/dispatch.js.map +1 -0
  95. package/dist/commands/mcp/mcp.js +34 -0
  96. package/dist/commands/mcp/mcp.js.map +1 -0
  97. package/dist/commands/mcp/serve.js +55 -0
  98. package/dist/commands/mcp/serve.js.map +1 -0
  99. package/dist/commands/migrate-openspec/materialize/active.js +170 -0
  100. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -0
  101. package/dist/commands/migrate-openspec/materialize/feature.js +41 -0
  102. package/dist/commands/migrate-openspec/materialize/feature.js.map +1 -0
  103. package/dist/commands/migrate-openspec/materialize/living.js +62 -0
  104. package/dist/commands/migrate-openspec/materialize/living.js.map +1 -0
  105. package/dist/commands/migrate-openspec/materialize/stage.js +142 -0
  106. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -0
  107. package/dist/commands/migrate-openspec/materialize/target.js +219 -0
  108. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -0
  109. package/dist/commands/migrate-openspec/migrate-openspec.js +142 -0
  110. package/dist/commands/migrate-openspec/migrate-openspec.js.map +1 -0
  111. package/dist/commands/migrate-openspec/openspec/decisions.js +91 -0
  112. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -0
  113. package/dist/commands/migrate-openspec/openspec/error.js +19 -0
  114. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -0
  115. package/dist/commands/migrate-openspec/openspec/mapping.js +170 -0
  116. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -0
  117. package/dist/commands/migrate-openspec/openspec/paths.js +43 -0
  118. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -0
  119. package/dist/commands/migrate-openspec/openspec/read.js +49 -0
  120. package/dist/commands/migrate-openspec/openspec/read.js.map +1 -0
  121. package/dist/commands/migrate-openspec/print.js +89 -0
  122. package/dist/commands/migrate-openspec/print.js.map +1 -0
  123. package/dist/commands/new/commit.js +76 -0
  124. package/dist/commands/new/commit.js.map +1 -0
  125. package/dist/commands/new/new.js +283 -0
  126. package/dist/commands/new/new.js.map +1 -0
  127. package/dist/commands/new/templates.js +355 -0
  128. package/dist/commands/new/templates.js.map +1 -0
  129. package/dist/commands/open.js +203 -0
  130. package/dist/commands/open.js.map +1 -0
  131. package/dist/commands/policy/format.js +152 -0
  132. package/dist/commands/policy/format.js.map +1 -0
  133. package/dist/commands/{docs-repo-gate.js → policy/gate.js} +11 -11
  134. package/dist/commands/policy/gate.js.map +1 -0
  135. package/dist/commands/rebase/edit.js +106 -0
  136. package/dist/commands/rebase/edit.js.map +1 -0
  137. package/dist/commands/rebase/living.js +185 -0
  138. package/dist/commands/rebase/living.js.map +1 -0
  139. package/dist/commands/rebase/plan.js +245 -0
  140. package/dist/commands/rebase/plan.js.map +1 -0
  141. package/dist/commands/rebase/rebase.js +275 -0
  142. package/dist/commands/rebase/rebase.js.map +1 -0
  143. package/dist/commands/seed/commit.js +192 -0
  144. package/dist/commands/seed/commit.js.map +1 -0
  145. package/dist/commands/seed/plan.js +133 -0
  146. package/dist/commands/seed/plan.js.map +1 -0
  147. package/dist/commands/seed/seed.js +172 -0
  148. package/dist/commands/seed/seed.js.map +1 -0
  149. package/dist/commands/show/feature.js +178 -0
  150. package/dist/commands/show/feature.js.map +1 -0
  151. package/dist/commands/show/marks.js +24 -0
  152. package/dist/commands/show/marks.js.map +1 -0
  153. package/dist/commands/{show.js → show/service.js} +63 -152
  154. package/dist/commands/show/service.js.map +1 -0
  155. package/dist/commands/show/show.js +77 -0
  156. package/dist/commands/show/show.js.map +1 -0
  157. package/dist/commands/status/json.js +45 -0
  158. package/dist/commands/status/json.js.map +1 -0
  159. package/dist/commands/status/print.js +157 -0
  160. package/dist/commands/status/print.js.map +1 -0
  161. package/dist/commands/status/status.js +119 -0
  162. package/dist/commands/status/status.js.map +1 -0
  163. package/dist/commands/steps/steps.js +189 -0
  164. package/dist/commands/steps/steps.js.map +1 -0
  165. package/dist/commands/subsystem/edit.js +192 -0
  166. package/dist/commands/subsystem/edit.js.map +1 -0
  167. package/dist/commands/subsystem/move.js +265 -0
  168. package/dist/commands/subsystem/move.js.map +1 -0
  169. package/dist/commands/subsystem/query.js +115 -0
  170. package/dist/commands/subsystem/query.js.map +1 -0
  171. package/dist/commands/subsystem/subsystem.js +101 -0
  172. package/dist/commands/subsystem/subsystem.js.map +1 -0
  173. package/dist/commands/subsystem/sync.js +103 -0
  174. package/dist/commands/subsystem/sync.js.map +1 -0
  175. package/dist/commands/subsystem/txn/txn.js +94 -0
  176. package/dist/commands/subsystem/txn/txn.js.map +1 -0
  177. package/dist/commands/subsystem/txn/views.js +70 -0
  178. package/dist/commands/subsystem/txn/views.js.map +1 -0
  179. package/dist/commands/unarchive/manifest.js +160 -0
  180. package/dist/commands/unarchive/manifest.js.map +1 -0
  181. package/dist/commands/{unarchive.js → unarchive/restore.js} +47 -160
  182. package/dist/commands/unarchive/restore.js.map +1 -0
  183. package/dist/commands/unarchive/unarchive.js +67 -0
  184. package/dist/commands/unarchive/unarchive.js.map +1 -0
  185. package/dist/commands/validate/arch-coverage.js +204 -0
  186. package/dist/commands/validate/arch-coverage.js.map +1 -0
  187. package/dist/commands/validate/checks/fleet-shape.js +296 -0
  188. package/dist/commands/validate/checks/fleet-shape.js.map +1 -0
  189. package/dist/commands/validate/checks/health-deps.js +55 -0
  190. package/dist/commands/validate/checks/health-deps.js.map +1 -0
  191. package/dist/commands/validate/checks/requirements.js +254 -0
  192. package/dist/commands/validate/checks/requirements.js.map +1 -0
  193. package/dist/commands/validate/checks/sources.js +76 -0
  194. package/dist/commands/validate/checks/sources.js.map +1 -0
  195. package/dist/commands/validate/checks/vocabulary.js +31 -0
  196. package/dist/commands/validate/checks/vocabulary.js.map +1 -0
  197. package/dist/commands/validate/feature.js +216 -0
  198. package/dist/commands/validate/feature.js.map +1 -0
  199. package/dist/commands/validate/fleet/census.js +69 -0
  200. package/dist/commands/validate/fleet/census.js.map +1 -0
  201. package/dist/commands/validate/fleet/kind-tags.js +39 -0
  202. package/dist/commands/validate/fleet/kind-tags.js.map +1 -0
  203. package/dist/commands/validate/fleet/landscape.js +353 -0
  204. package/dist/commands/validate/fleet/landscape.js.map +1 -0
  205. package/dist/commands/validate/fleet/load.js +52 -0
  206. package/dist/commands/validate/fleet/load.js.map +1 -0
  207. package/dist/commands/validate/fleet/obligations.js +270 -0
  208. package/dist/commands/validate/fleet/obligations.js.map +1 -0
  209. package/dist/commands/validate/fleet/scorecard/adoption.js +102 -0
  210. package/dist/commands/validate/fleet/scorecard/adoption.js.map +1 -0
  211. package/dist/commands/validate/fleet/scorecard/contracts.js +98 -0
  212. package/dist/commands/validate/fleet/scorecard/contracts.js.map +1 -0
  213. package/dist/commands/validate/fleet/scorecard/print.js +63 -0
  214. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -0
  215. package/dist/commands/validate/fleet/scorecard/scorecard.js +156 -0
  216. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -0
  217. package/dist/commands/validate/fleet/usecases/capability-tag.js +94 -0
  218. package/dist/commands/validate/fleet/usecases/capability-tag.js.map +1 -0
  219. package/dist/commands/validate/fleet/usecases/place.js +13 -0
  220. package/dist/commands/validate/fleet/usecases/place.js.map +1 -0
  221. package/dist/commands/validate/fleet/usecases/requirement-tag.js +95 -0
  222. package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +1 -0
  223. package/dist/commands/validate/fleet/usecases/steps.js +197 -0
  224. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -0
  225. package/dist/commands/validate/fleet/usecases/usecases.js +99 -0
  226. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -0
  227. package/dist/commands/validate/fleet/views/ids.js +40 -0
  228. package/dist/commands/validate/fleet/views/ids.js.map +1 -0
  229. package/dist/commands/validate/fleet/views/stale.js +59 -0
  230. package/dist/commands/validate/fleet/views/stale.js.map +1 -0
  231. package/dist/commands/validate/links/corpus.js +92 -0
  232. package/dist/commands/validate/links/corpus.js.map +1 -0
  233. package/dist/commands/validate/links/glossary.js +75 -0
  234. package/dist/commands/validate/links/glossary.js.map +1 -0
  235. package/dist/commands/validate/report.js +269 -0
  236. package/dist/commands/validate/report.js.map +1 -0
  237. package/dist/commands/validate/service/api.js +338 -0
  238. package/dist/commands/validate/service/api.js.map +1 -0
  239. package/dist/commands/validate/service/events/events.js +219 -0
  240. package/dist/commands/validate/service/events/events.js.map +1 -0
  241. package/dist/commands/validate/service/events/message-producers.js +77 -0
  242. package/dist/commands/validate/service/events/message-producers.js.map +1 -0
  243. package/dist/commands/validate/service/evidence-pins.js +131 -0
  244. package/dist/commands/validate/service/evidence-pins.js.map +1 -0
  245. package/dist/commands/validate/service/service.js +251 -0
  246. package/dist/commands/validate/service/service.js.map +1 -0
  247. package/dist/commands/validate/service/specs.js +251 -0
  248. package/dist/commands/validate/service/specs.js.map +1 -0
  249. package/dist/commands/validate/service/spine.js +113 -0
  250. package/dist/commands/validate/service/spine.js.map +1 -0
  251. package/dist/commands/validate/validate.js +338 -0
  252. package/dist/commands/validate/validate.js.map +1 -0
  253. package/dist/commands/verify/cross/diff.js +182 -0
  254. package/dist/commands/verify/cross/diff.js.map +1 -0
  255. package/dist/commands/verify/evidence/contract.js +32 -0
  256. package/dist/commands/verify/evidence/contract.js.map +1 -0
  257. package/dist/commands/verify/evidence/pins.js +52 -0
  258. package/dist/commands/verify/evidence/pins.js.map +1 -0
  259. package/dist/commands/verify/evidence/read.js +72 -0
  260. package/dist/commands/verify/evidence/read.js.map +1 -0
  261. package/dist/commands/verify/frozen.js +241 -0
  262. package/dist/commands/verify/frozen.js.map +1 -0
  263. package/dist/commands/verify/record.js +226 -0
  264. package/dist/commands/verify/record.js.map +1 -0
  265. package/dist/commands/verify/report.js +223 -0
  266. package/dist/commands/verify/report.js.map +1 -0
  267. package/dist/commands/verify/results.js +246 -0
  268. package/dist/commands/verify/results.js.map +1 -0
  269. package/dist/commands/verify/verify.js +217 -0
  270. package/dist/commands/verify/verify.js.map +1 -0
  271. package/dist/commands/vouch/contract.js +11 -0
  272. package/dist/commands/vouch/contract.js.map +1 -0
  273. package/dist/commands/vouch/pack/contract.js +2 -0
  274. package/dist/commands/vouch/pack/contract.js.map +1 -0
  275. package/dist/commands/vouch/pack/pack.js +266 -0
  276. package/dist/commands/vouch/pack/pack.js.map +1 -0
  277. package/dist/commands/vouch/pack/print.js +271 -0
  278. package/dist/commands/vouch/pack/print.js.map +1 -0
  279. package/dist/commands/vouch/pack/sections.js +142 -0
  280. package/dist/commands/vouch/pack/sections.js.map +1 -0
  281. package/dist/commands/vouch/prompt/confirm.js +46 -0
  282. package/dist/commands/vouch/prompt/confirm.js.map +1 -0
  283. package/dist/commands/vouch/report.js +101 -0
  284. package/dist/commands/vouch/report.js.map +1 -0
  285. package/dist/commands/vouch/run.js +285 -0
  286. package/dist/commands/vouch/run.js.map +1 -0
  287. package/dist/commands/vouch/sample/plan.js +125 -0
  288. package/dist/commands/vouch/sample/plan.js.map +1 -0
  289. package/dist/commands/vouch/sample/print.js +109 -0
  290. package/dist/commands/vouch/sample/print.js.map +1 -0
  291. package/dist/commands/vouch/vet/verify.js +123 -0
  292. package/dist/commands/vouch/vet/verify.js.map +1 -0
  293. package/dist/commands/vouch/vouch.js +176 -0
  294. package/dist/commands/vouch/vouch.js.map +1 -0
  295. package/dist/core/agent/agents-md/artifacts.js +220 -0
  296. package/dist/core/agent/agents-md/artifacts.js.map +1 -0
  297. package/dist/core/agent/agents-md/command-map.js +324 -0
  298. package/dist/core/agent/agents-md/command-map.js.map +1 -0
  299. package/dist/core/agent/agents-md/cycle.js +165 -0
  300. package/dist/core/agent/agents-md/cycle.js.map +1 -0
  301. package/dist/core/agent/agents-md/map/explain.js +34 -0
  302. package/dist/core/agent/agents-md/map/explain.js.map +1 -0
  303. package/dist/core/agent/agents-md/map/lenses/context.js +41 -0
  304. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -0
  305. package/dist/core/agent/agents-md/map/lenses/diff.js +46 -0
  306. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -0
  307. package/dist/core/agent/agents-md/map/lenses/gate.js +57 -0
  308. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -0
  309. package/dist/core/agent/agents-md/map/mcp.js +41 -0
  310. package/dist/core/agent/agents-md/map/mcp.js.map +1 -0
  311. package/dist/core/agent/agents-md/map/status.js +62 -0
  312. package/dist/core/agent/agents-md/map/status.js.map +1 -0
  313. package/dist/core/agent/agents-md/map/subsystem.js +32 -0
  314. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -0
  315. package/dist/core/agent/agents-md/map/usecases.js +60 -0
  316. package/dist/core/agent/agents-md/map/usecases.js.map +1 -0
  317. package/dist/core/agent/agents-md/refusals.js +246 -0
  318. package/dist/core/agent/agents-md/refusals.js.map +1 -0
  319. package/dist/core/agent/agents-md/shipped/archive-gate.js +98 -0
  320. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +1 -0
  321. package/dist/core/agent/agents-md/spine.js +40 -0
  322. package/dist/core/agent/agents-md/spine.js.map +1 -0
  323. package/dist/core/agent/agents-md.js +170 -0
  324. package/dist/core/agent/agents-md.js.map +1 -0
  325. package/dist/core/{agents-stamp.js → agent/agents-stamp.js} +50 -0
  326. package/dist/core/agent/agents-stamp.js.map +1 -0
  327. package/dist/core/agent/contract.js +2 -0
  328. package/dist/core/agent/contract.js.map +1 -0
  329. package/dist/core/agent/protocol.js +193 -0
  330. package/dist/core/agent/protocol.js.map +1 -0
  331. package/dist/core/agent/scaffold.js +259 -0
  332. package/dist/core/agent/scaffold.js.map +1 -0
  333. package/dist/core/agent/support/report.js +103 -0
  334. package/dist/core/agent/support/report.js.map +1 -0
  335. package/dist/core/agent/tools/dialects.js +150 -0
  336. package/dist/core/agent/tools/dialects.js.map +1 -0
  337. package/dist/core/agent/tools/registry.js +228 -0
  338. package/dist/core/agent/tools/registry.js.map +1 -0
  339. package/dist/core/agent/workflows/adopt.js +133 -0
  340. package/dist/core/agent/workflows/adopt.js.map +1 -0
  341. package/dist/core/agent/workflows/check.js +337 -0
  342. package/dist/core/agent/workflows/check.js.map +1 -0
  343. package/dist/core/agent/workflows/closing.js +262 -0
  344. package/dist/core/agent/workflows/closing.js.map +1 -0
  345. package/dist/core/agent/workflows/feature.js +105 -0
  346. package/dist/core/agent/workflows/feature.js.map +1 -0
  347. package/dist/core/agent/workflows/implement.js +105 -0
  348. package/dist/core/agent/workflows/implement.js.map +1 -0
  349. package/dist/core/agent/workflows/reference/authoring.js +209 -0
  350. package/dist/core/agent/workflows/reference/authoring.js.map +1 -0
  351. package/dist/core/agent/workflows/reference/codes.js +65 -0
  352. package/dist/core/agent/workflows/reference/codes.js.map +1 -0
  353. package/dist/core/agent/workflows/reference/done-check.js +188 -0
  354. package/dist/core/agent/workflows/reference/done-check.js.map +1 -0
  355. package/dist/core/agent/workflows/reference/reference.js +16 -0
  356. package/dist/core/agent/workflows/reference/reference.js.map +1 -0
  357. package/dist/core/agent/workflows/reference/spine.js +297 -0
  358. package/dist/core/agent/workflows/reference/spine.js.map +1 -0
  359. package/dist/core/asyncapi/baseline/plan.js +110 -0
  360. package/dist/core/asyncapi/baseline/plan.js.map +1 -0
  361. package/dist/core/asyncapi/baseline/surfaces.js +54 -0
  362. package/dist/core/asyncapi/baseline/surfaces.js.map +1 -0
  363. package/dist/core/asyncapi/depth.js +164 -0
  364. package/dist/core/asyncapi/depth.js.map +1 -0
  365. package/dist/core/asyncapi/digest.js +141 -0
  366. package/dist/core/asyncapi/digest.js.map +1 -0
  367. package/dist/core/asyncapi/merge/components.js +91 -0
  368. package/dist/core/asyncapi/merge/components.js.map +1 -0
  369. package/dist/core/asyncapi/merge/error.js +33 -0
  370. package/dist/core/asyncapi/merge/error.js.map +1 -0
  371. package/dist/core/asyncapi/merge/markers.js +308 -0
  372. package/dist/core/asyncapi/merge/markers.js.map +1 -0
  373. package/dist/core/asyncapi/merge/merge.js +215 -0
  374. package/dist/core/asyncapi/merge/merge.js.map +1 -0
  375. package/dist/core/asyncapi/merge/pin.js +107 -0
  376. package/dist/core/asyncapi/merge/pin.js.map +1 -0
  377. package/dist/core/asyncapi/model.js +11 -0
  378. package/dist/core/asyncapi/model.js.map +1 -0
  379. package/dist/core/asyncapi/producers.js +50 -0
  380. package/dist/core/asyncapi/producers.js.map +1 -0
  381. package/dist/core/{asyncapi.js → asyncapi/read.js} +95 -109
  382. package/dist/core/asyncapi/read.js.map +1 -0
  383. package/dist/core/brief/brief.js +225 -0
  384. package/dist/core/brief/brief.js.map +1 -0
  385. package/dist/core/brief/checks.js +250 -0
  386. package/dist/core/brief/checks.js.map +1 -0
  387. package/dist/core/brief/landscape.js +194 -0
  388. package/dist/core/brief/landscape.js.map +1 -0
  389. package/dist/core/brief/targets.js +226 -0
  390. package/dist/core/brief/targets.js.map +1 -0
  391. package/dist/core/brief/unchecked.js +48 -0
  392. package/dist/core/brief/unchecked.js.map +1 -0
  393. package/dist/core/c4/arch.js +174 -0
  394. package/dist/core/c4/arch.js.map +1 -0
  395. package/dist/core/c4/likec4.js +90 -0
  396. package/dist/core/c4/likec4.js.map +1 -0
  397. package/dist/core/c4/parsed/deployment.js +158 -0
  398. package/dist/core/c4/parsed/deployment.js.map +1 -0
  399. package/dist/core/c4/parsed/dynamic-views.js +132 -0
  400. package/dist/core/c4/parsed/dynamic-views.js.map +1 -0
  401. package/dist/core/c4/parsed/specification.js +78 -0
  402. package/dist/core/c4/parsed/specification.js.map +1 -0
  403. package/dist/core/c4/parsed/values.js +57 -0
  404. package/dist/core/c4/parsed/values.js.map +1 -0
  405. package/dist/core/c4/parsed/view-ids.js +75 -0
  406. package/dist/core/c4/parsed/view-ids.js.map +1 -0
  407. package/dist/core/c4/project/architecture.js +45 -0
  408. package/dist/core/c4/project/architecture.js.map +1 -0
  409. package/dist/core/c4/project/documents.js +58 -0
  410. package/dist/core/c4/project/documents.js.map +1 -0
  411. package/dist/core/c4/project/load.js +187 -0
  412. package/dist/core/c4/project/load.js.map +1 -0
  413. package/dist/core/c4/project/staged.js +220 -0
  414. package/dist/core/c4/project/staged.js.map +1 -0
  415. package/dist/core/c4/resolve/service.js +159 -0
  416. package/dist/core/c4/resolve/service.js.map +1 -0
  417. package/dist/core/c4/resolve/steps.js +144 -0
  418. package/dist/core/c4/resolve/steps.js.map +1 -0
  419. package/dist/core/c4/seed/fleet-file.js +201 -0
  420. package/dist/core/c4/seed/fleet-file.js.map +1 -0
  421. package/dist/core/c4/seed/items.js +91 -0
  422. package/dist/core/c4/seed/items.js.map +1 -0
  423. package/dist/core/c4/seed/stamp.js +48 -0
  424. package/dist/core/c4/seed/stamp.js.map +1 -0
  425. package/dist/core/c4/seed/template.js +109 -0
  426. package/dist/core/c4/seed/template.js.map +1 -0
  427. package/dist/core/c4/source-mask.js +82 -0
  428. package/dist/core/c4/source-mask.js.map +1 -0
  429. package/dist/core/c4/source-scan.js +227 -0
  430. package/dist/core/c4/source-scan.js.map +1 -0
  431. package/dist/core/c4/splice/authored-source.js +101 -0
  432. package/dist/core/c4/splice/authored-source.js.map +1 -0
  433. package/dist/core/c4/splice/contract.js +10 -0
  434. package/dist/core/c4/splice/contract.js.map +1 -0
  435. package/dist/core/c4/splice/delta-blocks.js +139 -0
  436. package/dist/core/c4/splice/delta-blocks.js.map +1 -0
  437. package/dist/core/c4/splice/identity/edges.js +36 -0
  438. package/dist/core/c4/splice/identity/edges.js.map +1 -0
  439. package/dist/core/c4/splice/landscape-merge.js +269 -0
  440. package/dist/core/c4/splice/landscape-merge.js.map +1 -0
  441. package/dist/core/c4/splice/placement.js +194 -0
  442. package/dist/core/c4/splice/placement.js.map +1 -0
  443. package/dist/core/c4/workspace.js +174 -0
  444. package/dist/core/c4/workspace.js.map +1 -0
  445. package/dist/core/capabilities/authoring/notes.js +141 -0
  446. package/dist/core/capabilities/authoring/notes.js.map +1 -0
  447. package/dist/core/capabilities/capabilities.js +124 -0
  448. package/dist/core/capabilities/capabilities.js.map +1 -0
  449. package/dist/core/capabilities/delta/doc.js +50 -0
  450. package/dist/core/capabilities/delta/doc.js.map +1 -0
  451. package/dist/core/capabilities/delta/overlay.js +129 -0
  452. package/dist/core/capabilities/delta/overlay.js.map +1 -0
  453. package/dist/core/capabilities/delta/summary.js +57 -0
  454. package/dist/core/capabilities/delta/summary.js.map +1 -0
  455. package/dist/core/capabilities/delta/tree.js +43 -0
  456. package/dist/core/capabilities/delta/tree.js.map +1 -0
  457. package/dist/core/capabilities/delta/uncovered.js +222 -0
  458. package/dist/core/capabilities/delta/uncovered.js.map +1 -0
  459. package/dist/core/capabilities/findings.js +342 -0
  460. package/dist/core/capabilities/findings.js.map +1 -0
  461. package/dist/core/capabilities/realizes/findings.js +181 -0
  462. package/dist/core/capabilities/realizes/findings.js.map +1 -0
  463. package/dist/core/capabilities/realizes/join.js +143 -0
  464. package/dist/core/capabilities/realizes/join.js.map +1 -0
  465. package/dist/core/capabilities/rollup.js +199 -0
  466. package/dist/core/capabilities/rollup.js.map +1 -0
  467. package/dist/core/capabilities/tree.js +126 -0
  468. package/dist/core/capabilities/tree.js.map +1 -0
  469. package/dist/core/capabilities/usecase-join.js +277 -0
  470. package/dist/core/capabilities/usecase-join.js.map +1 -0
  471. package/dist/core/coherence/authoring/scaffold.js +177 -0
  472. package/dist/core/coherence/authoring/scaffold.js.map +1 -0
  473. package/dist/core/coherence/authoring/sentinels.js +74 -0
  474. package/dist/core/coherence/authoring/sentinels.js.map +1 -0
  475. package/dist/core/coherence/coherence.js +287 -0
  476. package/dist/core/coherence/coherence.js.map +1 -0
  477. package/dist/core/coherence/declared.js +269 -0
  478. package/dist/core/coherence/declared.js.map +1 -0
  479. package/dist/core/coherence/events/declared.js +206 -0
  480. package/dist/core/coherence/events/declared.js.map +1 -0
  481. package/dist/core/coherence/events/events.js +204 -0
  482. package/dist/core/coherence/events/events.js.map +1 -0
  483. package/dist/core/coherence/events/grades.js +166 -0
  484. package/dist/core/coherence/events/grades.js.map +1 -0
  485. package/dist/core/coherence/events/lookups.js +122 -0
  486. package/dist/core/coherence/events/lookups.js.map +1 -0
  487. package/dist/core/coherence/events/merged.js +40 -0
  488. package/dist/core/coherence/events/merged.js.map +1 -0
  489. package/dist/core/coherence/living.js +198 -0
  490. package/dist/core/coherence/living.js.map +1 -0
  491. package/dist/core/coherence/lookups.js +155 -0
  492. package/dist/core/coherence/lookups.js.map +1 -0
  493. package/dist/core/coherence/pending.js +39 -0
  494. package/dist/core/coherence/pending.js.map +1 -0
  495. package/dist/core/conflict-markers.js +60 -0
  496. package/dist/core/conflict-markers.js.map +1 -0
  497. package/dist/core/delta/claims.js +73 -0
  498. package/dist/core/delta/claims.js.map +1 -0
  499. package/dist/core/delta/delta.js +296 -0
  500. package/dist/core/delta/delta.js.map +1 -0
  501. package/dist/core/delta/document.js +177 -0
  502. package/dist/core/delta/document.js.map +1 -0
  503. package/dist/core/delta/scope.js +48 -0
  504. package/dist/core/delta/scope.js.map +1 -0
  505. package/dist/core/delta/select.js +237 -0
  506. package/dist/core/delta/select.js.map +1 -0
  507. package/dist/core/dependencies/dependencies.js +141 -0
  508. package/dist/core/dependencies/dependencies.js.map +1 -0
  509. package/dist/core/dependencies/facts.js +165 -0
  510. package/dist/core/dependencies/facts.js.map +1 -0
  511. package/dist/core/dependencies/fanin.js +88 -0
  512. package/dist/core/dependencies/fanin.js.map +1 -0
  513. package/dist/core/dependencies/graph.js +106 -0
  514. package/dist/core/dependencies/graph.js.map +1 -0
  515. package/dist/core/deployment/delta.js +188 -0
  516. package/dist/core/deployment/delta.js.map +1 -0
  517. package/dist/core/deployment/objects.js +145 -0
  518. package/dist/core/deployment/objects.js.map +1 -0
  519. package/dist/core/diff/base-git.js +148 -0
  520. package/dist/core/diff/base-git.js.map +1 -0
  521. package/dist/core/diff/base-state.js +209 -0
  522. package/dist/core/diff/base-state.js.map +1 -0
  523. package/dist/core/diff/current-state.js +96 -0
  524. package/dist/core/diff/current-state.js.map +1 -0
  525. package/dist/core/diff/scope/changed-paths.js +188 -0
  526. package/dist/core/diff/scope/changed-paths.js.map +1 -0
  527. package/dist/core/diff/semantic.js +243 -0
  528. package/dist/core/diff/semantic.js.map +1 -0
  529. package/dist/core/diff/victims.js +247 -0
  530. package/dist/core/diff/victims.js.map +1 -0
  531. package/dist/core/docs.js +7 -64
  532. package/dist/core/docs.js.map +1 -1
  533. package/dist/core/doctor/agents.js +222 -0
  534. package/dist/core/doctor/agents.js.map +1 -0
  535. package/dist/core/doctor/config.js +135 -0
  536. package/dist/core/doctor/config.js.map +1 -0
  537. package/dist/core/doctor/doctor.js +275 -0
  538. package/dist/core/doctor/doctor.js.map +1 -0
  539. package/dist/core/doctor/report.js +13 -0
  540. package/dist/core/doctor/report.js.map +1 -0
  541. package/dist/core/doctor/residue.js +110 -0
  542. package/dist/core/doctor/residue.js.map +1 -0
  543. package/dist/core/document/apply.js +77 -0
  544. package/dist/core/document/apply.js.map +1 -0
  545. package/dist/core/{frontmatter.js → document/frontmatter.js} +48 -2
  546. package/dist/core/document/frontmatter.js.map +1 -0
  547. package/dist/core/document/grammar/directives.js +143 -0
  548. package/dist/core/document/grammar/directives.js.map +1 -0
  549. package/dist/core/document/parse.js +263 -0
  550. package/dist/core/document/parse.js.map +1 -0
  551. package/dist/core/document/scenarios.js +221 -0
  552. package/dist/core/document/scenarios.js.map +1 -0
  553. package/dist/core/document/spec.js +241 -0
  554. package/dist/core/document/spec.js.map +1 -0
  555. package/dist/core/{config.js → envelope/config.js} +79 -18
  556. package/dist/core/envelope/config.js.map +1 -0
  557. package/dist/core/envelope/json.js +175 -0
  558. package/dist/core/envelope/json.js.map +1 -0
  559. package/dist/core/{version.js → envelope/version.js} +3 -3
  560. package/dist/core/envelope/version.js.map +1 -0
  561. package/dist/core/explain/families.js +175 -0
  562. package/dist/core/explain/families.js.map +1 -0
  563. package/dist/core/explain/fix-tables.js +178 -0
  564. package/dist/core/explain/fix-tables.js.map +1 -0
  565. package/dist/core/explain/lookup.js +181 -0
  566. package/dist/core/explain/lookup.js.map +1 -0
  567. package/dist/core/explain/refusals.js +49 -0
  568. package/dist/core/explain/refusals.js.map +1 -0
  569. package/dist/core/explain/terms.js +144 -0
  570. package/dist/core/explain/terms.js.map +1 -0
  571. package/dist/core/explore/describe.js +105 -0
  572. package/dist/core/explore/describe.js.map +1 -0
  573. package/dist/core/{explore.js → explore/explore.js} +50 -101
  574. package/dist/core/explore/explore.js.map +1 -0
  575. package/dist/core/fleet-context.js +120 -78
  576. package/dist/core/fleet-context.js.map +1 -1
  577. package/dist/core/gate/checks.js +201 -0
  578. package/dist/core/gate/checks.js.map +1 -0
  579. package/dist/core/gate/partners.js +205 -0
  580. package/dist/core/gate/partners.js.map +1 -0
  581. package/dist/core/gate/report.js +47 -0
  582. package/dist/core/gate/report.js.map +1 -0
  583. package/dist/core/gate/verification.js +113 -0
  584. package/dist/core/gate/verification.js.map +1 -0
  585. package/dist/core/gherkin/digest.js +60 -0
  586. package/dist/core/gherkin/digest.js.map +1 -0
  587. package/dist/core/gherkin/emit.js +177 -0
  588. package/dist/core/gherkin/emit.js.map +1 -0
  589. package/dist/core/gherkin/read.js +76 -0
  590. package/dist/core/gherkin/read.js.map +1 -0
  591. package/dist/core/gherkin/stale.js +234 -0
  592. package/dist/core/gherkin/stale.js.map +1 -0
  593. package/dist/core/gherkin/stamp.js +77 -0
  594. package/dist/core/gherkin/stamp.js.map +1 -0
  595. package/dist/core/gherkin/steps/catalogue.js +154 -0
  596. package/dist/core/gherkin/steps/catalogue.js.map +1 -0
  597. package/dist/core/gherkin/steps/inventory.js +98 -0
  598. package/dist/core/gherkin/steps/inventory.js.map +1 -0
  599. package/dist/core/gherkin/steps/phrase.js +72 -0
  600. package/dist/core/gherkin/steps/phrase.js.map +1 -0
  601. package/dist/core/glossary/delta.js +106 -0
  602. package/dist/core/glossary/delta.js.map +1 -0
  603. package/dist/core/glossary/tree.js +86 -0
  604. package/dist/core/glossary/tree.js.map +1 -0
  605. package/dist/core/kernel/concurrency.js.map +1 -0
  606. package/dist/core/{document-bytes.js → kernel/document-bytes.js} +1 -1
  607. package/dist/core/kernel/document-bytes.js.map +1 -0
  608. package/dist/core/kernel/fences.js +42 -0
  609. package/dist/core/kernel/fences.js.map +1 -0
  610. package/dist/core/kernel/ids/capability.js +72 -0
  611. package/dist/core/kernel/ids/capability.js.map +1 -0
  612. package/dist/core/kernel/ids/dirs.js +33 -0
  613. package/dist/core/kernel/ids/dirs.js.map +1 -0
  614. package/dist/core/kernel/ids/feature.js +75 -0
  615. package/dist/core/kernel/ids/feature.js.map +1 -0
  616. package/dist/core/kernel/ids/service.js +156 -0
  617. package/dist/core/kernel/ids/service.js.map +1 -0
  618. package/dist/core/kernel/ids/subsystem.js +63 -0
  619. package/dist/core/kernel/ids/subsystem.js.map +1 -0
  620. package/dist/core/{path-safety.js → kernel/path-safety.js} +18 -4
  621. package/dist/core/kernel/path-safety.js.map +1 -0
  622. package/dist/core/kernel/records.js.map +1 -0
  623. package/dist/core/links/backlinks.js +68 -0
  624. package/dist/core/links/backlinks.js.map +1 -0
  625. package/dist/core/links/case.js +74 -0
  626. package/dist/core/links/case.js.map +1 -0
  627. package/dist/core/links/corpus.js +104 -0
  628. package/dist/core/links/corpus.js.map +1 -0
  629. package/dist/core/links/findings.js +110 -0
  630. package/dist/core/links/findings.js.map +1 -0
  631. package/dist/core/links/parse.js +236 -0
  632. package/dist/core/links/parse.js.map +1 -0
  633. package/dist/core/mcp/argv.js +111 -0
  634. package/dist/core/mcp/argv.js.map +1 -0
  635. package/dist/core/mcp/author-tools.js +72 -0
  636. package/dist/core/mcp/author-tools.js.map +1 -0
  637. package/dist/core/mcp/framing.js +46 -0
  638. package/dist/core/mcp/framing.js.map +1 -0
  639. package/dist/core/mcp/protocol.js +280 -0
  640. package/dist/core/mcp/protocol.js.map +1 -0
  641. package/dist/core/mcp/tools.js +314 -0
  642. package/dist/core/mcp/tools.js.map +1 -0
  643. package/dist/core/obligations/obligations.js +106 -0
  644. package/dist/core/obligations/obligations.js.map +1 -0
  645. package/dist/core/openapi/baseline/gate.js +166 -0
  646. package/dist/core/openapi/baseline/gate.js.map +1 -0
  647. package/dist/core/openapi/baseline/plan.js +112 -0
  648. package/dist/core/openapi/baseline/plan.js.map +1 -0
  649. package/dist/core/openapi/baseline/record.js +175 -0
  650. package/dist/core/openapi/baseline/record.js.map +1 -0
  651. package/dist/core/openapi/depth.js +122 -0
  652. package/dist/core/openapi/depth.js.map +1 -0
  653. package/dist/core/openapi/digest.js +189 -0
  654. package/dist/core/openapi/digest.js.map +1 -0
  655. package/dist/core/{openapi.js → openapi/doc.js} +44 -106
  656. package/dist/core/openapi/doc.js.map +1 -0
  657. package/dist/core/openapi/merge/components.js +234 -0
  658. package/dist/core/openapi/merge/components.js.map +1 -0
  659. package/dist/core/openapi/merge/error.js +26 -0
  660. package/dist/core/openapi/merge/error.js.map +1 -0
  661. package/dist/core/openapi/merge/markers.js +208 -0
  662. package/dist/core/openapi/merge/markers.js.map +1 -0
  663. package/dist/core/openapi/merge/merge.js +256 -0
  664. package/dist/core/openapi/merge/merge.js.map +1 -0
  665. package/dist/core/openapi/merge/pin.js +128 -0
  666. package/dist/core/openapi/merge/pin.js.map +1 -0
  667. package/dist/core/openspec/decide/capabilities.js +244 -0
  668. package/dist/core/openspec/decide/capabilities.js.map +1 -0
  669. package/dist/core/openspec/decide/dispositions.js +63 -0
  670. package/dist/core/openspec/decide/dispositions.js.map +1 -0
  671. package/dist/core/openspec/decide/renames.js +179 -0
  672. package/dist/core/openspec/decide/renames.js.map +1 -0
  673. package/dist/core/openspec/decide/verdict.js +84 -0
  674. package/dist/core/openspec/decide/verdict.js.map +1 -0
  675. package/dist/core/openspec/inventory.js +221 -0
  676. package/dist/core/openspec/inventory.js.map +1 -0
  677. package/dist/core/openspec/model/mapping.js +76 -0
  678. package/dist/core/openspec/model/mapping.js.map +1 -0
  679. package/dist/core/openspec/model/model.js +65 -0
  680. package/dist/core/openspec/model/model.js.map +1 -0
  681. package/dist/core/openspec/scan/changes.js +235 -0
  682. package/dist/core/openspec/scan/changes.js.map +1 -0
  683. package/dist/core/openspec/scan/config.js +200 -0
  684. package/dist/core/openspec/scan/config.js.map +1 -0
  685. package/dist/core/openspec/scan/shape.js +260 -0
  686. package/dist/core/openspec/scan/shape.js.map +1 -0
  687. package/dist/core/openspec/scan/walk.js +133 -0
  688. package/dist/core/openspec/scan/walk.js.map +1 -0
  689. package/dist/core/openspec/scan/workspace.js +71 -0
  690. package/dist/core/openspec/scan/workspace.js.map +1 -0
  691. package/dist/core/owners/codeowners.js +141 -0
  692. package/dist/core/owners/codeowners.js.map +1 -0
  693. package/dist/core/pack/features.js +120 -0
  694. package/dist/core/pack/features.js.map +1 -0
  695. package/dist/core/pack/joins.js +112 -0
  696. package/dist/core/pack/joins.js.map +1 -0
  697. package/dist/core/pack/living.js +137 -0
  698. package/dist/core/pack/living.js.map +1 -0
  699. package/dist/core/pack/pack.js +76 -0
  700. package/dist/core/pack/pack.js.map +1 -0
  701. package/dist/core/permissions/permissions.js +87 -0
  702. package/dist/core/permissions/permissions.js.map +1 -0
  703. package/dist/core/projection/api.js +84 -0
  704. package/dist/core/projection/api.js.map +1 -0
  705. package/dist/core/projection/arch-slice.js +63 -0
  706. package/dist/core/projection/arch-slice.js.map +1 -0
  707. package/dist/core/projection/events.js +34 -0
  708. package/dist/core/projection/events.js.map +1 -0
  709. package/dist/core/provenance/findings.js +208 -0
  710. package/dist/core/provenance/findings.js.map +1 -0
  711. package/dist/core/provenance/git.js +225 -0
  712. package/dist/core/provenance/git.js.map +1 -0
  713. package/dist/core/provenance/gitq/moves.js +182 -0
  714. package/dist/core/provenance/gitq/moves.js.map +1 -0
  715. package/dist/core/provenance/gitq/vouched-ancestor.js +179 -0
  716. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -0
  717. package/dist/core/provenance/sample/scope.js +238 -0
  718. package/dist/core/provenance/sample/scope.js.map +1 -0
  719. package/dist/core/provenance/sample/sections.js +61 -0
  720. package/dist/core/provenance/sample/sections.js.map +1 -0
  721. package/dist/core/provenance/sources.js +263 -0
  722. package/dist/core/provenance/sources.js.map +1 -0
  723. package/dist/core/provenance/stamp.js +232 -0
  724. package/dist/core/provenance/stamp.js.map +1 -0
  725. package/dist/core/provenance/walk.js +157 -0
  726. package/dist/core/provenance/walk.js.map +1 -0
  727. package/dist/core/repo/authored/paths.js +210 -0
  728. package/dist/core/repo/authored/paths.js.map +1 -0
  729. package/dist/core/repo/entries.js +126 -0
  730. package/dist/core/repo/entries.js.map +1 -0
  731. package/dist/core/repo/paths.js +277 -0
  732. package/dist/core/repo/paths.js.map +1 -0
  733. package/dist/core/repo/repo.js +248 -0
  734. package/dist/core/repo/repo.js.map +1 -0
  735. package/dist/core/repo/service-target.js +153 -0
  736. package/dist/core/repo/service-target.js.map +1 -0
  737. package/dist/core/repo/state.js +58 -0
  738. package/dist/core/repo/state.js.map +1 -0
  739. package/dist/core/repo/tree/find.js +52 -0
  740. package/dist/core/repo/tree/find.js.map +1 -0
  741. package/dist/core/repo/tree/fs.js +64 -0
  742. package/dist/core/repo/tree/fs.js.map +1 -0
  743. package/dist/core/repo/tree/marker.js +61 -0
  744. package/dist/core/repo/tree/marker.js.map +1 -0
  745. package/dist/core/repo/tree/views.js +172 -0
  746. package/dist/core/repo/tree/views.js.map +1 -0
  747. package/dist/core/repo/tree/walk.js +234 -0
  748. package/dist/core/repo/tree/walk.js.map +1 -0
  749. package/dist/core/results.js +99 -2
  750. package/dist/core/results.js.map +1 -1
  751. package/dist/core/scaffold/landscape.js +131 -0
  752. package/dist/core/scaffold/landscape.js.map +1 -0
  753. package/dist/core/scaffold/readme.js +84 -0
  754. package/dist/core/scaffold/readme.js.map +1 -0
  755. package/dist/core/staging/commit.js +241 -0
  756. package/dist/core/staging/commit.js.map +1 -0
  757. package/dist/core/staging/interrupted.js +96 -0
  758. package/dist/core/staging/interrupted.js.map +1 -0
  759. package/dist/core/staging/lock.js +206 -0
  760. package/dist/core/staging/lock.js.map +1 -0
  761. package/dist/core/staging/recovery/finding.js +35 -0
  762. package/dist/core/staging/recovery/finding.js.map +1 -0
  763. package/dist/core/staging/recovery/intent.js +98 -0
  764. package/dist/core/staging/recovery/intent.js.map +1 -0
  765. package/dist/core/staging/recovery/recover.js +126 -0
  766. package/dist/core/staging/recovery/recover.js.map +1 -0
  767. package/dist/core/staging/recovery/residue.js +62 -0
  768. package/dist/core/staging/recovery/residue.js.map +1 -0
  769. package/dist/core/staging/snapshot.js +202 -0
  770. package/dist/core/staging/snapshot.js.map +1 -0
  771. package/dist/core/staging/txn/forward.js +171 -0
  772. package/dist/core/staging/txn/forward.js.map +1 -0
  773. package/dist/core/staging/txn/journal.js +139 -0
  774. package/dist/core/staging/txn/journal.js.map +1 -0
  775. package/dist/core/staging/txn/transaction.js +75 -0
  776. package/dist/core/staging/txn/transaction.js.map +1 -0
  777. package/dist/core/staging/writes.js +113 -0
  778. package/dist/core/staging/writes.js.map +1 -0
  779. package/dist/core/status/actions/execution.js +49 -0
  780. package/dist/core/status/actions/execution.js.map +1 -0
  781. package/dist/core/status/contracts.js +102 -0
  782. package/dist/core/status/contracts.js.map +1 -0
  783. package/dist/core/status/feature/artifacts.js +196 -0
  784. package/dist/core/status/feature/artifacts.js.map +1 -0
  785. package/dist/core/status/feature/feature.js +207 -0
  786. package/dist/core/status/feature/feature.js.map +1 -0
  787. package/dist/core/status/feature/next.js +150 -0
  788. package/dist/core/status/feature/next.js.map +1 -0
  789. package/dist/core/status/feature/state.js +21 -0
  790. package/dist/core/status/feature/state.js.map +1 -0
  791. package/dist/core/status/fleet/fleet.js +197 -0
  792. package/dist/core/status/fleet/fleet.js.map +1 -0
  793. package/dist/core/status/fleet/next.js +207 -0
  794. package/dist/core/status/fleet/next.js.map +1 -0
  795. package/dist/core/status/interrupted.js +97 -0
  796. package/dist/core/status/interrupted.js.map +1 -0
  797. package/dist/core/status/report.js +31 -0
  798. package/dist/core/status/report.js.map +1 -0
  799. package/dist/core/status/scan.js +41 -0
  800. package/dist/core/status/scan.js.map +1 -0
  801. package/dist/core/status/verification.js +201 -0
  802. package/dist/core/status/verification.js.map +1 -0
  803. package/dist/core/usecases/capability.js +169 -0
  804. package/dist/core/usecases/capability.js.map +1 -0
  805. package/dist/core/usecases/delta/claims.js +116 -0
  806. package/dist/core/usecases/delta/claims.js.map +1 -0
  807. package/dist/core/usecases/delta/flows.js +103 -0
  808. package/dist/core/usecases/delta/flows.js.map +1 -0
  809. package/dist/core/usecases/delta/overlay.js +72 -0
  810. package/dist/core/usecases/delta/overlay.js.map +1 -0
  811. package/dist/core/usecases/fleet.js +137 -0
  812. package/dist/core/usecases/fleet.js.map +1 -0
  813. package/dist/core/usecases/operations.js +111 -0
  814. package/dist/core/usecases/operations.js.map +1 -0
  815. package/dist/core/usecases/place.js +44 -0
  816. package/dist/core/usecases/place.js.map +1 -0
  817. package/dist/core/usecases/touch.js +101 -0
  818. package/dist/core/usecases/touch.js.map +1 -0
  819. package/dist/core/verify/answers.js +173 -0
  820. package/dist/core/verify/answers.js.map +1 -0
  821. package/dist/core/verify/build.js +150 -0
  822. package/dist/core/verify/build.js.map +1 -0
  823. package/dist/core/verify/checklist.js +201 -0
  824. package/dist/core/verify/checklist.js.map +1 -0
  825. package/dist/core/verify/claims/identity.js +61 -0
  826. package/dist/core/verify/claims/identity.js.map +1 -0
  827. package/dist/core/verify/cross/diff.js +97 -0
  828. package/dist/core/verify/cross/diff.js.map +1 -0
  829. package/dist/core/verify/evidence/contract.js +169 -0
  830. package/dist/core/verify/evidence/contract.js.map +1 -0
  831. package/dist/core/verify/file.js +257 -0
  832. package/dist/core/verify/file.js.map +1 -0
  833. package/dist/core/verify/pins/lint.js +157 -0
  834. package/dist/core/verify/pins/lint.js.map +1 -0
  835. package/dist/core/verify/pins/pin.js +95 -0
  836. package/dist/core/verify/pins/pin.js.map +1 -0
  837. package/dist/core/verify/record.js +142 -0
  838. package/dist/core/verify/record.js.map +1 -0
  839. package/dist/core/verify/store/commit.js +89 -0
  840. package/dist/core/verify/store/commit.js.map +1 -0
  841. package/dist/core/verify/store/render.js +57 -0
  842. package/dist/core/verify/store/render.js.map +1 -0
  843. package/dist/core/vocabulary/health.js +91 -0
  844. package/dist/core/vocabulary/health.js.map +1 -0
  845. package/dist/core/vocabulary/issue.js +58 -0
  846. package/dist/core/vocabulary/issue.js.map +1 -0
  847. package/dist/core/vocabulary/maturity.js +145 -0
  848. package/dist/core/vocabulary/maturity.js.map +1 -0
  849. package/dist/core/{report.js → vocabulary/report.js} +30 -2
  850. package/dist/core/vocabulary/report.js.map +1 -0
  851. package/dist/core/vocabulary/steps.js +266 -0
  852. package/dist/core/vocabulary/steps.js.map +1 -0
  853. package/dist/core/workspace/discover.js +239 -0
  854. package/dist/core/workspace/discover.js.map +1 -0
  855. package/dist/core/workspace/render.js +52 -0
  856. package/dist/core/workspace/render.js.map +1 -0
  857. package/examples/README.md +189 -0
  858. package/examples/docs/architecture/adrs/0001-transactional-outbox.md +33 -0
  859. package/examples/docs/architecture/capabilities.yaml +31 -0
  860. package/examples/docs/architecture/landscape.likec4 +280 -0
  861. package/examples/docs/architecture/obligations.yaml +23 -0
  862. package/examples/docs/architecture/permissions.yaml +50 -0
  863. package/examples/docs/architecture/subsystems.likec4 +11 -0
  864. package/examples/docs/architecture/usecases/order-notification.likec4 +65 -0
  865. package/examples/docs/capabilities/checkout/spec.md +55 -0
  866. package/examples/docs/capabilities/identity/tokens/spec.md +42 -0
  867. package/examples/docs/capabilities/order-notifications/spec.md +43 -0
  868. package/examples/docs/features/FEAT-101-payment-splitting/adrs/0001-dedicated-split-service.md +18 -0
  869. package/examples/docs/features/FEAT-101-payment-splitting/delta.likec4 +94 -0
  870. package/examples/docs/features/FEAT-101-payment-splitting/intent.md +44 -0
  871. package/examples/docs/features/FEAT-101-payment-splitting/specs/checkout-web/spec.md +21 -0
  872. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +129 -0
  873. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/spec.md +36 -0
  874. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/arch.spec.md +40 -0
  875. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +116 -0
  876. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/spec.md +55 -0
  877. package/examples/docs/features/FEAT-112-retire-order-v1/intent.md +35 -0
  878. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +28 -0
  879. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/spec.md +17 -0
  880. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/architecture/landscape.likec4 +253 -0
  881. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/order-service/spec.md +121 -0
  882. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/openapi.yaml +111 -0
  883. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/spec.md +58 -0
  884. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/manifest.json +32 -0
  885. package/examples/docs/features/archive/FEAT-088-refunds/adrs/0001-refunds-stay-in-payment-service.md +26 -0
  886. package/examples/docs/features/archive/FEAT-088-refunds/delta.likec4 +28 -0
  887. package/examples/docs/features/archive/FEAT-088-refunds/intent.md +26 -0
  888. package/examples/docs/features/archive/FEAT-088-refunds/specs/order-service/spec.md +37 -0
  889. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +70 -0
  890. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/spec.md +23 -0
  891. package/examples/docs/features/archive/FEAT-088-refunds/verification.yaml +76 -0
  892. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/architecture/landscape.likec4 +275 -0
  893. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/asyncapi.yaml +96 -0
  894. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/spec.md +82 -0
  895. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/asyncapi.yaml +164 -0
  896. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/spec.md +91 -0
  897. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/manifest.json +46 -0
  898. package/examples/docs/features/archive/FEAT-120-refund-notification/delta.likec4 +36 -0
  899. package/examples/docs/features/archive/FEAT-120-refund-notification/intent.md +22 -0
  900. package/examples/docs/features/archive/FEAT-120-refund-notification/scenario-report.json +10 -0
  901. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/asyncapi.yaml +208 -0
  902. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/spec.md +24 -0
  903. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/asyncapi.yaml +137 -0
  904. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/spec.md +31 -0
  905. package/examples/docs/features/archive/FEAT-120-refund-notification/verification.yaml +84 -0
  906. package/examples/docs/glossary/order.md +15 -0
  907. package/examples/docs/glossary/payments/authorization.md +14 -0
  908. package/examples/docs/likec4.config.json +9 -0
  909. package/examples/docs/loam.json +3 -0
  910. package/examples/docs/services/checkout-web/model.likec4 +109 -0
  911. package/examples/docs/services/checkout-web/spec.md +91 -0
  912. package/examples/docs/services/checkout-web/ui/pages/checkout.page.yaml +40 -0
  913. package/examples/docs/services/checkout-web/ui/pages/order-status.page.yaml +24 -0
  914. package/examples/docs/services/checkout-web/ui/pages/split-payment.page.yaml +26 -0
  915. package/examples/docs/services/order-service/adrs/0001-orchestrated-order-lifecycle.md +52 -0
  916. package/examples/docs/services/order-service/arch.spec.md +127 -0
  917. package/examples/docs/services/order-service/asyncapi.yaml +108 -0
  918. package/examples/docs/services/order-service/health.yaml +57 -0
  919. package/examples/docs/services/order-service/model.likec4 +138 -0
  920. package/examples/docs/services/order-service/openapi.yaml +257 -0
  921. package/examples/docs/services/order-service/runbook.md +62 -0
  922. package/examples/docs/services/order-service/spec.md +134 -0
  923. package/examples/docs/services/payment-service/adrs/0001-payments-outbox.md +18 -0
  924. package/examples/docs/services/payment-service/adrs/0002-idempotency-keys-on-authorization.md +27 -0
  925. package/examples/docs/services/payment-service/arch.spec.md +73 -0
  926. package/examples/docs/services/payment-service/asyncapi.yaml +130 -0
  927. package/examples/docs/services/payment-service/health.yaml +39 -0
  928. package/examples/docs/services/payment-service/model.likec4 +67 -0
  929. package/examples/docs/services/payment-service/openapi.yaml +151 -0
  930. package/examples/docs/services/payment-service/runbook.md +37 -0
  931. package/examples/docs/services/payment-service/spec.md +88 -0
  932. package/examples/docs/services/platform/identity-service/adrs/0001-token-introspection-over-local-validation.md +60 -0
  933. package/examples/docs/services/platform/identity-service/arch.spec.md +117 -0
  934. package/examples/docs/services/platform/identity-service/health.yaml +70 -0
  935. package/examples/docs/services/platform/identity-service/model.likec4 +83 -0
  936. package/examples/docs/services/platform/identity-service/openapi.yaml +197 -0
  937. package/examples/docs/services/platform/identity-service/runbook.md +95 -0
  938. package/examples/docs/services/platform/identity-service/spec.md +119 -0
  939. package/examples/docs/services/platform/notification-service/asyncapi.yaml +199 -0
  940. package/examples/docs/services/platform/notification-service/health.yaml +35 -0
  941. package/examples/docs/services/platform/notification-service/model.likec4 +118 -0
  942. package/examples/docs/services/platform/notification-service/runbook.md +48 -0
  943. package/examples/docs/services/platform/notification-service/spec.md +112 -0
  944. package/examples/docs/services/platform/subsystem.yaml +2 -0
  945. package/examples/fleet.yaml +57 -0
  946. package/package.json +19 -6
  947. package/dist/commands/adopt.js +0 -240
  948. package/dist/commands/adopt.js.map +0 -1
  949. package/dist/commands/archive.js +0 -1313
  950. package/dist/commands/archive.js.map +0 -1
  951. package/dist/commands/delta.js +0 -388
  952. package/dist/commands/delta.js.map +0 -1
  953. package/dist/commands/docs-repo-gate.js.map +0 -1
  954. package/dist/commands/format.js +0 -17
  955. package/dist/commands/format.js.map +0 -1
  956. package/dist/commands/gherkin.js +0 -309
  957. package/dist/commands/gherkin.js.map +0 -1
  958. package/dist/commands/init.js.map +0 -1
  959. package/dist/commands/list.js +0 -295
  960. package/dist/commands/list.js.map +0 -1
  961. package/dist/commands/migrate-openspec.js +0 -950
  962. package/dist/commands/migrate-openspec.js.map +0 -1
  963. package/dist/commands/new.js +0 -413
  964. package/dist/commands/new.js.map +0 -1
  965. package/dist/commands/rebase.js +0 -356
  966. package/dist/commands/rebase.js.map +0 -1
  967. package/dist/commands/show.js.map +0 -1
  968. package/dist/commands/status.js +0 -262
  969. package/dist/commands/status.js.map +0 -1
  970. package/dist/commands/unarchive.js.map +0 -1
  971. package/dist/commands/validate.js +0 -1773
  972. package/dist/commands/validate.js.map +0 -1
  973. package/dist/commands/verify.js +0 -775
  974. package/dist/commands/verify.js.map +0 -1
  975. package/dist/commands/vouch.js +0 -421
  976. package/dist/commands/vouch.js.map +0 -1
  977. package/dist/core/agent.js +0 -2173
  978. package/dist/core/agent.js.map +0 -1
  979. package/dist/core/agents-stamp.js.map +0 -1
  980. package/dist/core/arch.js +0 -130
  981. package/dist/core/arch.js.map +0 -1
  982. package/dist/core/asyncapi.js.map +0 -1
  983. package/dist/core/brief.js +0 -755
  984. package/dist/core/brief.js.map +0 -1
  985. package/dist/core/coherence.js +0 -577
  986. package/dist/core/coherence.js.map +0 -1
  987. package/dist/core/concurrency.js.map +0 -1
  988. package/dist/core/config.js.map +0 -1
  989. package/dist/core/delta.js +0 -476
  990. package/dist/core/delta.js.map +0 -1
  991. package/dist/core/dependencies.js +0 -356
  992. package/dist/core/dependencies.js.map +0 -1
  993. package/dist/core/doctor.js +0 -596
  994. package/dist/core/doctor.js.map +0 -1
  995. package/dist/core/document-bytes.js.map +0 -1
  996. package/dist/core/explore.js.map +0 -1
  997. package/dist/core/frontmatter.js.map +0 -1
  998. package/dist/core/gherkin.js +0 -463
  999. package/dist/core/gherkin.js.map +0 -1
  1000. package/dist/core/health.js +0 -67
  1001. package/dist/core/health.js.map +0 -1
  1002. package/dist/core/ids.js +0 -122
  1003. package/dist/core/ids.js.map +0 -1
  1004. package/dist/core/issue.js +0 -9
  1005. package/dist/core/issue.js.map +0 -1
  1006. package/dist/core/json.js +0 -74
  1007. package/dist/core/json.js.map +0 -1
  1008. package/dist/core/likec4.js +0 -489
  1009. package/dist/core/likec4.js.map +0 -1
  1010. package/dist/core/maturity.js +0 -66
  1011. package/dist/core/maturity.js.map +0 -1
  1012. package/dist/core/openapi-merge.js +0 -552
  1013. package/dist/core/openapi-merge.js.map +0 -1
  1014. package/dist/core/openapi.js.map +0 -1
  1015. package/dist/core/openspec-inventory.js +0 -1440
  1016. package/dist/core/openspec-inventory.js.map +0 -1
  1017. package/dist/core/path-safety.js.map +0 -1
  1018. package/dist/core/provenance.js +0 -787
  1019. package/dist/core/provenance.js.map +0 -1
  1020. package/dist/core/records.js.map +0 -1
  1021. package/dist/core/repo.js +0 -438
  1022. package/dist/core/repo.js.map +0 -1
  1023. package/dist/core/report.js.map +0 -1
  1024. package/dist/core/spec.js +0 -505
  1025. package/dist/core/spec.js.map +0 -1
  1026. package/dist/core/staging.js +0 -890
  1027. package/dist/core/staging.js.map +0 -1
  1028. package/dist/core/status.js +0 -1132
  1029. package/dist/core/status.js.map +0 -1
  1030. package/dist/core/steps.js +0 -72
  1031. package/dist/core/steps.js.map +0 -1
  1032. package/dist/core/verify.js +0 -807
  1033. package/dist/core/verify.js.map +0 -1
  1034. package/dist/core/version.js.map +0 -1
  1035. /package/dist/core/{concurrency.js → kernel/concurrency.js} +0 -0
  1036. /package/dist/core/{records.js → kernel/records.js} +0 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * The docs repo's advisory lock: take it, describe its holder, and break it
3
+ * when that holder is a pid on this host that no longer exists.
4
+ *
5
+ * A module of its own because it is the only rule here that spans the whole
6
+ * fleet rather than one file, and because the two readers of a held lock have
7
+ * to agree on what "stale" means — `breakStaleLock` acts on the answer,
8
+ * `lockIsStale` only reports it to `doctor`. They were written as twins and
9
+ * stay together so they cannot drift into two definitions of a dead holder.
10
+ */
11
+ import { link, readFile, unlink, writeFile } from "node:fs/promises";
12
+ import { hostname } from "node:os";
13
+ import { join } from "node:path";
14
+ // Every `isRecord` below guards a record loam itself wrote and is reading back
15
+ // — the lock, the snapshot manifest — whose fields are asked for on the very
16
+ // next line, inside a REFUSAL path. `JSON.parse` answers with any JSON value,
17
+ // not with an object, and that is where a thrown `TypeError` costs the most: it
18
+ // does not make the command fail safely, it replaces a designed refusal with
19
+ // `internal`.
20
+ import { isRecord } from "../kernel/records.js";
21
+ import { quietRm, tempPath } from "./commit.js";
22
+ /** The lock file, in the docs repo root — one writer at a time across the whole fleet. */
23
+ export const DOCS_LOCK = ".loam-lock";
24
+ /**
25
+ * How long a WAITING writer sits out a live holder before answering
26
+ * `docs-busy`. One spelling for every writer that waits — including gherkin,
27
+ * whose lock guards the SERVICE repo's emission root rather than the docs
28
+ * repo: long enough to cover another writer's sub-second commit window with a
29
+ * margin for a loaded host, bounded so a wedged lock cannot hang anyone.
30
+ */
31
+ export const LOCK_WAIT_MS = 5_000;
32
+ /** Another writer holds the docs repo. Commands map this to the `docs-busy` envelope code. */
33
+ export class DocsBusyError extends Error {
34
+ lockPath;
35
+ name = "DocsBusyError";
36
+ constructor(lockPath, holder) {
37
+ super(`another loam command is writing this docs repo (${holder}) — nothing was read or written. ` +
38
+ `Wait for it to finish, or remove ${lockPath} if no such command is running.`);
39
+ this.lockPath = lockPath;
40
+ }
41
+ }
42
+ /**
43
+ * Take the docs repo's advisory lock for the whole plan+commit window, and
44
+ * return the release.
45
+ *
46
+ * Advisory, and deliberately coarse: the destructive commands write the SAME
47
+ * three shared files (a service spec, a service contract, the one landscape),
48
+ * so two of them overlapping is not a merge to be resolved but a merge one of
49
+ * them computed against a snapshot that no longer exists. Two archives used to
50
+ * plan against the same landscape bytes and the second's splice silently
51
+ * dropped the first's additions — exit 0 on both, and a `validate` that stayed
52
+ * green because the surviving document was perfectly coherent.
53
+ *
54
+ * link(2) over a fully-written sibling is the primitive: as atomic as the
55
+ * O_EXCL create it replaced, needs no daemon, and leaves a file a human can
56
+ * read and delete — but unlike open-then-write, the lock file can never be
57
+ * OBSERVED empty. `writeFile(path, …, "wx")` published existence first and
58
+ * payload a syscall later, and in that window (or after a crash inside it) the
59
+ * lock named no holder: `doctor` now grades exactly that shape as damage whose
60
+ * fix is deletion, so the healthy path must never wear it, even for a
61
+ * microsecond. A create that dies before its link leaves only a staging temp
62
+ * `doctor` already names as litter. A holder that died without releasing would
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.
67
+ */
68
+ /**
69
+ * Distinguishes concurrent acquires INSIDE one process. `tempPath` salts with
70
+ * pid and a millisecond timestamp, which is unique across processes and not
71
+ * within one: two in-process acquirers in the same millisecond shared a temp
72
+ * 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.
76
+ */
77
+ let lockAcquireSeq = 0;
78
+ export async function acquireDocsLock(docsDir) {
79
+ const path = join(docsDir, DOCS_LOCK);
80
+ const payload = `${JSON.stringify({ pid: process.pid, host: hostname(), at: new Date().toISOString() })}\n`;
81
+ lockAcquireSeq += 1;
82
+ const tmp = tempPath(path, lockAcquireSeq);
83
+ await writeFile(tmp, payload);
84
+ try {
85
+ for (let attempt = 0;; attempt += 1) {
86
+ try {
87
+ await link(tmp, path);
88
+ }
89
+ catch (err) {
90
+ if (err.code !== "EEXIST")
91
+ throw err;
92
+ if (attempt === 0 && (await breakStaleLock(path)))
93
+ continue;
94
+ throw new DocsBusyError(path, await describeLock(path));
95
+ }
96
+ let released = false;
97
+ return async () => {
98
+ if (released)
99
+ return;
100
+ released = true;
101
+ await quietRm(path);
102
+ };
103
+ }
104
+ }
105
+ finally {
106
+ await quietRm(tmp);
107
+ }
108
+ }
109
+ /**
110
+ * Take the docs lock, waiting out a live holder instead of refusing on first
111
+ * contact. Same lock, different caller contract: `archive` refuses fast because
112
+ * its holder may be mid-merge for seconds and its caller can re-run, but two
113
+ * `verify --record` runs for different services of one feature are BOTH
114
+ * supposed to land — the second must wait for the first's sub-second window and
115
+ * then merge over the record it left, or one service's attestation is refused
116
+ * for no reason a user can see. The wait is bounded: a holder that outlives
117
+ * `waitMs` yields the same `DocsBusyError` the fast form throws, so the caller
118
+ * maps it to `docs-busy` either way and nothing hangs on a wedged lock.
119
+ */
120
+ export async function acquireDocsLockWaiting(docsDir, waitMs) {
121
+ const deadline = Date.now() + waitMs;
122
+ for (;;) {
123
+ try {
124
+ return await acquireDocsLock(docsDir);
125
+ }
126
+ catch (err) {
127
+ if (!(err instanceof DocsBusyError) || Date.now() >= deadline)
128
+ throw err;
129
+ }
130
+ // Short enough that a released lock is picked up before a human notices;
131
+ // long enough that the poll does not busy-spin the directory.
132
+ await new Promise((resolve) => setTimeout(resolve, 25));
133
+ }
134
+ }
135
+ async function readLockHolder(path) {
136
+ let text;
137
+ try {
138
+ text = await readFile(path, "utf8");
139
+ }
140
+ catch (err) {
141
+ return err.code === "ENOENT" ? { kind: "absent" } : { kind: "unreadable" };
142
+ }
143
+ let holder;
144
+ try {
145
+ holder = JSON.parse(text);
146
+ }
147
+ catch {
148
+ return { kind: "unreadable" };
149
+ }
150
+ // A lock file holding `null` (or a bare string, or an array) is JSON this
151
+ // parses and nothing else understands. The dereference below used to run
152
+ // outside the guard in `acquireDocsLock`'s breaker and threw, replacing the
153
+ // `docs-busy` refusal — the designed answer — with an `internal` envelope.
154
+ if (!isRecord(holder) || typeof holder.host !== "string" || typeof holder.pid !== "number") {
155
+ return { kind: "unreadable" };
156
+ }
157
+ return { kind: "held", host: holder.host, pid: holder.pid, at: typeof holder.at === "string" ? holder.at : undefined };
158
+ }
159
+ /** Does this pid no longer exist on this host? Signal 0 asks without delivering; EPERM is alive-but-somebody-else's. */
160
+ function pidGone(pid) {
161
+ try {
162
+ process.kill(pid, 0);
163
+ return false;
164
+ }
165
+ catch (err) {
166
+ return err.code === "ESRCH";
167
+ }
168
+ }
169
+ /** Remove a lock whose holder is a dead process on this same host. Returns whether it did. */
170
+ async function breakStaleLock(path) {
171
+ const holder = await readLockHolder(path);
172
+ // Unreadable is deliberately NOT breakable: a lock we cannot understand is
173
+ // the one case where guessing is worst. `doctor` reports it as damage.
174
+ if (holder.kind !== "held" || holder.host !== hostname() || !pidGone(holder.pid))
175
+ return false;
176
+ try {
177
+ await unlink(path);
178
+ return true;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ export async function describeLock(path) {
185
+ const holder = await readLockHolder(path);
186
+ return holder.kind === "held" ? `pid ${holder.pid} on ${holder.host} since ${String(holder.at)}` : `see ${path}`;
187
+ }
188
+ /**
189
+ * The lock's whole state for `doctor`, from ONE read. Null means no lock.
190
+ * Three sequential reads used to answer this — exists, describe, stale — and a
191
+ * lock released between them made `doctor` describe a file that was gone,
192
+ * escalating an ordinary sub-second commit window into a blocker.
193
+ */
194
+ export async function lockResidue(path) {
195
+ const holder = await readLockHolder(path);
196
+ if (holder.kind === "absent")
197
+ return null;
198
+ if (holder.kind === "unreadable")
199
+ return { holder: `see ${path}`, stale: false, unreadable: true };
200
+ return {
201
+ holder: `pid ${holder.pid} on ${holder.host} since ${String(holder.at)}`,
202
+ stale: holder.host === hostname() && pidGone(holder.pid),
203
+ unreadable: false,
204
+ };
205
+ }
206
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * What an unfinished write means for `loam validate`.
3
+ *
4
+ * `core/doctor/residue.ts` grades the same fact for `doctor`; this module
5
+ * answers for `loam validate --all` — the fleet gate — and `loam gate`, the
6
+ * deploy-time query, reuses the same finding verbatim for its interrupted
7
+ * check. A `.loam-commit` in the repo means the last writer's commit never finished:
8
+ * some files hold the new bytes, some the old, and a validate that grades
9
+ * that tree green is certifying a state no run ever produced. In CI that
10
+ * green MERGES, which is why this is an error finding and not a warning.
11
+ */
12
+ import { existsSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { COMMIT_INTENT } from "../interrupted.js";
15
+ import {} from "../../vocabulary/report.js";
16
+ import { readTxnIntent } from "../txn/journal.js";
17
+ import { readCommitIntent } from "./intent.js";
18
+ /** The error every validate mode leads with while a journal sits in the repo, or null. */
19
+ export async function interruptedCommitFinding(docsDir) {
20
+ if (!existsSync(join(docsDir, COMMIT_INTENT)))
21
+ return null;
22
+ const intent = (await readCommitIntent(docsDir)) ?? (await readTxnIntent(docsDir));
23
+ const what = intent === null
24
+ ? `a commit this loam cannot read — check the living docs against version control, delete ${COMMIT_INTENT}, then re-run`
25
+ : "rerun" in intent
26
+ ? `an interrupted \`${intent.rerun}\` — re-running it recovers first, and \`loam doctor\` names the pending files and directory moves`
27
+ : `an interrupted \`loam ${intent.command} ${intent.feature}\` — re-running it recovers first, and \`loam doctor\` names the files`;
28
+ return {
29
+ severity: "error",
30
+ code: "docs.commit-interrupted",
31
+ message: `${COMMIT_INTENT} records ${what}. ` +
32
+ "Until it is gone the files under this journal's root may be half-committed, so nothing graded from them can be trusted to describe one state.",
33
+ };
34
+ }
35
+ //# sourceMappingURL=finding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finding.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/finding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAgB,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,0FAA0F;AAC1F,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,OAAe;IAC5D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,MAAM,GAAG,CAAC,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACnF,MAAM,IAAI,GACR,MAAM,KAAK,IAAI;QACb,CAAC,CAAC,0FAA0F,aAAa,eAAe;QACxH,CAAC,CAAC,OAAO,IAAI,MAAM;YACjB,CAAC,CAAC,oBAAoB,MAAM,CAAC,KAAK,oGAAoG;YACtI,CAAC,CAAC,yBAAyB,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,wEAAwE,CAAC;IAC1I,OAAO;QACL,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,yBAAyB;QAC/B,OAAO,EACL,GAAG,aAAa,YAAY,IAAI,IAAI;YACpC,+IAA+I;KAClJ,CAAC;AACJ,CAAC","sourcesContent":["/**\n * What an unfinished write means for `loam validate`.\n *\n * `core/doctor/residue.ts` grades the same fact for `doctor`; this module\n * answers for `loam validate --all` — the fleet gate — and `loam gate`, the\n * deploy-time query, reuses the same finding verbatim for its interrupted\n * check. A `.loam-commit` in the repo means the last writer's commit never finished:\n * some files hold the new bytes, some the old, and a validate that grades\n * that tree green is certifying a state no run ever produced. In CI that\n * green MERGES, which is why this is an error finding and not a warning.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { COMMIT_INTENT } from \"../interrupted.js\";\nimport { type Finding } from \"../../vocabulary/report.js\";\nimport { readTxnIntent } from \"../txn/journal.js\";\nimport { readCommitIntent } from \"./intent.js\";\n\n/** The error every validate mode leads with while a journal sits in the repo, or null. */\nexport async function interruptedCommitFinding(docsDir: string): Promise<Finding | null> {\n if (!existsSync(join(docsDir, COMMIT_INTENT))) return null;\n const intent = (await readCommitIntent(docsDir)) ?? (await readTxnIntent(docsDir));\n const what =\n intent === null\n ? `a commit this loam cannot read — check the living docs against version control, delete ${COMMIT_INTENT}, then re-run`\n : \"rerun\" in intent\n ? `an interrupted \\`${intent.rerun}\\` — re-running it recovers first, and \\`loam doctor\\` names the pending files and directory moves`\n : `an interrupted \\`loam ${intent.command} ${intent.feature}\\` — re-running it recovers first, and \\`loam doctor\\` names the files`;\n return {\n severity: \"error\",\n code: \"docs.commit-interrupted\",\n message:\n `${COMMIT_INTENT} records ${what}. ` +\n \"Until it is gone the files under this journal's root may be half-committed, so nothing graded from them can be trusted to describe one state.\",\n };\n}\n"]}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The `.loam-commit` journal — written before the first swap, cleared after the
3
+ * last — and the record that makes an interrupted commit detectable at all.
4
+ *
5
+ * Reading and writing the record is kept apart from acting on it
6
+ * (`./recover.ts`) because a corrupt record has to stay describable: `doctor`
7
+ * reports a `.loam-commit` it cannot parse, and a module that parsed and
8
+ * repaired in one step would have no way to say that without also attempting
9
+ * the repair.
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import { readFile } from "node:fs/promises";
13
+ import { hostname } from "node:os";
14
+ import { join } from "node:path";
15
+ import { repoPath } from "../../envelope/json.js";
16
+ import { isPathInside } from "../../kernel/path-safety.js";
17
+ import { COMMIT_INTENT, writeDurable } from "../interrupted.js";
18
+ import { isDigest, sha256 } from "../writes.js";
19
+ import { quietRm } from "../commit.js";
20
+ /** The version-1 record: archive's and unarchive's snapshot-backed journal. */
21
+ const COMMIT_INTENT_VERSION = 1;
22
+ /**
23
+ * Write the intent record and get it onto the disk before the first swap.
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.
30
+ */
31
+ export async function writeCommitIntent(docsDir, spec, staged) {
32
+ const intent = {
33
+ version: COMMIT_INTENT_VERSION,
34
+ command: spec.command,
35
+ restore: spec.restore,
36
+ pid: process.pid,
37
+ host: hostname(),
38
+ at: new Date().toISOString(),
39
+ feature: spec.feature,
40
+ moveFrom: repoPath(docsDir, spec.moveFrom),
41
+ moveTo: repoPath(docsDir, spec.moveTo),
42
+ files: staged.map((s) => ({
43
+ path: repoPath(docsDir, s.write.path),
44
+ before: s.before === null ? null : sha256(s.before),
45
+ after: s.content === null ? null : sha256(s.content),
46
+ })),
47
+ };
48
+ await writeDurable(join(docsDir, COMMIT_INTENT), JSON.stringify(intent, null, 2) + "\n");
49
+ }
50
+ /** The commit finished (or was rolled back): there is nothing left to recover from. */
51
+ export async function clearCommitIntent(docsDir) {
52
+ await quietRm(join(docsDir, COMMIT_INTENT));
53
+ }
54
+ /** The intent record as written, or null when it is absent or unreadable. */
55
+ export async function readCommitIntent(docsDir) {
56
+ const path = join(docsDir, COMMIT_INTENT);
57
+ if (!existsSync(path))
58
+ return null;
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(await readFile(path, "utf8"));
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ const i = parsed;
67
+ if (i === null || typeof i !== "object")
68
+ return null;
69
+ if (i.version !== COMMIT_INTENT_VERSION)
70
+ return null;
71
+ if (i.command !== "archive" && i.command !== "unarchive")
72
+ return null;
73
+ if (i.restore !== "before" && i.restore !== "after")
74
+ return null;
75
+ if (typeof i.feature !== "string" || typeof i.moveFrom !== "string" || typeof i.moveTo !== "string")
76
+ return null;
77
+ if (!Array.isArray(i.files))
78
+ return null;
79
+ // The journal is untrusted input, and recovery writes over what it names —
80
+ // the same containment the version-2 reader enforces, because an entry
81
+ // escaping the docs repo would turn repair into an arbitrary write.
82
+ const inside = (rel) => isPathInside(join(docsDir), join(docsDir, ...rel.split("/")));
83
+ if (!inside(i.moveFrom) || !inside(i.moveTo))
84
+ return null;
85
+ for (const f of i.files) {
86
+ if (typeof f?.path !== "string")
87
+ return null;
88
+ if (!isDigestOrNull(f.before) || !isDigestOrNull(f.after))
89
+ return null;
90
+ if (!inside(f.path))
91
+ return null;
92
+ }
93
+ return i;
94
+ }
95
+ function isDigestOrNull(value) {
96
+ return value === null || isDigest(value);
97
+ }
98
+ //# sourceMappingURL=intent.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Refuse or repair an interrupted commit, before this run reads anything.
3
+ *
4
+ * Split from `./intent.ts` because this is the only module on the write path
5
+ * that writes bytes nobody asked for in this run. Two journal versions, two
6
+ * doctrines, one dispatch: a version-1 record (archive/unarchive) is repaired
7
+ * BACKWARD from the snapshot pre-images, each verified against the digest the
8
+ * record captured; a version-2 record is handed to `txn/forward.ts`, which
9
+ * rolls FORWARD from the staged temps under the same verification. Either
10
+ * way, a file in NEITHER recorded state was written by somebody else since,
11
+ * so recovery refuses and names it rather than choosing which of two truths
12
+ * to destroy.
13
+ */
14
+ import { existsSync } from "node:fs";
15
+ import { readFile, rm } from "node:fs/promises";
16
+ import { dirname, join } from "node:path";
17
+ import { repoPath } from "../../envelope/json.js";
18
+ import { sha256 } from "../writes.js";
19
+ import { atomicWrite, quietRmdir } from "../commit.js";
20
+ import { SNAPSHOT_DIR } from "../snapshot.js";
21
+ import { COMMIT_INTENT, InterruptedCommitError, sweepPidTemps, } from "../interrupted.js";
22
+ import { readTxnIntent } from "../txn/journal.js";
23
+ import { recoverForward } from "../txn/forward.js";
24
+ import { clearCommitIntent, readCommitIntent, } from "./intent.js";
25
+ /**
26
+ * Refuse or repair before this run reads anything. Returns null when there is
27
+ * nothing to recover.
28
+ *
29
+ * Repair is only ever the bytes the interrupted run already had on disk: each
30
+ * file still in the wrong state is rewritten from the snapshot pre-image and
31
+ * only after that pre-image's own sha256 matches the digest the record captured
32
+ * before the crash. A file in NEITHER state was written by somebody else since,
33
+ * so this refuses and names it rather than choosing which of two truths to
34
+ * destroy — the same doctrine `rollbackStaged` follows when it leaves a file as
35
+ * found.
36
+ */
37
+ export async function recoverInterruptedCommit(docsDir) {
38
+ const path = join(docsDir, COMMIT_INTENT);
39
+ if (!existsSync(path))
40
+ return null;
41
+ const intent = await readCommitIntent(docsDir);
42
+ if (intent === null) {
43
+ // Version 2 — the smaller transaction's journal. Same file, different
44
+ // recovery: forward from the staged temps, in txn/forward.ts.
45
+ const txn = await readTxnIntent(docsDir);
46
+ if (txn !== null)
47
+ return recoverForward(docsDir, txn);
48
+ throw new InterruptedCommitError(`${repoPath(docsDir, path)} records a commit this loam cannot read, so it cannot tell a half-merged docs repo ` +
49
+ `from a healthy one. Check the living docs against version control, delete ${COMMIT_INTENT}, then re-run.`);
50
+ }
51
+ const moveFrom = join(docsDir, ...intent.moveFrom.split("/"));
52
+ const moveTo = join(docsDir, ...intent.moveTo.split("/"));
53
+ // The move is the commit's LAST step. If it landed, every swap before it did
54
+ // too, and only the record's own removal was lost.
55
+ if (existsSync(moveTo) && !existsSync(moveFrom)) {
56
+ await finishRecovery(docsDir, intent);
57
+ return { command: intent.command, feature: intent.feature, outcome: "completed", repaired: [] };
58
+ }
59
+ const target = (rel) => join(docsDir, ...rel.split("/"));
60
+ const wanted = (f) => (intent.restore === "before" ? f.before : f.after);
61
+ const other = (f) => (intent.restore === "before" ? f.after : f.before);
62
+ const pending = [];
63
+ const unknown = [];
64
+ for (const f of intent.files) {
65
+ const file = target(f.path);
66
+ const now = existsSync(file) ? sha256(await readFile(file)) : null;
67
+ if (now === wanted(f))
68
+ continue;
69
+ if (now === other(f))
70
+ pending.push(f);
71
+ else
72
+ unknown.push(f.path);
73
+ }
74
+ if (unknown.length > 0) {
75
+ throw new InterruptedCommitError(`a \`loam ${intent.command} ${intent.feature}\` was interrupted mid-commit (pid ${intent.pid} on ${intent.host}, ` +
76
+ `${intent.at}) and ${unknown.length} of the file(s) it was writing now hold bytes that are neither what it ` +
77
+ `found nor what it wrote — somebody edited them since: ${unknown.join(", ")}. loam will not choose between ` +
78
+ `those two truths. Reconcile them against version control, delete ${COMMIT_INTENT}, then re-run.`);
79
+ }
80
+ if (pending.length === 0) {
81
+ await finishRecovery(docsDir, intent);
82
+ return { command: intent.command, feature: intent.feature, outcome: "consistent", repaired: [] };
83
+ }
84
+ // The pre-images travel inside the feature directory, so they are at one of
85
+ // the two ends of the move depending on how far the interrupted run got.
86
+ // Deliberately NOT the snapshot's (service, artifact) re-keying: the journal
87
+ // 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.
92
+ const roots = [join(moveFrom, SNAPSHOT_DIR, "files"), join(moveTo, SNAPSHOT_DIR, "files")];
93
+ const repaired = [];
94
+ for (const f of pending) {
95
+ const file = target(f.path);
96
+ const want = wanted(f);
97
+ if (want === null) {
98
+ await rm(file, { force: true });
99
+ await quietRmdir(dirname(file));
100
+ repaired.push(f.path);
101
+ continue;
102
+ }
103
+ const source = roots.map((r) => join(r, ...f.path.split("/"))).find((p) => existsSync(p));
104
+ if (source === undefined) {
105
+ throw new InterruptedCommitError(`a \`loam ${intent.command} ${intent.feature}\` was interrupted mid-commit and ${f.path} is half-written, but ` +
106
+ `the pre-image that would repair it is gone. Restore ${f.path} from version control, delete ` +
107
+ `${COMMIT_INTENT}, then re-run.`);
108
+ }
109
+ const bytes = await readFile(source);
110
+ if (sha256(bytes) !== want) {
111
+ throw new InterruptedCommitError(`a \`loam ${intent.command} ${intent.feature}\` was interrupted mid-commit and ${f.path} is half-written, but ` +
112
+ `the pre-image that would repair it no longer matches the digest recorded before the commit — it has been ` +
113
+ `edited since. Restore ${f.path} from version control, delete ${COMMIT_INTENT}, then re-run.`);
114
+ }
115
+ await atomicWrite(file, bytes);
116
+ repaired.push(f.path);
117
+ }
118
+ await finishRecovery(docsDir, intent);
119
+ return { command: intent.command, feature: intent.feature, outcome: "repaired", repaired };
120
+ }
121
+ /** Drop the record, and (via the shared pid-scoped sweep) the killed run's own temps. */
122
+ async function finishRecovery(docsDir, intent) {
123
+ await sweepPidTemps(intent.files.map((f) => dirname(join(docsDir, ...f.path.split("/")))), intent.pid);
124
+ await clearCommitIntent(docsDir);
125
+ }
126
+ //# sourceMappingURL=recover.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Everything in a docs repo that only a write which did not finish can produce,
3
+ * gathered for `doctor` to grade.
4
+ *
5
+ * It lives beside the write path rather than in `doctor` because these are
6
+ * staging's own artefacts and it does not re-spell any of them: `.loam-lock`
7
+ * comes from `../lock.ts`, `.loam-commit` from `./intent.ts`, and the temp-file
8
+ * pattern from `../commit.ts`. A detector that spelled them a second time would
9
+ * go stale the first time any of the three changed. This module reports;
10
+ * `doctor` decides what the report means.
11
+ */
12
+ import { existsSync } from "node:fs";
13
+ import { readdir } from "node:fs/promises";
14
+ import { join } from "node:path";
15
+ import { repoPath } from "../../envelope/json.js";
16
+ import { DOCS_LOCK, lockResidue } from "../lock.js";
17
+ import { TEMP_FILE_RE } from "../commit.js";
18
+ import { COMMIT_INTENT } from "../interrupted.js";
19
+ import { readTxnIntent } from "../txn/journal.js";
20
+ import { readCommitIntent } from "./intent.js";
21
+ export async function scanWritePathResidue(docsDir) {
22
+ const lockPath = join(docsDir, DOCS_LOCK);
23
+ // One read decides everything about the lock. An exists-then-describe-then-
24
+ // grade sequence read the file up to four times, and a lock released between
25
+ // reads — the ordinary sub-second commit window of a healthy archive — could
26
+ // be reported in a state no single moment ever had.
27
+ const residue = await lockResidue(lockPath);
28
+ const lock = residue === null ? null : { path: repoPath(docsDir, lockPath), ...residue };
29
+ // Both journal versions wear the same filename; a version-2 record must
30
+ // not be graded "unreadable" merely because the version-1 reader rejects
31
+ // its version field.
32
+ const intent = (await readCommitIntent(docsDir)) ?? (await readTxnIntent(docsDir));
33
+ return {
34
+ lock,
35
+ intent,
36
+ intentUnreadable: intent === null && existsSync(join(docsDir, COMMIT_INTENT)),
37
+ temps: (await tempLeftovers(docsDir)).map((p) => repoPath(docsDir, p)).sort(),
38
+ };
39
+ }
40
+ async function tempLeftovers(dir) {
41
+ const out = [];
42
+ let entries;
43
+ try {
44
+ entries = await readdir(dir, { withFileTypes: true });
45
+ }
46
+ catch {
47
+ return out;
48
+ }
49
+ for (const entry of entries) {
50
+ // `.git` alone can hold more files than the whole docs repo, and nothing
51
+ // loam writes ever lands in it.
52
+ if (entry.name === ".git" || entry.name === "node_modules")
53
+ continue;
54
+ const path = join(dir, entry.name);
55
+ if (entry.isDirectory())
56
+ out.push(...(await tempLeftovers(path)));
57
+ else if (entry.isFile() && TEMP_FILE_RE.test(entry.name))
58
+ out.push(path);
59
+ }
60
+ return out;
61
+ }
62
+ //# sourceMappingURL=residue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"residue.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/residue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAkB,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAqB,MAAM,aAAa,CAAC;AAsBlE,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,OAAe;IACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC1C,4EAA4E;IAC5E,6EAA6E;IAC7E,6EAA6E;IAC7E,oDAAoD;IACpD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,IAAI,GACR,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC;IAC9E,wEAAwE;IACxE,yEAAyE;IACzE,qBAAqB;IACrB,MAAM,MAAM,GAAG,CAAC,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACnF,OAAO;QACL,IAAI;QACJ,MAAM;QACN,gBAAgB,EAAE,MAAM,KAAK,IAAI,IAAI,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QAC7E,KAAK,EAAE,CAAC,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;KAC9E,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,GAAW;IACtC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC;IACb,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,yEAAyE;QACzE,gCAAgC;QAChC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc;YAAE,SAAS;QACrE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,KAAK,CAAC,WAAW,EAAE;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;aAC7D,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * Everything in a docs repo that only a write which did not finish can produce,\n * gathered for `doctor` to grade.\n *\n * It lives beside the write path rather than in `doctor` because these are\n * staging's own artefacts and it does not re-spell any of them: `.loam-lock`\n * comes from `../lock.ts`, `.loam-commit` from `./intent.ts`, and the temp-file\n * pattern from `../commit.ts`. A detector that spelled them a second time would\n * go stale the first time any of the three changed. This module reports;\n * `doctor` decides what the report means.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { DOCS_LOCK, lockResidue } from \"../lock.js\";\nimport { TEMP_FILE_RE } from \"../commit.js\";\nimport { COMMIT_INTENT } from \"../interrupted.js\";\nimport { readTxnIntent, type TxnIntent } from \"../txn/journal.js\";\nimport { readCommitIntent, type CommitIntent } from \"./intent.js\";\n\n/** The four shapes an unfinished write leaves behind, each named by its cause. */\nexport interface WritePathResidue {\n /**\n * A held `.loam-lock`; whether its holder is a process that no longer exists\n * on this host; and whether the file cannot name a holder at all — in which\n * case nothing will ever release it and `stale` is meaningless.\n */\n lock: { path: string; holder: string; stale: boolean; unreadable: boolean } | null;\n /**\n * An interrupted commit's intent record — present means a commit was in\n * flight when something killed it. Either journal version: the consumer\n * tells them apart by shape (`rerun` exists only on version 2).\n */\n intent: CommitIntent | TxnIntent | null;\n /** True when `.loam-commit` exists but cannot be parsed: the worst case, because nothing can grade it. */\n intentUnreadable: boolean;\n /** Docs-repo-relative paths of orphaned `.loam-*.tmp` staging files. */\n temps: string[];\n}\n\nexport async function scanWritePathResidue(docsDir: string): Promise<WritePathResidue> {\n const lockPath = join(docsDir, DOCS_LOCK);\n // One read decides everything about the lock. An exists-then-describe-then-\n // grade sequence read the file up to four times, and a lock released between\n // reads — the ordinary sub-second commit window of a healthy archive — could\n // be reported in a state no single moment ever had.\n const residue = await lockResidue(lockPath);\n const lock: WritePathResidue[\"lock\"] =\n residue === null ? null : { path: repoPath(docsDir, lockPath), ...residue };\n // Both journal versions wear the same filename; a version-2 record must\n // not be graded \"unreadable\" merely because the version-1 reader rejects\n // its version field.\n const intent = (await readCommitIntent(docsDir)) ?? (await readTxnIntent(docsDir));\n return {\n lock,\n intent,\n intentUnreadable: intent === null && existsSync(join(docsDir, COMMIT_INTENT)),\n temps: (await tempLeftovers(docsDir)).map((p) => repoPath(docsDir, p)).sort(),\n };\n}\n\nasync function tempLeftovers(dir: string): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n // `.git` alone can hold more files than the whole docs repo, and nothing\n // loam writes ever lands in it.\n if (entry.name === \".git\" || entry.name === \"node_modules\") continue;\n const path = join(dir, entry.name);\n if (entry.isDirectory()) out.push(...(await tempLeftovers(path)));\n else if (entry.isFile() && TEMP_FILE_RE.test(entry.name)) out.push(path);\n }\n return out;\n}\n\n"]}